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
+228
View File
@@ -0,0 +1,228 @@
from __future__ import annotations
from datetime import timedelta
from uuid import uuid4
from app.core.config import settings
from app.core.datetime_utils import ensure_utc, utc_now
from app.core.email import send_template_email
from app.core.security import (
create_access_token,
generate_opaque_token,
generate_refresh_token,
hash_opaque_token,
hash_password,
hash_refresh_token,
verify_password,
)
from app.modules.auth import repository as auth_repository
from app.modules.users import repository
from app.modules.users.models import User
def _token_expires_at():
return utc_now() + timedelta(hours=settings.auth_token_ttl_hours)
def _send_verification_email(user: User, token: str) -> None:
verify_url = f"{settings.frontend_url}/verify?token={token}"
show_token = settings.email_delivery_mode == "memory" or settings.enable_test_routes
token_line = f"TOKEN:{token}\n" if show_token else ""
body = "Confirm your Compton account.\n\n" + f"Open: {verify_url}\n" + token_line
send_template_email(
to=user.email,
template="verify_email",
subject="Confirm your Compton account",
body=body,
)
def _send_password_reset_email(user: User, token: str) -> None:
reset_url = f"{settings.frontend_url}/reset-password?token={token}"
show_token = settings.email_delivery_mode == "memory" or settings.enable_test_routes
token_line = f"TOKEN:{token}\n" if show_token else ""
body = "Reset your Compton password.\n\n" + f"Open: {reset_url}\n" + token_line
send_template_email(
to=user.email,
template="reset_password",
subject="Reset your Compton password",
body=body,
)
def _issue_verification_token(user_id: str) -> str:
token = generate_opaque_token()
auth_repository.create_email_verification_token(
user_id=user_id,
token_hash=hash_opaque_token(token),
expires_at=_token_expires_at(),
)
return token
def _issue_password_reset_token(user_id: str) -> str:
token = generate_opaque_token()
auth_repository.create_password_reset_token(
user_id=user_id,
token_hash=hash_opaque_token(token),
expires_at=_token_expires_at(),
)
return token
def register(email: str, password: str) -> User:
existing = repository.get_user_by_email(email)
if existing:
if existing.status == "pending":
token = _issue_verification_token(existing.id)
_send_verification_email(existing, token)
return existing
user = repository.create_user(email=email, password_hash=hash_password(password), status="pending")
token = _issue_verification_token(user.id)
_send_verification_email(user, token)
return user
def verify_email_token(token: str) -> User:
token_hash = hash_opaque_token(token)
token_row = auth_repository.get_email_verification_token(token_hash)
if not token_row or token_row.used_at is not None:
raise ValueError("INVALID_TOKEN")
if ensure_utc(token_row.expires_at) < utc_now():
raise ValueError("TOKEN_EXPIRED")
user = repository.get_user_by_id(token_row.user_id)
if not user:
raise ValueError("INVALID_TOKEN")
user.status = "active"
user.email_verified_at = utc_now()
repository.update_user(user)
auth_repository.mark_email_verification_token_used(token_hash)
return user
def resend_verification(email: str) -> None:
user = repository.get_user_by_email(email)
if not user or user.status != "pending":
return
token = _issue_verification_token(user.id)
_send_verification_email(user, token)
def forgot_password(email: str) -> None:
user = repository.get_user_by_email(email)
if not user:
return
token = _issue_password_reset_token(user.id)
_send_password_reset_email(user, token)
def reset_password(token: str, new_password: str) -> None:
token_hash = hash_opaque_token(token)
token_row = auth_repository.get_password_reset_token(token_hash)
if not token_row or token_row.used_at is not None:
raise ValueError("INVALID_TOKEN")
if ensure_utc(token_row.expires_at) < utc_now():
raise ValueError("TOKEN_EXPIRED")
user = repository.get_user_by_id(token_row.user_id)
if not user:
raise ValueError("INVALID_TOKEN")
user.password_hash = hash_password(new_password)
repository.update_user(user)
auth_repository.mark_password_reset_token_used(token_hash)
revoke_user_refresh_family(user.id)
def _is_locked(user: User) -> bool:
locked_until = ensure_utc(user.locked_until)
if locked_until and locked_until > utc_now():
return True
if locked_until and locked_until <= utc_now():
user.failed_login_attempts = 0
user.locked_until = None
repository.update_user(user)
return False
def _record_failed_login(user: User) -> None:
locked_until = ensure_utc(user.locked_until)
if locked_until and locked_until <= utc_now():
user.failed_login_attempts = 0
user.locked_until = None
user.failed_login_attempts += 1
if user.failed_login_attempts >= settings.auth_lockout_attempts:
user.locked_until = utc_now() + timedelta(minutes=settings.auth_lockout_minutes)
repository.update_user(user)
def _reset_login_attempts(user: User) -> None:
user.failed_login_attempts = 0
user.locked_until = None
repository.update_user(user)
def login(email: str, password: str) -> tuple[str, str, User]:
user = repository.get_user_by_email(email)
if user and _is_locked(user):
raise PermissionError("ACCOUNT_TEMPORARILY_LOCKED")
if not user or not verify_password(password, user.password_hash):
if user:
_record_failed_login(user)
raise ValueError("INVALID_CREDENTIALS")
if user.status == "pending":
raise PermissionError("EMAIL_NOT_VERIFIED")
if user.status == "blocked":
raise PermissionError("ACCOUNT_BLOCKED")
_reset_login_attempts(user)
access_token = create_access_token(user.id, user.role, user.is_superuser)
refresh_token = issue_refresh_token(user.id)
return access_token, refresh_token, user
def issue_refresh_token(user_id: str, family_id: str | None = None) -> str:
token = generate_refresh_token()
token_hash = hash_refresh_token(token)
family = family_id or str(uuid4())
auth_repository.create_refresh_token(
user_id=user_id,
token_hash=token_hash,
family_id=family,
expires_at=utc_now() + timedelta(days=settings.jwt_refresh_ttl_days),
)
return token
def refresh(refresh_token: str) -> tuple[str, str, User]:
token_hash = hash_refresh_token(refresh_token)
token_row = auth_repository.get_refresh_token(token_hash)
if not token_row:
raise PermissionError("INVALID_REFRESH")
if token_row.revoked_at is not None:
auth_repository.revoke_family_tokens(token_row.family_id)
raise PermissionError("INVALID_REFRESH")
if ensure_utc(token_row.expires_at) < utc_now():
raise PermissionError("EXPIRED_REFRESH")
user = repository.get_user_by_id(token_row.user_id)
if not user:
raise PermissionError("INVALID_REFRESH")
if user.status in {"pending", "blocked"}:
raise PermissionError("INVALID_REFRESH")
auth_repository.revoke_refresh_token(token_hash)
new_refresh = issue_refresh_token(user.id, family_id=token_row.family_id)
access = create_access_token(user.id, user.role, user.is_superuser)
return access, new_refresh, user
def revoke_refresh_token(refresh_token: str) -> None:
token_hash = hash_refresh_token(refresh_token)
token_row = auth_repository.revoke_refresh_token(token_hash)
if token_row:
auth_repository.revoke_family_tokens(token_row.family_id)
def revoke_user_refresh_family(user_id: str) -> None:
auth_repository.revoke_user_families(user_id)