auth.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649
  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 sqlalchemy.orm import selectinload
  6. from backend.app.core.auth import (
  7. ACCESS_TOKEN_EXPIRE_MINUTES,
  8. authenticate_user,
  9. authenticate_user_by_email,
  10. create_access_token,
  11. get_current_active_user,
  12. get_password_hash,
  13. get_user_by_email,
  14. get_user_by_username,
  15. )
  16. from backend.app.core.database import get_db
  17. from backend.app.models.group import Group
  18. from backend.app.models.settings import Settings
  19. from backend.app.models.user import User
  20. from backend.app.schemas.auth import (
  21. ForgotPasswordRequest,
  22. ForgotPasswordResponse,
  23. GroupBrief,
  24. LoginRequest,
  25. LoginResponse,
  26. ResetPasswordRequest,
  27. ResetPasswordResponse,
  28. SetupRequest,
  29. SetupResponse,
  30. SMTPSettings,
  31. TestSMTPRequest,
  32. TestSMTPResponse,
  33. UserResponse,
  34. )
  35. from backend.app.services.email_service import (
  36. create_password_reset_email,
  37. generate_secure_password,
  38. get_smtp_settings,
  39. save_smtp_settings,
  40. send_email,
  41. )
  42. def _user_to_response(user: User) -> UserResponse:
  43. """Convert a User model to UserResponse schema."""
  44. return UserResponse(
  45. id=user.id,
  46. username=user.username,
  47. email=user.email,
  48. role=user.role,
  49. is_active=user.is_active,
  50. is_admin=user.is_admin,
  51. groups=[GroupBrief(id=g.id, name=g.name) for g in user.groups],
  52. permissions=sorted(user.get_permissions()),
  53. created_at=user.created_at.isoformat(),
  54. )
  55. router = APIRouter(prefix="/auth", tags=["authentication"])
  56. async def is_auth_enabled(db: AsyncSession) -> bool:
  57. """Check if authentication is enabled."""
  58. result = await db.execute(select(Settings).where(Settings.key == "auth_enabled"))
  59. setting = result.scalar_one_or_none()
  60. if setting is None:
  61. return False
  62. return setting.value.lower() == "true"
  63. async def is_advanced_auth_enabled(db: AsyncSession) -> bool:
  64. """Check if advanced authentication is enabled."""
  65. result = await db.execute(select(Settings).where(Settings.key == "advanced_auth_enabled"))
  66. setting = result.scalar_one_or_none()
  67. if setting is None:
  68. return False
  69. return setting.value.lower() == "true"
  70. async def set_advanced_auth_enabled(db: AsyncSession, enabled: bool) -> None:
  71. """Set advanced authentication enabled status."""
  72. from sqlalchemy import func
  73. from sqlalchemy.dialects.sqlite import insert as sqlite_insert
  74. stmt = sqlite_insert(Settings).values(key="advanced_auth_enabled", value="true" if enabled else "false")
  75. stmt = stmt.on_conflict_do_update(
  76. index_elements=["key"], set_={"value": "true" if enabled else "false", "updated_at": func.now()}
  77. )
  78. await db.execute(stmt)
  79. async def set_auth_enabled(db: AsyncSession, enabled: bool) -> None:
  80. """Set authentication enabled status."""
  81. from sqlalchemy import func
  82. from sqlalchemy.dialects.sqlite import insert as sqlite_insert
  83. stmt = sqlite_insert(Settings).values(key="auth_enabled", value="true" if enabled else "false")
  84. stmt = stmt.on_conflict_do_update(
  85. index_elements=["key"], set_={"value": "true" if enabled else "false", "updated_at": func.now()}
  86. )
  87. await db.execute(stmt)
  88. # Note: Don't commit here - let get_db handle it or commit explicitly in the route
  89. async def is_setup_completed(db: AsyncSession) -> bool:
  90. """Check if setup has been completed."""
  91. result = await db.execute(select(Settings).where(Settings.key == "setup_completed"))
  92. setting = result.scalar_one_or_none()
  93. return setting and setting.value.lower() == "true"
  94. async def set_setup_completed(db: AsyncSession, completed: bool) -> None:
  95. """Set setup completed status."""
  96. from sqlalchemy import func
  97. from sqlalchemy.dialects.sqlite import insert as sqlite_insert
  98. stmt = sqlite_insert(Settings).values(key="setup_completed", value="true" if completed else "false")
  99. stmt = stmt.on_conflict_do_update(
  100. index_elements=["key"], set_={"value": "true" if completed else "false", "updated_at": func.now()}
  101. )
  102. await db.execute(stmt)
  103. # Note: Don't commit here - let get_db handle it or commit explicitly in the route
  104. @router.post("/setup", response_model=SetupResponse)
  105. async def setup_auth(request: SetupRequest, db: AsyncSession = Depends(get_db)):
  106. """First-time setup: enable/disable authentication and create admin user."""
  107. import logging
  108. logger = logging.getLogger(__name__)
  109. try:
  110. # If auth_enabled is true but no users exist, allow re-setup (recovery scenario)
  111. admin_created = False
  112. if request.auth_enabled:
  113. # Check if admin users already exist
  114. admin_users_result = await db.execute(select(User).where(User.role == "admin"))
  115. existing_admin_users = list(admin_users_result.scalars().all())
  116. has_admin_users = len(existing_admin_users) > 0
  117. if has_admin_users:
  118. # Admin users already exist, just enable auth (don't create new admin)
  119. logger.info(
  120. f"Admin users already exist ({len(existing_admin_users)} found), enabling authentication without creating new admin"
  121. )
  122. admin_created = False
  123. else:
  124. # No admin users exist, require admin credentials to create first admin
  125. if not request.admin_username or not request.admin_password:
  126. raise HTTPException(
  127. status_code=status.HTTP_400_BAD_REQUEST,
  128. detail="Admin username and password are required when enabling authentication (no admin users exist)",
  129. )
  130. # Check if username already exists (shouldn't happen if no admin users exist, but check anyway)
  131. existing_user = await get_user_by_username(db, request.admin_username)
  132. if existing_user:
  133. raise HTTPException(
  134. status_code=status.HTTP_400_BAD_REQUEST,
  135. detail="User with this username already exists",
  136. )
  137. # Create admin user FIRST (before enabling auth)
  138. try:
  139. logger.info("Creating admin user: %s", request.admin_username)
  140. admin_user = User(
  141. username=request.admin_username,
  142. password_hash=get_password_hash(request.admin_password),
  143. role="admin",
  144. is_active=True,
  145. )
  146. # Try to add user to Administrators group if it exists
  147. admin_group_result = await db.execute(select(Group).where(Group.name == "Administrators"))
  148. admin_group = admin_group_result.scalar_one_or_none()
  149. if admin_group:
  150. admin_user.groups.append(admin_group)
  151. logger.info("Added new admin user to Administrators group")
  152. db.add(admin_user)
  153. logger.info("Admin user added to session: %s", request.admin_username)
  154. admin_created = True
  155. except Exception as e:
  156. await db.rollback()
  157. logger.error("Failed to create admin user: %s", e, exc_info=True)
  158. raise HTTPException(
  159. status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
  160. detail=f"Failed to create admin user: {str(e)}",
  161. )
  162. # Set auth enabled and mark setup as completed
  163. await set_auth_enabled(db, request.auth_enabled)
  164. await set_setup_completed(db, True)
  165. await db.commit()
  166. if admin_created:
  167. await db.refresh(admin_user)
  168. logger.info("Admin user created successfully: %s", admin_user.id)
  169. logger.info("Setup completed: auth_enabled=%s, admin_created=%s", request.auth_enabled, admin_created)
  170. return SetupResponse(auth_enabled=request.auth_enabled, admin_created=admin_created)
  171. except HTTPException:
  172. raise
  173. except Exception as e:
  174. logger.error("Setup error: %s", e, exc_info=True)
  175. await db.rollback()
  176. raise HTTPException(
  177. status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
  178. detail=f"Setup failed: {str(e)}",
  179. )
  180. @router.get("/status")
  181. async def get_auth_status(db: AsyncSession = Depends(get_db)):
  182. """Get authentication status (public endpoint)."""
  183. auth_enabled = await is_auth_enabled(db)
  184. setup_completed = await is_setup_completed(db)
  185. # Only require setup if it hasn't been completed yet
  186. requires_setup = not setup_completed
  187. return {"auth_enabled": auth_enabled, "requires_setup": requires_setup}
  188. @router.post("/disable", response_model=dict)
  189. async def disable_auth(
  190. current_user: User = Depends(get_current_active_user),
  191. db: AsyncSession = Depends(get_db),
  192. ):
  193. """Disable authentication (admin only)."""
  194. import logging
  195. logger = logging.getLogger(__name__)
  196. # Reload user with groups for proper is_admin check
  197. result = await db.execute(select(User).where(User.id == current_user.id).options(selectinload(User.groups)))
  198. user = result.scalar_one()
  199. # Only admins can disable authentication
  200. if not user.is_admin:
  201. raise HTTPException(
  202. status_code=status.HTTP_403_FORBIDDEN,
  203. detail="Only admins can disable authentication",
  204. )
  205. try:
  206. await set_auth_enabled(db, False)
  207. await db.commit()
  208. logger.info("Authentication disabled by admin user: %s", user.username)
  209. return {"message": "Authentication disabled successfully", "auth_enabled": False}
  210. except Exception as e:
  211. await db.rollback()
  212. logger.error("Failed to disable authentication: %s", e, exc_info=True)
  213. raise HTTPException(
  214. status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
  215. detail=f"Failed to disable authentication: {str(e)}",
  216. )
  217. @router.post("/login", response_model=LoginResponse)
  218. async def login(request: LoginRequest, db: AsyncSession = Depends(get_db)):
  219. """Login and get access token.
  220. Supports username or email-based login. Username lookup is case-insensitive.
  221. """
  222. # Check if auth is enabled
  223. auth_enabled = await is_auth_enabled(db)
  224. if not auth_enabled:
  225. raise HTTPException(
  226. status_code=status.HTTP_400_BAD_REQUEST,
  227. detail="Authentication is not enabled",
  228. )
  229. # Try username-based authentication first
  230. user = await authenticate_user(db, request.username, request.password)
  231. # If username auth failed and advanced auth is enabled, try email-based authentication
  232. if not user:
  233. advanced_auth = await is_advanced_auth_enabled(db)
  234. if advanced_auth:
  235. user = await authenticate_user_by_email(db, request.username, request.password)
  236. if not user:
  237. raise HTTPException(
  238. status_code=status.HTTP_401_UNAUTHORIZED,
  239. detail="Incorrect username or password",
  240. headers={"WWW-Authenticate": "Bearer"},
  241. )
  242. # Reload user with groups for proper permission calculation
  243. result = await db.execute(select(User).where(User.id == user.id).options(selectinload(User.groups)))
  244. user = result.scalar_one()
  245. access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
  246. access_token = create_access_token(data={"sub": user.username}, expires_delta=access_token_expires)
  247. return LoginResponse(
  248. access_token=access_token,
  249. token_type="bearer",
  250. user=_user_to_response(user),
  251. )
  252. @router.get("/me", response_model=UserResponse)
  253. async def get_current_user_info(
  254. current_user: User = Depends(get_current_active_user),
  255. db: AsyncSession = Depends(get_db),
  256. ):
  257. """Get current user information."""
  258. # Reload user with groups for proper permission calculation
  259. result = await db.execute(select(User).where(User.id == current_user.id).options(selectinload(User.groups)))
  260. user = result.scalar_one()
  261. return _user_to_response(user)
  262. @router.post("/logout")
  263. async def logout():
  264. """Logout (client should discard token)."""
  265. return {"message": "Logged out successfully"}
  266. # Advanced Authentication Endpoints
  267. @router.post("/smtp/test", response_model=TestSMTPResponse)
  268. async def test_smtp_connection(
  269. test_request: TestSMTPRequest,
  270. current_user: User = Depends(get_current_active_user),
  271. db: AsyncSession = Depends(get_db),
  272. ):
  273. """Test SMTP connection with provided settings (admin only)."""
  274. import logging
  275. logger = logging.getLogger(__name__)
  276. # Reload user with groups for proper is_admin check
  277. result = await db.execute(select(User).where(User.id == current_user.id).options(selectinload(User.groups)))
  278. user = result.scalar_one()
  279. if not user.is_admin:
  280. raise HTTPException(
  281. status_code=status.HTTP_403_FORBIDDEN,
  282. detail="Only admins can test SMTP settings",
  283. )
  284. try:
  285. smtp_settings = SMTPSettings(
  286. smtp_host=test_request.smtp_host,
  287. smtp_port=test_request.smtp_port,
  288. smtp_username=test_request.smtp_username,
  289. smtp_password=test_request.smtp_password,
  290. smtp_use_tls=test_request.smtp_use_tls,
  291. smtp_from_email=test_request.smtp_from_email,
  292. )
  293. # Send test email
  294. send_email(
  295. smtp_settings=smtp_settings,
  296. to_email=test_request.test_recipient,
  297. subject="BamBuddy SMTP Test",
  298. body_text="This is a test email from BamBuddy. If you received this, your SMTP settings are working correctly!",
  299. body_html="<p>This is a test email from <strong>BamBuddy</strong>.</p><p>If you received this, your SMTP settings are working correctly!</p>",
  300. )
  301. logger.info(f"Test email sent successfully to {test_request.test_recipient}")
  302. return TestSMTPResponse(success=True, message="Test email sent successfully")
  303. except Exception as e:
  304. logger.error(f"Failed to send test email: {e}")
  305. return TestSMTPResponse(success=False, message=f"Failed to send test email: {str(e)}")
  306. @router.get("/smtp", response_model=SMTPSettings | None)
  307. async def get_smtp_config(
  308. current_user: User = Depends(get_current_active_user),
  309. db: AsyncSession = Depends(get_db),
  310. ):
  311. """Get SMTP settings (admin only). Password is not returned."""
  312. # Reload user with groups for proper is_admin check
  313. result = await db.execute(select(User).where(User.id == current_user.id).options(selectinload(User.groups)))
  314. user = result.scalar_one()
  315. if not user.is_admin:
  316. raise HTTPException(
  317. status_code=status.HTTP_403_FORBIDDEN,
  318. detail="Only admins can view SMTP settings",
  319. )
  320. smtp_settings = await get_smtp_settings(db)
  321. if smtp_settings:
  322. # Don't return password in response
  323. smtp_settings.smtp_password = None
  324. return smtp_settings
  325. @router.post("/smtp", response_model=dict)
  326. async def save_smtp_config(
  327. smtp_settings: SMTPSettings,
  328. current_user: User = Depends(get_current_active_user),
  329. db: AsyncSession = Depends(get_db),
  330. ):
  331. """Save SMTP settings (admin only)."""
  332. import logging
  333. logger = logging.getLogger(__name__)
  334. # Reload user with groups for proper is_admin check
  335. result = await db.execute(select(User).where(User.id == current_user.id).options(selectinload(User.groups)))
  336. user = result.scalar_one()
  337. if not user.is_admin:
  338. raise HTTPException(
  339. status_code=status.HTTP_403_FORBIDDEN,
  340. detail="Only admins can update SMTP settings",
  341. )
  342. try:
  343. await save_smtp_settings(db, smtp_settings)
  344. await db.commit()
  345. logger.info(f"SMTP settings updated by admin user: {user.username}")
  346. return {"message": "SMTP settings saved successfully"}
  347. except Exception as e:
  348. await db.rollback()
  349. logger.error(f"Failed to save SMTP settings: {e}")
  350. raise HTTPException(
  351. status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
  352. detail=f"Failed to save SMTP settings: {str(e)}",
  353. )
  354. @router.post("/advanced-auth/enable", response_model=dict)
  355. async def enable_advanced_auth(
  356. current_user: User = Depends(get_current_active_user),
  357. db: AsyncSession = Depends(get_db),
  358. ):
  359. """Enable advanced authentication (admin only).
  360. Requires SMTP settings to be configured and tested first.
  361. """
  362. import logging
  363. logger = logging.getLogger(__name__)
  364. # Reload user with groups for proper is_admin check
  365. result = await db.execute(select(User).where(User.id == current_user.id).options(selectinload(User.groups)))
  366. user = result.scalar_one()
  367. if not user.is_admin:
  368. raise HTTPException(
  369. status_code=status.HTTP_403_FORBIDDEN,
  370. detail="Only admins can enable advanced authentication",
  371. )
  372. # Verify SMTP settings are configured
  373. smtp_settings = await get_smtp_settings(db)
  374. if not smtp_settings:
  375. raise HTTPException(
  376. status_code=status.HTTP_400_BAD_REQUEST,
  377. detail="SMTP settings must be configured before enabling advanced authentication",
  378. )
  379. try:
  380. await set_advanced_auth_enabled(db, True)
  381. await db.commit()
  382. logger.info(f"Advanced authentication enabled by admin user: {user.username}")
  383. return {"message": "Advanced authentication enabled successfully", "advanced_auth_enabled": True}
  384. except Exception as e:
  385. await db.rollback()
  386. logger.error(f"Failed to enable advanced authentication: {e}")
  387. raise HTTPException(
  388. status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
  389. detail=f"Failed to enable advanced authentication: {str(e)}",
  390. )
  391. @router.post("/advanced-auth/disable", response_model=dict)
  392. async def disable_advanced_auth(
  393. current_user: User = Depends(get_current_active_user),
  394. db: AsyncSession = Depends(get_db),
  395. ):
  396. """Disable advanced authentication (admin only)."""
  397. import logging
  398. logger = logging.getLogger(__name__)
  399. # Reload user with groups for proper is_admin check
  400. result = await db.execute(select(User).where(User.id == current_user.id).options(selectinload(User.groups)))
  401. user = result.scalar_one()
  402. if not user.is_admin:
  403. raise HTTPException(
  404. status_code=status.HTTP_403_FORBIDDEN,
  405. detail="Only admins can disable advanced authentication",
  406. )
  407. try:
  408. await set_advanced_auth_enabled(db, False)
  409. await db.commit()
  410. logger.info(f"Advanced authentication disabled by admin user: {user.username}")
  411. return {"message": "Advanced authentication disabled successfully", "advanced_auth_enabled": False}
  412. except Exception as e:
  413. await db.rollback()
  414. logger.error(f"Failed to disable advanced authentication: {e}")
  415. raise HTTPException(
  416. status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
  417. detail=f"Failed to disable advanced authentication: {str(e)}",
  418. )
  419. @router.get("/advanced-auth/status")
  420. async def get_advanced_auth_status(db: AsyncSession = Depends(get_db)):
  421. """Get advanced authentication status."""
  422. advanced_auth_enabled = await is_advanced_auth_enabled(db)
  423. smtp_configured = await get_smtp_settings(db) is not None
  424. return {
  425. "advanced_auth_enabled": advanced_auth_enabled,
  426. "smtp_configured": smtp_configured,
  427. }
  428. @router.post("/forgot-password", response_model=ForgotPasswordResponse)
  429. async def forgot_password(request: ForgotPasswordRequest, db: AsyncSession = Depends(get_db)):
  430. """Request password reset via email (advanced auth only)."""
  431. import logging
  432. import os
  433. logger = logging.getLogger(__name__)
  434. # Check if advanced auth is enabled
  435. advanced_auth = await is_advanced_auth_enabled(db)
  436. if not advanced_auth:
  437. raise HTTPException(
  438. status_code=status.HTTP_400_BAD_REQUEST,
  439. detail="Advanced authentication is not enabled",
  440. )
  441. # Get SMTP settings
  442. smtp_settings = await get_smtp_settings(db)
  443. if not smtp_settings:
  444. raise HTTPException(
  445. status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
  446. detail="Email service is not configured",
  447. )
  448. # Find user by email
  449. user = await get_user_by_email(db, request.email)
  450. # Always return success message to prevent email enumeration
  451. # but only send email if user exists
  452. if user and user.is_active:
  453. try:
  454. # Generate new password
  455. new_password = generate_secure_password()
  456. user.password_hash = get_password_hash(new_password)
  457. await db.commit()
  458. # Get login URL from environment or use default
  459. login_url = os.environ.get("APP_URL", "http://localhost:5173") + "/login"
  460. # Send password reset email
  461. subject, text_body, html_body = create_password_reset_email(user.username, new_password, login_url)
  462. send_email(smtp_settings, user.email, subject, text_body, html_body)
  463. logger.info(f"Password reset email sent to {user.email}")
  464. except Exception as e:
  465. logger.error(f"Failed to send password reset email: {e}")
  466. # Don't reveal error to user for security
  467. return ForgotPasswordResponse(
  468. message="If the email address is associated with an account, a password reset email has been sent."
  469. )
  470. @router.post("/reset-password", response_model=ResetPasswordResponse)
  471. async def reset_user_password(
  472. request: ResetPasswordRequest,
  473. current_user: User = Depends(get_current_active_user),
  474. db: AsyncSession = Depends(get_db),
  475. ):
  476. """Reset a user's password and send them an email (admin only, advanced auth only)."""
  477. import logging
  478. import os
  479. logger = logging.getLogger(__name__)
  480. # Reload user with groups for proper is_admin check
  481. result = await db.execute(select(User).where(User.id == current_user.id).options(selectinload(User.groups)))
  482. admin_user = result.scalar_one()
  483. if not admin_user.is_admin:
  484. raise HTTPException(
  485. status_code=status.HTTP_403_FORBIDDEN,
  486. detail="Only admins can reset user passwords",
  487. )
  488. # Check if advanced auth is enabled
  489. advanced_auth = await is_advanced_auth_enabled(db)
  490. if not advanced_auth:
  491. raise HTTPException(
  492. status_code=status.HTTP_400_BAD_REQUEST,
  493. detail="Advanced authentication is not enabled",
  494. )
  495. # Get SMTP settings
  496. smtp_settings = await get_smtp_settings(db)
  497. if not smtp_settings:
  498. raise HTTPException(
  499. status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
  500. detail="Email service is not configured",
  501. )
  502. # Find user to reset
  503. result = await db.execute(select(User).where(User.id == request.user_id))
  504. user = result.scalar_one_or_none()
  505. if not user:
  506. raise HTTPException(
  507. status_code=status.HTTP_404_NOT_FOUND,
  508. detail="User not found",
  509. )
  510. if not user.email:
  511. raise HTTPException(
  512. status_code=status.HTTP_400_BAD_REQUEST,
  513. detail="User does not have an email address configured",
  514. )
  515. try:
  516. # Generate new password
  517. new_password = generate_secure_password()
  518. user.password_hash = get_password_hash(new_password)
  519. await db.commit()
  520. # Get login URL from environment or use default
  521. login_url = os.environ.get("APP_URL", "http://localhost:5173") + "/login"
  522. # Send password reset email
  523. subject, text_body, html_body = create_password_reset_email(user.username, new_password, login_url)
  524. send_email(smtp_settings, user.email, subject, text_body, html_body)
  525. logger.info(f"Password reset by admin {admin_user.username} for user {user.username}")
  526. return ResetPasswordResponse(message=f"Password reset email sent to {user.email}")
  527. except Exception as e:
  528. await db.rollback()
  529. logger.error(f"Failed to reset password for user {user.username}: {e}")
  530. raise HTTPException(
  531. status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
  532. detail=f"Failed to reset password: {str(e)}",
  533. )