admin / Bid-Sentinel
publicBid Scrape and Tracking Application with AI Capability
Bid-Sentinel / bid-sentinel-v2 / backend / app / routers / auth.py
3467 B · main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | """Authentication endpoints: first-run setup, login, current-user introspection.""" from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi.security import OAuth2PasswordRequestForm from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.audit import write_audit from app.database import get_db from app.deps import get_current_user from app.models import User, UserRole from app.schemas import AdminSetup, SetupStatus, Token, UserOut from app.security import create_access_token, hash_password, verify_password router = APIRouter(prefix="/auth", tags=["auth"]) async def _user_count(db: AsyncSession) -> int: return (await db.execute(select(func.count()).select_from(User))).scalar_one() @router.get("/setup-status", response_model=SetupStatus) async def setup_status(db: AsyncSession = Depends(get_db)): """Tell the frontend whether the first-run admin setup is required.""" return SetupStatus(needs_setup=(await _user_count(db)) == 0) @router.post("/setup", response_model=Token, status_code=status.HTTP_201_CREATED) async def setup_admin(payload: AdminSetup, db: AsyncSession = Depends(get_db)): """Create the first administrator account. Only available while no users exist; locks itself permanently once the first account is created. Logs the new admin straight in by returning a token. """ if (await _user_count(db)) > 0: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Setup already completed. Please sign in.", ) if len(payload.password) < 8: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Password must be at least 8 characters.", ) admin = User( email=payload.email, full_name=payload.full_name or "Administrator", role=UserRole.admin, hashed_password=hash_password(payload.password), ) db.add(admin) await db.commit() token = create_access_token(subject=admin.email, role=admin.role.value) return Token(access_token=token) @router.post("/login", response_model=Token) async def login( request: Request, form_data: OAuth2PasswordRequestForm = Depends(), db: AsyncSession = Depends(get_db), ): """OAuth2 password flow. `username` field carries the email address.""" ip = request.client.host if request.client else None result = await db.execute(select(User).where(User.email == form_data.username)) user = result.scalar_one_or_none() if not user or not verify_password(form_data.password, user.hashed_password): await write_audit(form_data.username, "Login failed", status_code=401, ip=ip) raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect email or password", headers={"WWW-Authenticate": "Bearer"}, ) if not user.is_active: await write_audit(user.email, "Login blocked (disabled account)", status_code=403, ip=ip) raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Account is disabled") token = create_access_token(subject=user.email, role=user.role.value) await write_audit(user.email, "Login success", status_code=200, ip=ip) return Token(access_token=token) @router.get("/me", response_model=UserOut) async def me(current_user: User = Depends(get_current_user)): return current_user |