Files
app/backend/fluksio/api/routes/login.py
T
stroblmeandClaude Opus 5 640654bd66 Rename the import package app to fluksio
A wheel whose top-level module is `app` collides with anything else in a
user's venv, so the package that is about to be published takes the name
it is published under. Only the Python package moves; the repo, the
Docker WORKDIR and the compose project keep theirs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 21:48:05 +02:00

124 lines
4.0 KiB
Python

from datetime import timedelta
from typing import Annotated, Any
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import HTMLResponse
from fastapi.security import OAuth2PasswordRequestForm
from fluksio import crud
from fluksio.api.deps import CurrentUser, SessionDep, get_current_active_superuser
from fluksio.core import security
from fluksio.core.config import settings
from fluksio.models import Message, NewPassword, Token, UserPublic, UserUpdate
from fluksio.utils import (
generate_password_reset_token,
generate_reset_password_email,
send_email,
verify_password_reset_token,
)
router = APIRouter(tags=["login"])
@router.post("/login/access-token")
def login_access_token(
session: SessionDep, form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
) -> Token:
"""
OAuth2 compatible token login, get an access token for future requests
"""
user = crud.authenticate(
session=session, email=form_data.username, password=form_data.password
)
if not user:
raise HTTPException(status_code=400, detail="Incorrect email or password")
elif not user.is_active:
raise HTTPException(status_code=400, detail="Inactive user")
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
return Token(
access_token=security.create_access_token(
user.id, expires_delta=access_token_expires
)
)
@router.post("/login/test-token", response_model=UserPublic)
def test_token(current_user: CurrentUser) -> Any:
"""
Test access token
"""
return current_user
@router.post("/password-recovery/{email}")
def recover_password(email: str, session: SessionDep) -> Message:
"""
Password Recovery
"""
user = crud.get_user_by_email(session=session, email=email)
# Always return the same response to prevent email enumeration attacks
# Only send email if user actually exists
if user:
password_reset_token = generate_password_reset_token(email=email)
email_data = generate_reset_password_email(
email_to=user.email, email=email, token=password_reset_token
)
send_email(
email_to=user.email,
subject=email_data.subject,
html_content=email_data.html_content,
)
return Message(
message="If that email is registered, we sent a password recovery link"
)
@router.post("/reset-password/")
def reset_password(session: SessionDep, body: NewPassword) -> Message:
"""
Reset password
"""
email = verify_password_reset_token(token=body.token)
if not email:
raise HTTPException(status_code=400, detail="Invalid token")
user = crud.get_user_by_email(session=session, email=email)
if not user:
# Don't reveal that the user doesn't exist - use same error as invalid token
raise HTTPException(status_code=400, detail="Invalid token")
elif not user.is_active:
raise HTTPException(status_code=400, detail="Inactive user")
user_in_update = UserUpdate(password=body.new_password)
crud.update_user(
session=session,
db_user=user,
user_in=user_in_update,
)
return Message(message="Password updated successfully")
@router.post(
"/password-recovery-html-content/{email}",
dependencies=[Depends(get_current_active_superuser)],
response_class=HTMLResponse,
)
def recover_password_html_content(email: str, session: SessionDep) -> Any:
"""
HTML Content for Password Recovery
"""
user = crud.get_user_by_email(session=session, email=email)
if not user:
raise HTTPException(
status_code=404,
detail="The user with this username does not exist in the system.",
)
password_reset_token = generate_password_reset_token(email=email)
email_data = generate_reset_password_email(
email_to=user.email, email=email, token=password_reset_token
)
return HTMLResponse(
content=email_data.html_content, headers={"subject:": email_data.subject}
)