Update admin theme/layout and refresh README details.

Align the project baseline with the latest admin interface styling and layout structure while documenting setup and usage updates in README.
This commit is contained in:
vlad
2026-07-14 17:12:28 +03:00
commit 86cc3fa541
278 changed files with 19416 additions and 0 deletions
+206
View File
@@ -0,0 +1,206 @@
from app.core.datetime_utils import ensure_utc, utc_now
from fastapi import APIRouter, HTTPException, Request, Response, status
from app.core.config import settings
from app.core.redis import check_rate_limit, client_ip
from app.modules.auth.schemas import (
ForgotPasswordIn,
LoginIn,
LoginOut,
RegisterIn,
ResendVerificationIn,
ResetPasswordIn,
VerifyEmailIn,
)
from app.modules.auth.service import (
forgot_password,
login,
refresh,
register,
resend_verification,
reset_password,
revoke_refresh_token,
verify_email_token,
)
from app.modules.users import repository
router = APIRouter()
def _allowed_origins() -> set[str]:
origins: set[str] = set()
for raw in (settings.frontend_url, settings.public_base_url, *settings.cors_origins):
if not raw:
continue
normalized = raw.rstrip("/")
origins.add(normalized)
if "://localhost" in normalized:
origins.add(normalized.replace("://localhost", "://127.0.0.1"))
if "://127.0.0.1" in normalized:
origins.add(normalized.replace("://127.0.0.1", "://localhost"))
return origins
def _enforce_origin(request: Request, *, require_header: bool = False) -> None:
allowed = _allowed_origins()
origin = (request.headers.get("Origin") or "").rstrip("/")
referer = request.headers.get("Referer") or ""
if require_header and not origin and not referer:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="INVALID_ORIGIN")
if origin and origin not in allowed:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="INVALID_ORIGIN")
if not origin and referer:
if not any(referer.startswith(base) for base in allowed):
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="INVALID_ORIGIN")
def _set_refresh_cookie(response: Response, refresh_token: str) -> None:
response.set_cookie(
key="refresh_token",
value=refresh_token,
httponly=True,
secure=settings.cookie_secure,
samesite="lax",
path="/api/v1/auth",
max_age=settings.jwt_refresh_ttl_days * 24 * 60 * 60,
)
@router.post("/register")
async def register_route(payload: RegisterIn, request: Request):
_enforce_origin(request)
check_rate_limit(f"register:{client_ip(request)}", limit=3, window_seconds=3600)
user = register(payload.email, payload.password)
_ = user
return {"message": "If email is valid, verification has been sent."}
@router.post("/verify-email")
async def verify_email_route(payload: VerifyEmailIn):
try:
user = verify_email_token(payload.token)
except ValueError as exc:
detail = str(exc)
if detail == "TOKEN_EXPIRED":
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="TOKEN_EXPIRED")
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="INVALID_TOKEN")
_ = user
return {"status": "active"}
@router.post("/login", response_model=LoginOut)
async def login_route(payload: LoginIn, request: Request, response: Response):
_enforce_origin(request)
user = repository.get_user_by_email(payload.email)
if user:
locked_until = ensure_utc(user.locked_until)
if locked_until and locked_until > utc_now():
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail="ACCOUNT_TEMPORARILY_LOCKED",
)
check_rate_limit(
f"login:{client_ip(request)}:{payload.email.lower()}",
limit=5,
window_seconds=60,
)
try:
access_token, refresh_token, user = login(payload.email, payload.password)
except ValueError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="INVALID_CREDENTIALS")
except PermissionError as exc:
detail = str(exc)
if detail == "EMAIL_NOT_VERIFIED":
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="EMAIL_NOT_VERIFIED")
if detail == "ACCOUNT_TEMPORARILY_LOCKED":
raise HTTPException(status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="ACCOUNT_TEMPORARILY_LOCKED")
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="ACCOUNT_BLOCKED")
_set_refresh_cookie(response, refresh_token)
return {
"access_token": access_token,
"expires_in": settings.jwt_access_ttl_min * 60,
"user": {
"id": user.id,
"email": user.email,
"role": user.role,
"is_superuser": user.is_superuser,
"status": user.status,
},
}
@router.post("/refresh", response_model=LoginOut)
async def refresh_route(request: Request, response: Response):
refresh_token = request.cookies.get("refresh_token")
if not refresh_token:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="REFRESH_MISSING")
_enforce_origin(request, require_header=True)
check_rate_limit(f"refresh:{client_ip(request)}", limit=30, window_seconds=60)
try:
access_token, new_refresh, user = refresh(refresh_token)
except PermissionError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="INVALID_REFRESH")
_set_refresh_cookie(response, new_refresh)
return {
"access_token": access_token,
"expires_in": settings.jwt_access_ttl_min * 60,
"user": {
"id": user.id,
"email": user.email,
"role": user.role,
"is_superuser": user.is_superuser,
"status": user.status,
},
}
@router.post("/logout")
async def logout_route(request: Request, response: Response):
_enforce_origin(request, require_header=True)
refresh_token = request.cookies.get("refresh_token")
if refresh_token:
revoke_refresh_token(refresh_token)
response.delete_cookie(
"refresh_token",
path="/api/v1/auth",
secure=settings.cookie_secure,
samesite="lax",
)
return {"message": "logged_out"}
@router.post("/forgot-password")
async def forgot_password_route(payload: ForgotPasswordIn, request: Request):
_enforce_origin(request)
check_rate_limit(
f"forgot:{client_ip(request)}:{payload.email.lower()}",
limit=3,
window_seconds=3600,
)
forgot_password(payload.email)
return {"message": "If email is registered, reset instructions have been sent."}
@router.post("/reset-password")
async def reset_password_route(payload: ResetPasswordIn):
try:
reset_password(payload.token, payload.new_password)
except ValueError as exc:
detail = str(exc)
if detail == "TOKEN_EXPIRED":
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="TOKEN_EXPIRED")
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="INVALID_TOKEN")
return {"message": "password_updated"}
@router.post("/resend-verification")
async def resend_verification_route(payload: ResendVerificationIn, request: Request):
_enforce_origin(request)
check_rate_limit(
f"resend:{client_ip(request)}:{payload.email.lower()}",
limit=3,
window_seconds=3600,
)
resend_verification(payload.email)
return {"message": "If email is registered, verification has been sent."}