Files

175 lines
5.7 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, status, Response
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from typing import Optional, List
from database import get_db
from auth import (
verify_password,
get_password_hash,
create_access_token,
create_refresh_token,
verify_token,
get_current_user,
require_admin,
create_default_admin,
log_action,
)
from models import User
from schemas import UserCreate, UserOut, LoginPayload, Token, TokenRefresh, UserUpdate, UserListOut
router = APIRouter(prefix="/api/auth", tags=["auth"])
@router.post("/register", response_model=UserOut, status_code=status.HTTP_201_CREATED)
def register(payload: UserCreate, db: Session = Depends(get_db)):
existing = db.query(User).filter(User.username == payload.username).first()
if existing:
raise HTTPException(status_code=400, detail="Username already exists")
user = User(
username=payload.username,
password_hash=get_password_hash(payload.password),
role="viewer",
)
db.add(user)
db.commit()
db.refresh(user)
return user
@router.post("/login", response_model=Token)
def login(
response: Response,
form_data: OAuth2PasswordRequestForm = Depends(),
db: Session = Depends(get_db),
):
create_default_admin(db)
user = db.query(User).filter(User.username == form_data.username).first()
if not user or not verify_password(form_data.password, user.password_hash):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
access = create_access_token({"sub": user.username, "role": user.role})
refresh = create_refresh_token({"sub": user.username, "role": user.role})
response.set_cookie(
key="access_token",
value=access,
httponly=True,
max_age=1800,
samesite="lax",
)
return {"access_token": access, "refresh_token": refresh}
@router.post("/refresh", response_model=Token)
def refresh(payload: TokenRefresh, response: Response, db: Session = Depends(get_db)):
data = verify_token(payload.refresh_token, "refresh")
if not data or "sub" not in data:
raise HTTPException(status_code=401, detail="Invalid refresh token")
user = db.query(User).filter(User.username == data["sub"]).first()
if not user:
raise HTTPException(status_code=401, detail="User not found")
access = create_access_token({"sub": user.username, "role": user.role})
refresh = create_refresh_token({"sub": user.username, "role": user.role})
response.set_cookie(
key="access_token",
value=access,
httponly=True,
max_age=1800,
samesite="lax",
)
return {"access_token": access, "refresh_token": refresh}
@router.get("/me", response_model=UserOut)
def me(user: User = Depends(get_current_user)):
return user
@router.post("/logout")
def logout(response: Response):
response.delete_cookie("access_token")
return {"ok": True}
# Admin user management
@router.get("/users", response_model=List[UserListOut])
def list_users(
db: Session = Depends(get_db),
admin: User = Depends(require_admin),
):
return db.query(User).order_by(User.username).all()
@router.post("/users", response_model=UserOut, status_code=status.HTTP_201_CREATED)
def create_user(
payload: UserCreate,
db: Session = Depends(get_db),
admin: User = Depends(require_admin),
):
existing = db.query(User).filter(User.username == payload.username).first()
if existing:
raise HTTPException(status_code=400, detail="Username already exists")
user = User(
username=payload.username,
password_hash=get_password_hash(payload.password),
role="viewer",
)
db.add(user)
db.commit()
db.refresh(user)
log_action(db, admin, "user_created", f"Created user {user.username} (id={user.id})")
return user
@router.patch("/users/{user_id}", response_model=UserOut)
def update_user(
user_id: int,
payload: UserUpdate,
db: Session = Depends(get_db),
admin: User = Depends(require_admin),
):
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
if user.id == admin.id and payload.role and payload.role != "admin":
raise HTTPException(status_code=400, detail="Cannot downgrade yourself")
if payload.role:
if payload.role not in ("admin", "viewer"):
raise HTTPException(status_code=400, detail="Invalid role")
user.role = payload.role
if payload.password:
user.password_hash = get_password_hash(payload.password)
db.commit()
db.refresh(user)
log_action(db, admin, "user_updated", f"Updated user {user.username} (id={user.id}), role={user.role}, password_changed={bool(payload.password)}")
return user
@router.delete("/users/{user_id}")
def delete_user(
user_id: int,
db: Session = Depends(get_db),
admin: User = Depends(require_admin),
):
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
if user.id == admin.id:
raise HTTPException(status_code=400, detail="Cannot delete yourself")
# prevent deleting last admin
if user.role == "admin":
admin_count = db.query(User).filter(User.role == "admin").count()
if admin_count <= 1:
raise HTTPException(status_code=400, detail="Cannot delete the last admin")
username = user.username
db.delete(user)
db.commit()
log_action(db, admin, "user_deleted", f"Deleted user {username} (id={user_id})")
return {"ok": True}