Files
stackpilot/backend/routers/auth.py
T
menzeljandClaude Opus 4.8 f732cb080b Initial commit: StackPilot Phase 1 (Core)
Self-hosted Docker Compose manager.
- Backend: FastAPI + docker-py + SQLite (JWT auth, file-first stacks,
  lifecycle, live status, WebSocket logs, docker-run converter, audit log)
- Frontend: React + Vite + Tailwind (login/setup, dashboard, stacks,
  stack detail, Monaco editor, dark/light theme)
- Deployment: docker-compose.yml, Dockerfiles, nginx reverse proxy

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 16:04:58 +00:00

112 lines
3.2 KiB
Python

"""Authentication routes + first-launch setup wizard."""
from __future__ import annotations
import time
from collections import defaultdict, deque
from fastapi import APIRouter, Depends, HTTPException, Request, status
from sqlmodel import Session
import auth as auth_mod
from database import get_session
from models.user import (
LoginRequest,
RefreshRequest,
TokenPair,
User,
UserCreate,
UserRead,
)
from services import audit_service
router = APIRouter(prefix="/api/auth", tags=["auth"])
# Simple in-memory rate limiter for login (max 10 / minute / IP).
_LOGIN_HITS: dict[str, deque] = defaultdict(deque)
_RATE_LIMIT = 10
_RATE_WINDOW = 60.0
def _check_rate_limit(ip: str) -> None:
now = time.monotonic()
hits = _LOGIN_HITS[ip]
while hits and now - hits[0] > _RATE_WINDOW:
hits.popleft()
if len(hits) >= _RATE_LIMIT:
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail="Too many login attempts, slow down.",
)
hits.append(now)
def _tokens_for(user: User) -> TokenPair:
return TokenPair(
access_token=auth_mod.create_access_token(user),
refresh_token=auth_mod.create_refresh_token(user),
)
@router.get("/needs-setup")
def needs_setup(session: Session = Depends(get_session)) -> dict:
"""First-launch wizard check: True if no users exist yet."""
return {"needs_setup": not auth_mod.users_exist(session)}
@router.post("/setup", response_model=TokenPair)
def setup(
body: UserCreate, session: Session = Depends(get_session)
) -> TokenPair:
if auth_mod.users_exist(session):
raise HTTPException(status_code=400, detail="Setup already completed")
user = User(
username=body.username,
hashed_password=auth_mod.hash_password(body.password),
role="admin",
)
session.add(user)
session.commit()
session.refresh(user)
audit_service.record(
session, user=user.username, action="user.setup", target=user.username
)
return _tokens_for(user)
@router.post("/login", response_model=TokenPair)
def login(
body: LoginRequest,
request: Request,
session: Session = Depends(get_session),
) -> TokenPair:
ip = request.client.host if request.client else "unknown"
_check_rate_limit(ip)
user = auth_mod.authenticate(session, body.username, body.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
)
audit_service.record(
session, user=user.username, action="auth.login", target=user.username, ip=ip
)
return _tokens_for(user)
@router.post("/refresh", response_model=TokenPair)
def refresh(
body: RefreshRequest, session: Session = Depends(get_session)
) -> TokenPair:
payload = auth_mod.decode_token(body.refresh_token, "refresh")
user = auth_mod.get_user(session, payload.get("sub", ""))
if not user or not user.is_active:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token"
)
return _tokens_for(user)
@router.get("/me", response_model=UserRead)
def me(user: User = Depends(auth_mod.get_current_user)) -> User:
return user