utils.py 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. import logging
  2. from datetime import UTC, datetime
  3. from uuid import UUID
  4. from fastapi import HTTPException
  5. from sqlalchemy import exists, select
  6. from sqlalchemy.orm import Session
  7. from sqlalchemy.sql import func
  8. from app.core.email import EmailDeliveryError, EmailSender
  9. from app.core.security import RESET_TOKEN_TTL, generate_reset_token
  10. from app.models import PasswordResetToken, Project, User
  11. logger = logging.getLogger(__name__)
  12. def assert_project_exists_or_404(session: Session, project_id: UUID) -> None:
  13. if not session.execute(select(exists().where(Project.id == project_id))).scalar():
  14. raise HTTPException(status_code=404, detail="Project not found")
  15. def get_project_or_404(session: Session, project_id: UUID) -> Project:
  16. p = session.get(Project, project_id)
  17. if p is None:
  18. raise HTTPException(status_code=404, detail="Project not found")
  19. return p
  20. def get_project_organization_id(session: Session, project_id: UUID) -> str:
  21. """Fetches only Project.organization_id -- avoids loading the full
  22. Project row (and its relationships) just to check existence/ownership."""
  23. org_id = session.execute(
  24. select(Project.organization_id).where(Project.id == project_id)
  25. ).scalar_one_or_none()
  26. if org_id is None:
  27. raise HTTPException(status_code=404, detail="Project not found")
  28. return org_id
  29. async def verify_id_list(
  30. session: Session,
  31. id_list: list[UUID],
  32. project_id: UUID,
  33. ObjectClass,
  34. error_message: str = "Invalid id list",
  35. ) -> None:
  36. """Verfiy the list of uuids exists and are from the right project
  37. ---
  38. Parameters
  39. - session : sqlachlemy Async session to use to verify slot validity
  40. - id_list : list of id to check
  41. - project_id :
  42. - ObjectClass : the ORM class of the object to check the ids from. Need to have id and project_id property
  43. ---
  44. Raise
  45. HTTPException(400, error_message) - if all the ids doesn't exists or are not associated with the right project
  46. """
  47. statement = select(func.count(ObjectClass.id)).where(
  48. ObjectClass.id.in_(id_list) & (ObjectClass.project_id == project_id)
  49. )
  50. results = session.execute(statement)
  51. if results.scalar() != len(id_list):
  52. raise HTTPException(status_code=400, detail=error_message)
  53. def update_object_from_payload(obj, payload: dict):
  54. """Update the ORM model object from a pydantic payload dictionary"""
  55. for attr_name, value in payload.items():
  56. setattr(obj, attr_name, value)
  57. async def issue_reset_token(
  58. session: Session, user: User, purpose: str, email_sender: EmailSender, base_url: str
  59. ) -> None:
  60. raw_token, token_hash = generate_reset_token()
  61. session.add(
  62. PasswordResetToken(
  63. user_id=user.id,
  64. token_hash=token_hash,
  65. expires_at=datetime.now(UTC) + RESET_TOKEN_TTL[purpose],
  66. purpose=purpose,
  67. )
  68. )
  69. session.commit()
  70. link = f"{base_url}/set-password?token={raw_token}"
  71. if purpose == "invite":
  72. subject, body = (
  73. "Vous avez été invité·e",
  74. f"Créez votre mot de passe : <a href='{link}'>{link}</a>",
  75. )
  76. else:
  77. subject, body = (
  78. "Réinitialisation du mot de passe",
  79. f"Réinitialisez ici : <a href='{link}'>{link}</a>",
  80. )
  81. try:
  82. await email_sender.send(user.email, subject, body)
  83. except EmailDeliveryError as exc:
  84. logger.error(
  85. "Failed to send %s email to user_id=%s email=%s: %s",
  86. purpose,
  87. user.id,
  88. user.email,
  89. exc,
  90. )