feat: JWT auth, user management, audit logging system
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
"""Audit middleware: log all mutating requests to AuditLog."""
|
||||
import json
|
||||
|
||||
from fastapi import Request
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.responses import Response
|
||||
|
||||
from app.database import async_session
|
||||
from app.models.audit import AuditLog
|
||||
from app.services.auth_service import decode_access_token, get_user_by_id
|
||||
|
||||
|
||||
class AuditMiddleware(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request: Request, call_next) -> Response:
|
||||
if request.method in ("POST", "PUT", "PATCH", "DELETE"):
|
||||
response = await call_next(request)
|
||||
await self._log(request, response)
|
||||
return response
|
||||
return await call_next(request)
|
||||
|
||||
async def _log(self, request: Request, response: Response):
|
||||
try:
|
||||
user_id = "anonymous"
|
||||
username = "anonymous"
|
||||
|
||||
auth_header = request.headers.get("authorization")
|
||||
if auth_header and auth_header.startswith("Bearer "):
|
||||
try:
|
||||
payload = decode_access_token(auth_header[7:])
|
||||
async with async_session() as db:
|
||||
user = await get_user_by_id(db, payload["sub"])
|
||||
if user:
|
||||
user_id = user.id
|
||||
username = user.username
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
path = request.url.path
|
||||
action = f"{request.method} {path}"
|
||||
|
||||
detail = {}
|
||||
if response.status_code >= 400:
|
||||
body = b""
|
||||
try:
|
||||
body = await request.body()
|
||||
if body:
|
||||
detail["request_body"] = json.loads(body)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async with async_session() as db:
|
||||
log = AuditLog(
|
||||
user_id=user_id,
|
||||
username=username,
|
||||
action=action,
|
||||
target=path,
|
||||
detail=json.dumps(detail) if detail else None,
|
||||
ip_address=request.client.host if request.client else None,
|
||||
)
|
||||
db.add(log)
|
||||
await db.commit()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,27 @@
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.services.auth_service import decode_access_token, get_user_by_id
|
||||
|
||||
security = HTTPBearer()
|
||||
|
||||
|
||||
async def require_auth(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
payload = decode_access_token(credentials.credentials)
|
||||
user = await get_user_by_id(db, payload["sub"])
|
||||
if not user or not user.is_active:
|
||||
raise HTTPException(status_code=401, detail="Invalid or inactive user")
|
||||
return user
|
||||
|
||||
|
||||
def require_role(*roles: str):
|
||||
async def role_check(user=Depends(require_auth)):
|
||||
if user.role.value not in roles:
|
||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||
return user
|
||||
return role_check
|
||||
Reference in New Issue
Block a user