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
@@ -0,0 +1,26 @@
from __future__ import annotations
def _super_headers(client) -> dict[str, str]:
login = client.post(
"/api/v1/auth/login",
json={"email": "admin@compton.example", "password": "Admin1234"},
)
assert login.status_code == 200
return {"Authorization": f"Bearer {login.json()['access_token']}"}
def test_admin_create_user_rejects_weak_password(client):
headers = _super_headers(client)
response = client.post(
"/api/v1/admin/users",
headers=headers,
json={
"email": "weak-pass@example.com",
"password": "password",
"role": "user",
"is_superuser": False,
"status": "active",
},
)
assert response.status_code == 422
@@ -0,0 +1,73 @@
from app.core.security import hash_password
from app.modules.users import repository
def _admin_headers(client):
login = client.post(
"/api/v1/auth/login",
json={"email": "admin@compton.example", "password": "Admin1234"},
)
token = login.json()["access_token"]
return {"Authorization": f"Bearer {token}"}
def _plain_admin_headers(client):
email = "ops-admin@compton.example"
if not repository.get_user_by_email(email):
repository.create_user(
email=email,
password_hash=hash_password("Admin1234"),
role="admin",
is_superuser=False,
status="active",
)
login = client.post("/api/v1/auth/login", json={"email": email, "password": "Admin1234"})
token = login.json()["access_token"]
return {"Authorization": f"Bearer {token}"}
def test_admin_users_list(client):
response = client.get("/api/v1/admin/users", headers=_admin_headers(client))
assert response.status_code == 200
assert "data" in response.json()
def test_non_superuser_cannot_patch_settings(client):
response = client.patch(
"/api/v1/admin/settings",
json={"values": {"enable_docs": False}},
headers=_plain_admin_headers(client),
)
assert response.status_code == 403
assert response.json()["detail"] == "SUPERUSER_ONLY"
def test_superuser_can_patch_settings(client):
response = client.patch(
"/api/v1/admin/settings",
json={"values": {"enable_docs": False}},
headers=_admin_headers(client),
)
assert response.status_code == 200
payload = response.json()
assert "values" in payload
def test_superuser_can_create_and_delete_user(client):
created = client.post(
"/api/v1/admin/users",
json={
"email": "created-by-admin@compton.example",
"password": "StrongPass123A",
"role": "user",
"is_superuser": False,
"status": "active",
},
headers=_admin_headers(client),
)
assert created.status_code == 200
user_id = created.json()["id"]
deleted = client.delete(f"/api/v1/admin/users/{user_id}", headers=_admin_headers(client))
assert deleted.status_code == 200
assert deleted.json()["status"] == "deleted"
@@ -0,0 +1,35 @@
from __future__ import annotations
def _login(client, email: str, password: str) -> dict[str, str]:
response = client.post("/api/v1/auth/login", json={"email": email, "password": password})
assert response.status_code == 200
return {"Authorization": f"Bearer {response.json()['access_token']}"}
def test_superuser_can_read_install_secrets(client):
headers = _login(client, "admin@compton.example", "Admin1234")
response = client.get("/api/v1/admin/secrets", headers=headers)
assert response.status_code == 200
payload = response.json()
assert "secrets_status" in payload
assert payload["secrets_status"]["postgres_password"] in {"configured", "missing"}
assert "POSTGRES_PASSWORD" not in str(payload)
def test_non_superuser_cannot_read_install_secrets(client):
headers = _login(client, "ops@compton.example", "OpsAdmin1234")
response = client.get("/api/v1/admin/secrets", headers=headers)
assert response.status_code == 403
def test_superuser_can_reveal_db_password(client):
headers = _login(client, "admin@compton.example", "Admin1234")
response = client.post(
"/api/v1/admin/secrets/reveal",
headers=headers,
json={"key": "database_password"},
)
assert response.status_code == 200
assert response.json()["key"] == "database_password"
assert "value" in response.json()
@@ -0,0 +1,33 @@
from unittest.mock import patch
from app.modules.admin.service import patch_user
from app.modules.users import repository
from app.modules.users.repository import create_user, get_user_by_email
from app.core.security import hash_password
def test_admin_cannot_self_demote():
admin = get_user_by_email("admin@compton.example")
try:
patch_user(admin, admin.id, "user", None)
assert False, "Expected self-demotion error"
except ValueError as exc:
assert str(exc) == "SELF_DEMOTION_FORBIDDEN"
def test_admin_can_promote_user():
admin = get_user_by_email("admin@compton.example")
regular = create_user("sample@example.com", hash_password("Valid123"), role="user", status="active")
result = patch_user(admin, regular.id, "admin", None)
assert result["role"] == "admin"
def test_last_admin_protected():
admin = get_user_by_email("admin@compton.example")
target = create_user("target@example.com", hash_password("Valid123"), role="admin", status="active")
with patch.object(repository, "count_admins", return_value=1):
try:
patch_user(admin, target.id, "user", None)
assert False, "Expected last-admin protection"
except ValueError as exc:
assert str(exc) == "LAST_ADMIN_PROTECTED"
@@ -0,0 +1,42 @@
from tests.helpers import latest_token, register_and_verify
def test_health(client):
response = client.get("/api/v1/health")
assert response.status_code == 200
assert response.json()["status"] == "ok"
def test_register_and_verify_and_login(client):
register_response = client.post(
"/api/v1/auth/register",
json={"email": "user@example.com", "password": "Valid123"},
)
assert register_response.status_code == 200
assert "user_id" not in register_response.json()
duplicate = client.post(
"/api/v1/auth/register",
json={"email": "user@example.com", "password": "Valid123"},
)
assert duplicate.status_code == 200
assert "user_id" not in duplicate.json()
verify_response = client.post(
"/api/v1/auth/verify-email",
json={"token": latest_token("user@example.com", "verify_email")},
)
assert verify_response.status_code == 200
login_response = client.post(
"/api/v1/auth/login",
json={"email": "user@example.com", "password": "Valid123"},
)
assert login_response.status_code == 200
assert "access_token" in login_response.json()
def test_forgot_password_anti_enumeration(client):
response = client.post("/api/v1/auth/forgot-password", json={"email": "missing@example.com"})
assert response.status_code == 200
assert "If email is registered" in response.json()["message"]
@@ -0,0 +1,20 @@
from tests.helpers import latest_token
def test_verify_email_rejects_invalid_token(client):
client.post("/api/v1/auth/register", json={"email": "bad@example.com", "password": "Valid123"})
response = client.post("/api/v1/auth/verify-email", json={"token": "invalid-token"})
assert response.status_code == 400
assert response.json()["detail"] == "INVALID_TOKEN"
def test_resend_verification_sends_new_token(client):
client.post("/api/v1/auth/register", json={"email": "resend@example.com", "password": "Valid123"})
first_token = latest_token("resend@example.com", "verify_email")
client.post("/api/v1/auth/resend-verification", json={"email": "resend@example.com"})
second_token = latest_token("resend@example.com", "verify_email")
assert first_token != second_token
verify = client.post("/api/v1/auth/verify-email", json={"token": second_token})
assert verify.status_code == 200
@@ -0,0 +1,19 @@
from tests.helpers import latest_token, register_and_verify
def test_auth_brute_force_lockout(client):
register_and_verify(client, "locked@example.com")
for _ in range(5):
response = client.post(
"/api/v1/auth/login",
json={"email": "locked@example.com", "password": "WrongPass1"},
)
assert response.status_code == 401
locked = client.post(
"/api/v1/auth/login",
json={"email": "locked@example.com", "password": "Valid123"},
)
assert locked.status_code == 429
assert locked.json()["detail"] == "ACCOUNT_TEMPORARILY_LOCKED"
@@ -0,0 +1,7 @@
from app.core.password_policy import validate_password_strength
import pytest
def test_password_policy_rejects_denied_password():
with pytest.raises(ValueError, match="too common"):
validate_password_strength("Password123")
@@ -0,0 +1,12 @@
import pytest
from app.core.password_policy import validate_password_strength
def test_password_policy_accepts_valid_password():
assert validate_password_strength("Valid123") == "Valid123"
def test_password_policy_rejects_weak_password():
with pytest.raises(ValueError, match="uppercase letter"):
validate_password_strength("valid123")
@@ -0,0 +1,22 @@
from uuid import uuid4
def test_login_rate_limit(client, monkeypatch):
from app.core.config import settings
monkeypatch.setattr(settings, "enable_rate_limit", True)
email = f"missing-{uuid4().hex}@example.com"
for _ in range(5):
response = client.post(
"/api/v1/auth/login",
json={"email": email, "password": "Valid123"},
)
assert response.status_code == 401
blocked = client.post(
"/api/v1/auth/login",
json={"email": email, "password": "Valid123"},
)
assert blocked.status_code == 429
assert blocked.json()["detail"] == "RATE_LIMIT_EXCEEDED"
@@ -0,0 +1,47 @@
from __future__ import annotations
from tests.helpers import register_and_verify
def _admin_headers(client) -> dict[str, str]:
from app.core.security import create_access_token
from app.modules.users.repository import get_user_by_email
admin = get_user_by_email("admin@compton.example")
token = create_access_token(admin.id, admin.role, admin.is_superuser)
return {"Authorization": f"Bearer {token}"}
def test_refresh_fails_for_blocked_user(client):
register_and_verify(client, "blocked-refresh@example.com")
login = client.post(
"/api/v1/auth/login",
json={"email": "blocked-refresh@example.com", "password": "Valid123"},
)
assert login.status_code == 200
admin_headers = _admin_headers(client)
me = client.get(
"/api/v1/users/me",
headers={"Authorization": f"Bearer {login.json()['access_token']}"},
)
user_id = me.json()["user"]["id"]
blocked = client.patch(
f"/api/v1/admin/users/{user_id}",
headers=admin_headers,
json={"status": "blocked"},
)
assert blocked.status_code == 200
refresh = client.post("/api/v1/auth/refresh", headers={"Origin": "http://localhost:5173"})
assert refresh.status_code == 401
def test_refresh_requires_origin_header_when_cookie_present(client):
register_and_verify(client, "origin-required@example.com")
login = client.post(
"/api/v1/auth/login",
json={"email": "origin-required@example.com", "password": "Valid123"},
)
assert login.status_code == 200
response = client.post("/api/v1/auth/refresh")
assert response.status_code == 403
assert response.json()["detail"] == "INVALID_ORIGIN"
@@ -0,0 +1,26 @@
from app.core.security import (
create_access_token,
decode_access_token,
generate_refresh_token,
hash_password,
hash_refresh_token,
verify_password,
)
def test_password_hashing_roundtrip():
hashed = hash_password("Strong123")
assert verify_password("Strong123", hashed) is True
def test_access_token_encode_decode():
token = create_access_token("u1", "user", False)
payload = decode_access_token(token)
assert payload["sub"] == "u1"
assert payload["role"] == "user"
assert payload["is_superuser"] is False
def test_refresh_token_hashing():
token = generate_refresh_token()
assert hash_refresh_token(token) == hash_refresh_token(token)
@@ -0,0 +1,45 @@
def _admin_headers(client):
login = client.post(
"/api/v1/auth/login",
json={"email": "admin@compton.example", "password": "Admin1234"},
)
token = login.json()["access_token"]
return {"Authorization": f"Bearer {token}"}
def test_create_and_read_published_page(client):
headers = _admin_headers(client)
created = client.post(
"/api/v1/content/pages",
headers=headers,
json={"slug": "about", "title": "About", "body": "<p>Hello</p>", "status": "published"},
)
assert created.status_code == 200
fetched = client.get("/api/v1/content/pages/about")
assert fetched.status_code == 200
assert fetched.json()["slug"] == "about"
def test_list_all_pages_requires_admin(client):
response = client.get("/api/v1/content/pages/manage/all")
assert response.status_code == 401
def test_list_all_pages_includes_drafts(client):
headers = _admin_headers(client)
created = client.post(
"/api/v1/content/pages",
headers=headers,
json={"slug": "draft-page", "title": "Draft", "body": "<p>Draft</p>", "status": "draft"},
)
assert created.status_code == 200
listed = client.get("/api/v1/content/pages/manage/all", headers=headers)
assert listed.status_code == 200
slugs = [page["slug"] for page in listed.json()["data"]]
assert "draft-page" in slugs
public = client.get("/api/v1/content/pages")
public_slugs = [page["slug"] for page in public.json()["data"]]
assert "draft-page" not in public_slugs
@@ -0,0 +1,9 @@
from __future__ import annotations
from app.modules.content.service import sanitize_html
def test_sanitize_html_strips_javascript_protocol():
raw = '<a href="javascript:alert(1)">x</a><img src="javascript:alert(1)" alt="x" />'
clean = sanitize_html(raw)
assert "javascript:" not in clean
@@ -0,0 +1,21 @@
def test_media_requires_signature(client):
response = client.get("/api/v1/media/files/avatars/missing.png")
assert response.status_code == 422
def test_media_file_not_found(client):
from app.core.media_signing import build_signed_media_url
signed_url = build_signed_media_url("/api/v1/media/files/avatars/missing.png")
response = client.get(signed_url)
assert response.status_code == 404
assert response.json()["detail"] == "FILE_NOT_FOUND"
def test_media_rejects_non_avatar_path(client):
from app.core.media_signing import build_signed_media_url
signed_url = build_signed_media_url("/api/v1/media/files/other/file.png")
response = client.get(signed_url)
assert response.status_code == 404
assert response.json()["detail"] == "FILE_NOT_FOUND"
@@ -0,0 +1,14 @@
import pytest
from app.modules.media.service import AvatarValidationError, validate_and_process_avatar
from tests.fixtures import make_test_png
def test_validate_rejects_empty():
with pytest.raises(AvatarValidationError, match="INVALID_IMAGE"):
validate_and_process_avatar(b"")
def test_validate_rejects_corrupt_bytes():
with pytest.raises(AvatarValidationError, match="INVALID_IMAGE"):
validate_and_process_avatar(b"not-an-image")
@@ -0,0 +1,7 @@
from app.modules.notifications.service import enqueue_email
def test_enqueue_email_returns_payload():
data = enqueue_email("verify", "u@example.com", {"token": "123"})
assert data["queued"] is True
assert data["template"] == "verify"
@@ -0,0 +1,170 @@
from tests.fixtures import make_test_png
from tests.helpers import latest_token, register_and_verify, register_verify_login
def _admin_headers(client) -> dict[str, str]:
login = client.post(
"/api/v1/auth/login",
json={"email": "admin@compton.example", "password": "Admin1234"},
)
token = login.json()["access_token"]
return {"Authorization": f"Bearer {token}"}
def test_auth_refresh_and_logout(client):
register_and_verify(client, "refresh@example.com")
login = client.post(
"/api/v1/auth/login",
json={"email": "refresh@example.com", "password": "Valid123"},
)
assert login.status_code == 200
assert "refresh_token" in login.cookies
refresh = client.post("/api/v1/auth/refresh", headers={"Origin": "http://localhost:5173"})
assert refresh.status_code == 200
assert "access_token" in refresh.json()
logout = client.post("/api/v1/auth/logout", headers={"Origin": "http://localhost:5173"})
assert logout.status_code == 200
def test_auth_invalid_login(client):
response = client.post(
"/api/v1/auth/login",
json={"email": "missing@example.com", "password": "Valid123"},
)
assert response.status_code == 401
def test_users_patch_and_password_and_avatar(client):
headers = register_verify_login(client, "patch@example.com")
patch = client.patch("/api/v1/users/me", headers=headers, json={"display_name": "Patched"})
assert patch.status_code == 200
assert patch.json()["profile"]["display_name"] == "Patched"
bad_password = client.post(
"/api/v1/users/me/password",
headers=headers,
json={"current_password": "wrong", "new_password": "NewValid1"},
)
assert bad_password.status_code == 400
avatar = client.post(
"/api/v1/users/me/avatar",
headers=headers,
files={"file": ("avatar.png", make_test_png(), "image/png")},
)
assert avatar.status_code == 200
assert avatar.json()["profile"]["avatar_url"]
def test_users_unauthorized(client):
response = client.get("/api/v1/users/me")
assert response.status_code == 401
def test_content_crud_flow(client):
headers = _admin_headers(client)
created = client.post(
"/api/v1/content/pages",
headers=headers,
json={"slug": "terms", "title": "Terms", "body": "<p>Terms</p>", "status": "draft"},
)
assert created.status_code == 200
page_id = created.json()["id"]
listed = client.get("/api/v1/content/pages")
assert listed.status_code == 200
updated = client.patch(
f"/api/v1/content/pages/{page_id}",
headers=headers,
json={"status": "published"},
)
assert updated.status_code == 200
fetched = client.get("/api/v1/content/pages/terms")
assert fetched.status_code == 200
deleted = client.delete(f"/api/v1/content/pages/{page_id}", headers=headers)
assert deleted.status_code == 200
def test_admin_stats_and_patch_user(client):
headers = _admin_headers(client)
stats = client.get("/api/v1/admin/stats", headers=headers)
assert stats.status_code == 200
user_headers = register_verify_login(client, "blockme@example.com")
me = client.get("/api/v1/users/me", headers=user_headers)
user_id = me.json()["user"]["id"]
blocked = client.patch(
f"/api/v1/admin/users/{user_id}",
headers=headers,
json={"status": "blocked"},
)
assert blocked.status_code == 200
assert blocked.json()["status"] == "blocked"
def test_auth_pending_user_cannot_login(client):
client.post("/api/v1/auth/register", json={"email": "pending@example.com", "password": "Valid123"})
response = client.post(
"/api/v1/auth/login",
json={"email": "pending@example.com", "password": "Valid123"},
)
assert response.status_code == 403
assert response.json()["detail"] == "EMAIL_NOT_VERIFIED"
def test_pending_user_cannot_access_profile(client):
from app.core.security import create_access_token
from app.modules.users.repository import get_user_by_email
client.post("/api/v1/auth/register", json={"email": "pendingme@example.com", "password": "Valid123"})
user = get_user_by_email("pendingme@example.com")
token = create_access_token(user.id, user.role)
response = client.get("/api/v1/users/me", headers={"Authorization": f"Bearer {token}"})
assert response.status_code == 403
assert response.json()["detail"] == "EMAIL_NOT_VERIFIED"
def test_auth_invalid_refresh_token(client):
response = client.post("/api/v1/auth/refresh")
assert response.status_code == 401
client.cookies.set("refresh_token", "invalid-token", path="/api/v1/auth")
invalid = client.post("/api/v1/auth/refresh", headers={"Origin": "http://localhost:5173"})
assert invalid.status_code == 401
def test_auth_resend_and_reset_password(client):
client.post("/api/v1/auth/register", json={"email": "resetme@example.com", "password": "Valid123"})
resend = client.post("/api/v1/auth/resend-verification", json={"email": "resetme@example.com"})
assert resend.status_code == 200
verify_token = latest_token("resetme@example.com", "verify_email")
verify = client.post("/api/v1/auth/verify-email", json={"token": verify_token})
assert verify.status_code == 200
forgot = client.post("/api/v1/auth/forgot-password", json={"email": "resetme@example.com"})
assert forgot.status_code == 200
reset_token = latest_token("resetme@example.com", "reset_password")
reset = client.post(
"/api/v1/auth/reset-password",
json={"token": reset_token, "new_password": "NewValid1"},
)
assert reset.status_code == 200
login = client.post(
"/api/v1/auth/login",
json={"email": "resetme@example.com", "password": "NewValid1"},
)
assert login.status_code == 200
invalid = client.post(
"/api/v1/auth/reset-password",
json={"token": "invalid-token", "new_password": "NewValid1"},
)
assert invalid.status_code == 400
@@ -0,0 +1,42 @@
from tests.fixtures import make_test_png
from tests.helpers import register_verify_login
def test_upload_avatar_success(client):
headers = register_verify_login(client, "avatar@example.com")
response = client.post(
"/api/v1/users/me/avatar",
headers=headers,
files={"file": ("avatar.png", make_test_png(), "image/png")},
)
assert response.status_code == 200
avatar_url = response.json()["profile"]["avatar_url"]
assert avatar_url.startswith("/api/v1/media/files/avatars/")
media = client.get(avatar_url)
assert media.status_code == 200
assert media.headers["content-type"].startswith("image/")
def test_upload_avatar_rejects_svg(client):
headers = register_verify_login(client, "svg@example.com")
svg = b"<svg xmlns='http://www.w3.org/2000/svg'><rect width='10' height='10'/></svg>"
response = client.post(
"/api/v1/users/me/avatar",
headers=headers,
files={"file": ("avatar.svg", svg, "image/svg+xml")},
)
assert response.status_code == 400
assert response.json()["detail"] in {"INVALID_MIME", "INVALID_IMAGE"}
def test_upload_avatar_rejects_oversize(client):
headers = register_verify_login(client, "big@example.com")
oversized = make_test_png() + b"0" * (2 * 1024 * 1024)
response = client.post(
"/api/v1/users/me/avatar",
headers=headers,
files={"file": ("big.png", oversized, "image/png")},
)
assert response.status_code == 400
assert response.json()["detail"] == "FILE_TOO_LARGE"
@@ -0,0 +1,30 @@
from tests.helpers import register_verify_login
def test_me_endpoint(client):
headers = register_verify_login(client, "me@example.com", "Valid123")
response = client.get("/api/v1/users/me", headers=headers)
assert response.status_code == 200
assert response.json()["user"]["email"] == "me@example.com"
def test_patch_me_updates_display_name(client):
headers = register_verify_login(client, "display@example.com", "Valid123")
response = client.patch(
"/api/v1/users/me",
headers=headers,
json={"display_name": "Updated Name"},
)
assert response.status_code == 200
assert response.json()["profile"]["display_name"] == "Updated Name"
def test_change_password_rejects_invalid_current(client):
headers = register_verify_login(client, "pwd@example.com", "Valid123")
response = client.post(
"/api/v1/users/me/password",
headers=headers,
json={"current_password": "Wrong123", "new_password": "NewValid1"},
)
assert response.status_code == 400
assert response.json()["detail"] == "INVALID_CURRENT_PASSWORD"