Password hashing via pwdlib (argon2id), stateless JWT auth (2h expiry) delivered as an Authorization: Bearer token, with an OAuth2PasswordBearer dependency (app/dependencies.py) protecting future routes. Establishes app/routers/ as the convention for feature routes, explicitly registered in main.py (no auto-discovery, unlike app/models/). The first admin user is seeded via an Alembic data migration gated on SUPERUSER_EMAIL/SUPERUSER_PASSWORD env vars (read at migration-run time only, never persisted to .env/git) rather than a script or open endpoint, so a fresh `alembic upgrade head` still succeeds without them. The migration mirrors the users/organization tables locally instead of importing the live SQLModel classes, keeping it a stable schema snapshot; it does import app.core.security.hash_password, a pure utility with no table-shape dependency. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
37 lines
1.2 KiB
Python
37 lines
1.2 KiB
Python
from typing import Annotated
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from fastapi.security import OAuth2PasswordRequestForm
|
|
from sqlmodel import SQLModel, select
|
|
|
|
from app.core.security import create_access_token, verify_password
|
|
from app.db import SessionDep
|
|
from app.dependencies import CurrentUser
|
|
from app.models.user import User, UserPublic
|
|
|
|
router = APIRouter(prefix="/auth", tags=["auth"])
|
|
|
|
|
|
class Token(SQLModel):
|
|
access_token: str
|
|
token_type: str = "bearer"
|
|
|
|
|
|
@router.post("/login")
|
|
def login(
|
|
session: SessionDep,
|
|
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
|
|
) -> Token:
|
|
user = session.exec(select(User).where(User.email == form_data.username)).first()
|
|
if user is None or not user.is_active or not verify_password(form_data.password, user.hashed_password):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Incorrect email or password",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
return Token(access_token=create_access_token(subject=str(user.id)))
|
|
|
|
|
|
@router.get("/me", response_model=UserPublic)
|
|
def me(current_user: CurrentUser) -> User:
|
|
return current_user
|