deps.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. """
  2. FastAPI dependencies for authentication and authorization.
  3. """
  4. from typing import Annotated
  5. from fastapi import Depends, HTTPException, status
  6. from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
  7. from sqlalchemy import select
  8. from sqlalchemy.ext.asyncio import AsyncSession
  9. from app.core.database import get_db
  10. from app.core.security import verify_token
  11. from app.models.device import Device
  12. from app.models.user import User
  13. # HTTP Bearer security scheme
  14. security = HTTPBearer()
  15. async def get_current_user(
  16. credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)],
  17. db: Annotated[AsyncSession, Depends(get_db)],
  18. ) -> User:
  19. """
  20. Get current authenticated user from JWT token.
  21. Args:
  22. credentials: HTTP Bearer token from Authorization header
  23. db: Database session
  24. Returns:
  25. User model instance
  26. Raises:
  27. HTTPException: If token is invalid or user not found
  28. """
  29. # Verify token
  30. token = credentials.credentials
  31. payload = verify_token(token, expected_type="access")
  32. if payload is None:
  33. raise HTTPException(
  34. status_code=status.HTTP_401_UNAUTHORIZED,
  35. detail="Invalid or expired token",
  36. headers={"WWW-Authenticate": "Bearer"},
  37. )
  38. # Extract user_id from token (sub is stored as string in JWT)
  39. user_id_str: str = payload.get("sub")
  40. if user_id_str is None:
  41. raise HTTPException(
  42. status_code=status.HTTP_401_UNAUTHORIZED,
  43. detail="Invalid token payload",
  44. )
  45. try:
  46. user_id = int(user_id_str)
  47. except (ValueError, TypeError):
  48. raise HTTPException(
  49. status_code=status.HTTP_401_UNAUTHORIZED,
  50. detail="Invalid user ID in token",
  51. )
  52. # Get user from database
  53. result = await db.execute(select(User).where(User.id == user_id))
  54. user = result.scalar_one_or_none()
  55. if user is None:
  56. raise HTTPException(
  57. status_code=status.HTTP_401_UNAUTHORIZED,
  58. detail="User not found",
  59. )
  60. if user.status != "active":
  61. raise HTTPException(
  62. status_code=status.HTTP_403_FORBIDDEN,
  63. detail=f"User account is {user.status}",
  64. )
  65. return user
  66. async def get_current_superadmin(
  67. current_user: Annotated[User, Depends(get_current_user)],
  68. ) -> User:
  69. """
  70. Verify that current user is a superadmin.
  71. Args:
  72. current_user: Current authenticated user
  73. Returns:
  74. User model instance
  75. Raises:
  76. HTTPException: If user is not a superadmin
  77. """
  78. if not current_user.is_superadmin:
  79. raise HTTPException(
  80. status_code=status.HTTP_403_FORBIDDEN,
  81. detail="Superadmin access required",
  82. )
  83. return current_user
  84. async def get_current_owner(
  85. current_user: Annotated[User, Depends(get_current_user)],
  86. ) -> User:
  87. """
  88. Verify that current user is an organization owner.
  89. Args:
  90. current_user: Current authenticated user
  91. Returns:
  92. User model instance
  93. Raises:
  94. HTTPException: If user is not an owner
  95. """
  96. if not current_user.is_owner and not current_user.is_superadmin:
  97. raise HTTPException(
  98. status_code=status.HTTP_403_FORBIDDEN,
  99. detail="Owner access required",
  100. )
  101. return current_user
  102. async def get_current_active_user(
  103. current_user: Annotated[User, Depends(get_current_user)],
  104. ) -> User:
  105. """
  106. Verify that current user is active and email verified.
  107. Args:
  108. current_user: Current authenticated user
  109. Returns:
  110. User model instance
  111. Raises:
  112. HTTPException: If user is not active or email not verified
  113. """
  114. if not current_user.email_verified:
  115. raise HTTPException(
  116. status_code=status.HTTP_403_FORBIDDEN,
  117. detail="Email not verified",
  118. )
  119. return current_user
  120. async def get_current_device(
  121. credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)],
  122. db: Annotated[AsyncSession, Depends(get_db)],
  123. ) -> Device:
  124. """
  125. Get current authenticated device from JWT token.
  126. Args:
  127. credentials: HTTP Bearer token from Authorization header
  128. db: Database session
  129. Returns:
  130. Device model instance
  131. Raises:
  132. HTTPException: If token is invalid or device not found
  133. """
  134. # Verify token (don't check type yet, will verify it's "device" below)
  135. token = credentials.credentials
  136. payload = verify_token(token, expected_type=None)
  137. if payload is None:
  138. raise HTTPException(
  139. status_code=status.HTTP_401_UNAUTHORIZED,
  140. detail="Invalid or expired token",
  141. headers={"WWW-Authenticate": "Bearer"},
  142. )
  143. # Verify it's a device token
  144. token_type = payload.get("type")
  145. if token_type != "device":
  146. raise HTTPException(
  147. status_code=status.HTTP_401_UNAUTHORIZED,
  148. detail="Invalid token type, device token required",
  149. )
  150. # Extract device_id from token (sub is stored as string in JWT)
  151. device_id_str: str = payload.get("sub")
  152. if device_id_str is None:
  153. raise HTTPException(
  154. status_code=status.HTTP_401_UNAUTHORIZED,
  155. detail="Invalid token payload",
  156. )
  157. try:
  158. device_id = int(device_id_str)
  159. except (ValueError, TypeError):
  160. raise HTTPException(
  161. status_code=status.HTTP_401_UNAUTHORIZED,
  162. detail="Invalid device ID in token",
  163. )
  164. # Get device from database
  165. result = await db.execute(select(Device).where(Device.id == device_id))
  166. device = result.scalar_one_or_none()
  167. if device is None:
  168. raise HTTPException(
  169. status_code=status.HTTP_401_UNAUTHORIZED,
  170. detail="Device not found",
  171. )
  172. if device.status == "inactive":
  173. raise HTTPException(
  174. status_code=status.HTTP_403_FORBIDDEN,
  175. detail=f"Device is inactive",
  176. )
  177. return device