feat: JWT auth, user management, audit logging system
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
"""Audit API: list audit logs — only owner."""
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.middleware.auth import require_role
|
||||
from app.models.audit import AuditLog
|
||||
from app.models.user import User
|
||||
|
||||
router = APIRouter(prefix="/api/v1/audit", tags=["audit"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_audit(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
action: str | None = Query(None),
|
||||
user: User = Depends(require_role("owner")),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
query = select(AuditLog).order_by(AuditLog.created_at.desc())
|
||||
if action:
|
||||
query = query.where(AuditLog.action.ilike(f"%{action}%"))
|
||||
query = query.offset((page - 1) * page_size).limit(page_size)
|
||||
result = await db.execute(query)
|
||||
return result.scalars().all()
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Auth API: register, login, refresh, change-password."""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import settings
|
||||
from app.database import get_db
|
||||
from app.middleware.auth import require_auth
|
||||
from app.models.user import User
|
||||
from app.schemas.auth import (
|
||||
ChangePasswordRequest,
|
||||
LoginRequest,
|
||||
RegisterRequest,
|
||||
TokenResponse,
|
||||
)
|
||||
from app.schemas.user import UserResponse
|
||||
from app.services.auth_service import (
|
||||
authenticate_user,
|
||||
create_access_token,
|
||||
create_user,
|
||||
decode_access_token,
|
||||
get_user_by_username,
|
||||
hash_password,
|
||||
verify_password,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/api/v1/auth", tags=["auth"])
|
||||
|
||||
|
||||
@router.post("/register", status_code=201, response_model=UserResponse)
|
||||
async def register(body: RegisterRequest, db: AsyncSession = Depends(get_db)):
|
||||
existing = await get_user_by_username(db, body.username)
|
||||
if existing:
|
||||
raise HTTPException(status_code=409, detail="Username already exists")
|
||||
|
||||
result = await db.execute(select(User).where(User.email == body.email))
|
||||
if result.scalar_one_or_none():
|
||||
raise HTTPException(status_code=409, detail="Email already exists")
|
||||
|
||||
user = await create_user(db, body.username, body.email, body.password)
|
||||
return user
|
||||
|
||||
|
||||
@router.post("/login", response_model=TokenResponse)
|
||||
async def login(body: LoginRequest, db: AsyncSession = Depends(get_db)):
|
||||
user = await authenticate_user(db, body.username, body.password)
|
||||
token = create_access_token(user.id, user.role.value)
|
||||
return TokenResponse(
|
||||
access_token=token,
|
||||
expires_in=settings.jwt_expire_minutes * 60,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/refresh", response_model=TokenResponse)
|
||||
async def refresh(user: User = Depends(require_auth)):
|
||||
token = create_access_token(user.id, user.role.value)
|
||||
return TokenResponse(
|
||||
access_token=token,
|
||||
expires_in=settings.jwt_expire_minutes * 60,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/change-password")
|
||||
async def change_password(
|
||||
body: ChangePasswordRequest,
|
||||
user: User = Depends(require_auth),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
if not verify_password(body.old_password, user.hashed_password):
|
||||
raise HTTPException(status_code=400, detail="Old password is incorrect")
|
||||
|
||||
user.hashed_password = hash_password(body.new_password)
|
||||
await db.commit()
|
||||
return {"detail": "Password changed successfully"}
|
||||
@@ -0,0 +1,78 @@
|
||||
"""User management API: list, detail, update, approve — only owner."""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.middleware.auth import require_role
|
||||
from app.models.user import User
|
||||
from app.schemas.user import UserResponse, UserUpdateRequest
|
||||
|
||||
router = APIRouter(prefix="/api/v1/users", tags=["users"])
|
||||
|
||||
|
||||
@router.get("", response_model=list[UserResponse])
|
||||
async def list_users(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
user: User = Depends(require_role("owner")),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(
|
||||
select(User).offset((page - 1) * page_size).limit(page_size)
|
||||
)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
@router.get("/{user_id}", response_model=UserResponse)
|
||||
async def get_user(
|
||||
user_id: str,
|
||||
user: User = Depends(require_role("owner")),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(select(User).where(User.id == user_id))
|
||||
target = result.scalar_one_or_none()
|
||||
if not target:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return target
|
||||
|
||||
|
||||
@router.patch("/{user_id}", response_model=UserResponse)
|
||||
async def update_user(
|
||||
user_id: str,
|
||||
body: UserUpdateRequest,
|
||||
user: User = Depends(require_role("owner")),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(select(User).where(User.id == user_id))
|
||||
target = result.scalar_one_or_none()
|
||||
if not target:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
if body.role is not None:
|
||||
target.role = body.role
|
||||
if body.is_active is not None:
|
||||
target.is_active = body.is_active
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(target)
|
||||
return target
|
||||
|
||||
|
||||
@router.post("/{user_id}/approve", response_model=UserResponse)
|
||||
async def approve_user(
|
||||
user_id: str,
|
||||
user: User = Depends(require_role("owner")),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(select(User).where(User.id == user_id))
|
||||
target = result.scalar_one_or_none()
|
||||
if not target:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
if target.is_approved:
|
||||
raise HTTPException(status_code=400, detail="User already approved")
|
||||
|
||||
target.is_approved = True
|
||||
await db.commit()
|
||||
await db.refresh(target)
|
||||
return target
|
||||
Reference in New Issue
Block a user