auth.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. from datetime import timedelta
  2. from fastapi import APIRouter, Depends, HTTPException, status
  3. from sqlalchemy import select
  4. from sqlalchemy.ext.asyncio import AsyncSession
  5. from backend.app.core.auth import (
  6. ACCESS_TOKEN_EXPIRE_MINUTES,
  7. authenticate_user,
  8. create_access_token,
  9. get_current_active_user,
  10. get_password_hash,
  11. get_user_by_username,
  12. )
  13. from backend.app.core.database import get_db
  14. from backend.app.models.settings import Settings
  15. from backend.app.models.user import User
  16. from backend.app.schemas.auth import LoginRequest, LoginResponse, SetupRequest, SetupResponse, UserResponse
  17. router = APIRouter(prefix="/auth", tags=["authentication"])
  18. async def is_auth_enabled(db: AsyncSession) -> bool:
  19. """Check if authentication is enabled."""
  20. result = await db.execute(select(Settings).where(Settings.key == "auth_enabled"))
  21. setting = result.scalar_one_or_none()
  22. return setting and setting.value.lower() == "true"
  23. async def set_auth_enabled(db: AsyncSession, enabled: bool) -> None:
  24. """Set authentication enabled status."""
  25. from sqlalchemy import func
  26. from sqlalchemy.dialects.sqlite import insert as sqlite_insert
  27. stmt = sqlite_insert(Settings).values(key="auth_enabled", value="true" if enabled else "false")
  28. stmt = stmt.on_conflict_do_update(index_elements=["key"], set_={"value": "true" if enabled else "false", "updated_at": func.now()})
  29. await db.execute(stmt)
  30. # Note: Don't commit here - let get_db handle it or commit explicitly in the route
  31. @router.post("/setup", response_model=SetupResponse)
  32. async def setup_auth(request: SetupRequest, db: AsyncSession = Depends(get_db)):
  33. """First-time setup: enable/disable authentication and create admin user."""
  34. import logging
  35. logger = logging.getLogger(__name__)
  36. try:
  37. # Check if auth is already configured (prevent re-setup)
  38. result = await db.execute(select(Settings).where(Settings.key == "auth_enabled"))
  39. existing_setting = result.scalar_one_or_none()
  40. # Check if users exist
  41. user_count_result = await db.execute(select(User))
  42. user_count = len(user_count_result.scalars().all())
  43. if existing_setting and user_count > 0:
  44. # Auth already configured and users exist - prevent re-setup
  45. raise HTTPException(
  46. status_code=status.HTTP_400_BAD_REQUEST,
  47. detail="Authentication is already configured. Use user management to modify users.",
  48. )
  49. # If auth_enabled is true but no users exist, allow re-setup (recovery scenario)
  50. admin_created = False
  51. if request.auth_enabled:
  52. if not request.admin_username or not request.admin_password:
  53. raise HTTPException(
  54. status_code=status.HTTP_400_BAD_REQUEST,
  55. detail="Admin username and password are required when enabling authentication",
  56. )
  57. # Check if admin already exists
  58. existing_admin = await get_user_by_username(db, request.admin_username)
  59. if existing_admin:
  60. raise HTTPException(
  61. status_code=status.HTTP_400_BAD_REQUEST,
  62. detail="Admin user already exists",
  63. )
  64. # Create admin user FIRST (before enabling auth)
  65. try:
  66. logger.info(f"Creating admin user: {request.admin_username}")
  67. admin_user = User(
  68. username=request.admin_username,
  69. password_hash=get_password_hash(request.admin_password),
  70. role="admin",
  71. is_active=True,
  72. )
  73. db.add(admin_user)
  74. logger.info(f"Admin user added to session: {request.admin_username}")
  75. admin_created = True
  76. except Exception as e:
  77. await db.rollback()
  78. logger.error(f"Failed to create admin user: {e}", exc_info=True)
  79. raise HTTPException(
  80. status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
  81. detail=f"Failed to create admin user: {str(e)}",
  82. )
  83. # Set auth enabled and commit everything together
  84. await set_auth_enabled(db, request.auth_enabled)
  85. await db.commit()
  86. if admin_created:
  87. await db.refresh(admin_user)
  88. logger.info(f"Admin user created successfully: {admin_user.id}")
  89. return SetupResponse(auth_enabled=request.auth_enabled, admin_created=admin_created)
  90. except HTTPException:
  91. raise
  92. except Exception as e:
  93. logger.error(f"Setup error: {e}", exc_info=True)
  94. await db.rollback()
  95. raise HTTPException(
  96. status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
  97. detail=f"Setup failed: {str(e)}",
  98. )
  99. @router.get("/status")
  100. async def get_auth_status(db: AsyncSession = Depends(get_db)):
  101. """Get authentication status (public endpoint)."""
  102. auth_enabled = await is_auth_enabled(db)
  103. return {"auth_enabled": auth_enabled, "requires_setup": not auth_enabled}
  104. @router.post("/disable", response_model=dict)
  105. async def disable_auth(
  106. current_user: User = Depends(get_current_active_user),
  107. db: AsyncSession = Depends(get_db),
  108. ):
  109. """Disable authentication (admin only)."""
  110. import logging
  111. logger = logging.getLogger(__name__)
  112. # Only admins can disable authentication
  113. if current_user.role != "admin":
  114. raise HTTPException(
  115. status_code=status.HTTP_403_FORBIDDEN,
  116. detail="Only admins can disable authentication",
  117. )
  118. try:
  119. await set_auth_enabled(db, False)
  120. await db.commit()
  121. logger.info(f"Authentication disabled by admin user: {current_user.username}")
  122. return {"message": "Authentication disabled successfully", "auth_enabled": False}
  123. except Exception as e:
  124. await db.rollback()
  125. logger.error(f"Failed to disable authentication: {e}", exc_info=True)
  126. raise HTTPException(
  127. status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
  128. detail=f"Failed to disable authentication: {str(e)}",
  129. )
  130. @router.post("/login", response_model=LoginResponse)
  131. async def login(request: LoginRequest, db: AsyncSession = Depends(get_db)):
  132. """Login and get access token."""
  133. # Check if auth is enabled
  134. auth_enabled = await is_auth_enabled(db)
  135. if not auth_enabled:
  136. raise HTTPException(
  137. status_code=status.HTTP_400_BAD_REQUEST,
  138. detail="Authentication is not enabled",
  139. )
  140. user = await authenticate_user(db, request.username, request.password)
  141. if not user:
  142. raise HTTPException(
  143. status_code=status.HTTP_401_UNAUTHORIZED,
  144. detail="Incorrect username or password",
  145. headers={"WWW-Authenticate": "Bearer"},
  146. )
  147. access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
  148. access_token = create_access_token(data={"sub": user.username}, expires_delta=access_token_expires)
  149. return LoginResponse(
  150. access_token=access_token,
  151. token_type="bearer",
  152. user=UserResponse(
  153. id=user.id,
  154. username=user.username,
  155. role=user.role,
  156. is_active=user.is_active,
  157. created_at=user.created_at.isoformat(),
  158. ),
  159. )
  160. @router.get("/me", response_model=UserResponse)
  161. async def get_current_user_info(current_user: User = Depends(get_current_active_user)):
  162. """Get current user information."""
  163. return UserResponse(
  164. id=current_user.id,
  165. username=current_user.username,
  166. role=current_user.role,
  167. is_active=current_user.is_active,
  168. created_at=current_user.created_at.isoformat(),
  169. )
  170. @router.post("/logout")
  171. async def logout():
  172. """Logout (client should discard token)."""
  173. return {"message": "Logged out successfully"}