Catalyst / admin/Bid-Sentinel 14.8 GB / 57.8 GB 40.0 GB free
Help Sign in

admin / Bid-Sentinel

public

Bid Scrape and Tracking Application with AI Capability

Code Issues Pull requests Pipelines Packages Security Insights Wiki Settings
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