← full-stack-fastapi-template  /  backend/app/api/routes/login.py

1
from datetime import timedelta
2
from typing import Annotated, Any
3
4
from fastapi import APIRouter, Depends, HTTPException
5
from fastapi.responses import HTMLResponse
6
from fastapi.security import OAuth2PasswordRequestForm
7
8
from app import crud
9
from app.api.deps import CurrentUser, SessionDep, get_current_active_superuser
10
from app.core import security
11
from app.core.config import settings
12
from app.models import Message, NewPassword, Token, UserPublic, UserUpdate
13
from app.utils import (
14
    generate_password_reset_token,
15
    generate_reset_password_email,
16
    send_email,
17
    verify_password_reset_token,
18
)
19
20
router = APIRouter(tags=["login"])
21
22
23
@router.post("/login/access-token")
24
def 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(
31
        session=session, email=form_data.username, password=form_data.password
32
    )
33
    if not user:
34
        raise HTTPException(status_code=400, detail="Incorrect email or password")
35
    elif not user.is_active:
36
        raise HTTPException(status_code=400, detail="Inactive user")
37
    access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
38
    return Token(
39
        access_token=security.create_access_token(
40
            user.id, expires_delta=access_token_expires
41
        )
42
    )
43
44
45
@router.post("/login/test-token", response_model=UserPublic)
46
def test_token(current_user: CurrentUser) -> Any:
47
    """
48
    Test access token
49
    """
50
    return current_user
51
52
53
@router.post("/password-recovery/{email}")
54
def recover_password(email: str, session: SessionDep) -> Message:
55
    """
56
    Password Recovery
57
    """
58
    user = crud.get_user_by_email(session=session, email=email)
59
60
    # Always return the same response to prevent email enumeration attacks
61
    # Only send email if user actually exists
62
    if user:
63
        password_reset_token = generate_password_reset_token(email=email)
64
        email_data = generate_reset_password_email(
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(
73
        message="If that email is registered, we sent a password recovery link"
74
    )
75
76
77
@router.post("/reset-password/")
78
def reset_password(session: SessionDep, body: NewPassword) -> Message:
79
    """
80
    Reset password
81
    """
82
    email = verify_password_reset_token(token=body.token)
83
    if not email:
84
        raise HTTPException(status_code=400, detail="Invalid token")
85
    user = crud.get_user_by_email(session=session, email=email)
86
    if not user:
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:
90
        raise HTTPException(status_code=400, detail="Inactive user")
91
    user_in_update = UserUpdate(password=body.new_password)
92
    crud.update_user(
93
        session=session,
94
        db_user=user,
95
        user_in=user_in_update,
96
    )
97
    return Message(message="Password updated successfully")
98
99
100
@router.post(
101
    "/password-recovery-html-content/{email}",
102
    dependencies=[Depends(get_current_active_superuser)],
103
    response_class=HTMLResponse,
104
)
105
def 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)
110
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
    )
120
121
    return HTMLResponse(
122
        content=email_data.html_content, headers={"subject:": email_data.subject}
123
    )
124