Coverage for app/api/routes/login.py: 83%
53 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-10-26 10:48 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-10-26 10:48 +0000
1from datetime import timedelta
2from typing import Annotated, Any
4from fastapi import APIRouter, Depends, HTTPException
5from fastapi.responses import HTMLResponse
6from fastapi.security import OAuth2PasswordRequestForm
8from app import crud
9from app.api.deps import CurrentUser, SessionDep, get_current_active_superuser
10from app.core import security
11from app.core.config import settings
12from app.models import Message, NewPassword, Token, UserPublic, UserUpdate
13from app.utils import (
14 generate_password_reset_token,
15 generate_reset_password_email,
16 send_email,
17 verify_password_reset_token,
18)
20router = APIRouter(tags=["login"])
23@router.post("/login/access-token")
24def login_access_token(
25 session: SessionDep, form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
26) -> Token:
27 """
28 OAuth2 compatible token login, get an access token for future requests
29 """
30 user = crud.authenticate( 1acidebfg
31 session=session, email=form_data.username, password=form_data.password
32 )
33 if not user: 1acidebfg
34 raise HTTPException(status_code=400, detail="Incorrect email or password") 1i
35 elif not user.is_active: 1acdebfg
36 raise HTTPException(status_code=400, detail="Inactive user")
37 access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) 1acdebfg
38 return Token( 1acdebfg
39 access_token=security.create_access_token(
40 user.id, expires_delta=access_token_expires
41 )
42 )
45@router.post("/login/test-token", response_model=UserPublic)
46def test_token(current_user: CurrentUser) -> Any:
47 """
48 Test access token
49 """
50 return current_user 1l
53@router.post("/password-recovery/{email}")
54def recover_password(email: str, session: SessionDep) -> Message:
55 """
56 Password Recovery
57 """
58 user = crud.get_user_by_email(session=session, email=email) 1hj
60 # Always return the same response to prevent email enumeration attacks
61 # Only send email if user actually exists
62 if user: 1hj
63 password_reset_token = generate_password_reset_token(email=email) 1h
64 email_data = generate_reset_password_email( 1h
65 email_to=user.email, email=email, token=password_reset_token
66 )
67 send_email(
68 email_to=user.email,
69 subject=email_data.subject,
70 html_content=email_data.html_content,
71 )
72 return Message( 1aj
73 message="If that email is registered, we sent a password recovery link"
74 )
77@router.post("/reset-password/")
78def reset_password(session: SessionDep, body: NewPassword) -> Message:
79 """
80 Reset password
81 """
82 email = verify_password_reset_token(token=body.token) 1bk
83 if not email: 1bk
84 raise HTTPException(status_code=400, detail="Invalid token") 1k
85 user = crud.get_user_by_email(session=session, email=email) 1b
86 if not user: 1b
87 # Don't reveal that the user doesn't exist - use same error as invalid token
88 raise HTTPException(status_code=400, detail="Invalid token")
89 elif not user.is_active: 1b
90 raise HTTPException(status_code=400, detail="Inactive user")
91 user_in_update = UserUpdate(password=body.new_password) 1b
92 crud.update_user( 1b
93 session=session,
94 db_user=user,
95 user_in=user_in_update,
96 )
97 return Message(message="Password updated successfully") 1b
100@router.post(
101 "/password-recovery-html-content/{email}",
102 dependencies=[Depends(get_current_active_superuser)],
103 response_class=HTMLResponse,
104)
105def recover_password_html_content(email: str, session: SessionDep) -> Any:
106 """
107 HTML Content for Password Recovery
108 """
109 user = crud.get_user_by_email(session=session, email=email)
111 if not user:
112 raise HTTPException(
113 status_code=404,
114 detail="The user with this username does not exist in the system.",
115 )
116 password_reset_token = generate_password_reset_token(email=email)
117 email_data = generate_reset_password_email(
118 email_to=user.email, email=email, token=password_reset_token
119 )
121 return HTMLResponse(
122 content=email_data.html_content, headers={"subject:": email_data.subject}
123 )