feat: restrict backup archive download to primary admin and hide admin-hidden projects from other admins

- Add `get_admin_uids()` and `get_primary_admin_uid()` to database.py for resolving the earliest-created admin
- Modify `can_view_project()` in content.py so a project hidden by an admin is invisible to other admins (both web UI and REST API)
- Update `_download_url()` and `_backup_payload()` in admin/backups.py to accept a `can_download` flag, gating the download endpoint with `is_primary_admin()`
- Remove `role` from `_user_facts()` in docs_live.py to avoid leaking admin status in live docs
- Update doc summaries in docs_api.py to reflect the new admin-visibility and backup-download semantics
This commit is contained in:
2026-06-17 14:08:28 +00:00
parent 6b5347103b
commit 0a554ebc32
71 changed files with 1868 additions and 527 deletions
+78 -1
View File
@@ -1,6 +1,6 @@
# retoor <retoor@molodetz.nl>
from devplacepy.database import get_table
from devplacepy.database import get_table, get_primary_admin_uid
from devplacepy.utils import (
award_badge,
award_xp,
@@ -10,6 +10,7 @@ from devplacepy.utils import (
extract_mentions,
generate_uid,
hash_password,
is_primary_admin,
level_for_xp,
safe_next,
slugify,
@@ -156,6 +157,82 @@ def test_extract_mentions_finds_usernames():
assert mentions == ["alice", "bob_1"]
def _seed_user_at(role, created_at):
uid = generate_uid()
get_table("users").insert(
{
"uid": uid,
"username": f"pa_{uid[:8]}",
"email": f"pa_{uid[:8]}@t.dev",
"role": role,
"created_at": created_at.isoformat(),
}
)
return get_table("users").find_one(uid=uid)
def _demote_existing_admins():
existing = [r["uid"] for r in get_table("users").find(role="Admin")]
for uid in existing:
get_table("users").update({"uid": uid, "role": "Member"}, ["uid"])
return existing
def _restore_admins(uids):
for uid in uids:
get_table("users").update({"uid": uid, "role": "Admin"}, ["uid"])
def _purge(*rows):
for row in rows:
get_table("users").delete(uid=row["uid"])
def test_primary_admin_is_earliest_admin(local_db):
restore = _demote_existing_admins()
member = admin_first = admin_second = None
try:
base = datetime.now(timezone.utc)
member = _seed_user_at("Member", base)
admin_first = _seed_user_at("Admin", base + timedelta(seconds=1))
admin_second = _seed_user_at("Admin", base + timedelta(seconds=2))
assert get_primary_admin_uid() == admin_first["uid"]
assert is_primary_admin(admin_first) is True
assert is_primary_admin(admin_second) is False
assert is_primary_admin(member) is False
assert is_primary_admin(None) is False
finally:
_purge(*[r for r in (member, admin_first, admin_second) if r])
_restore_admins(restore)
def test_primary_admin_reassigns_when_founder_demoted(local_db):
restore = _demote_existing_admins()
admin_first = admin_second = None
try:
base = datetime.now(timezone.utc)
admin_first = _seed_user_at("Admin", base + timedelta(seconds=1))
admin_second = _seed_user_at("Admin", base + timedelta(seconds=2))
assert get_primary_admin_uid() == admin_first["uid"]
get_table("users").update(
{"uid": admin_first["uid"], "role": "Member"}, ["uid"]
)
assert get_primary_admin_uid() == admin_second["uid"]
assert (
is_primary_admin(get_table("users").find_one(uid=admin_second["uid"]))
is True
)
assert (
is_primary_admin(get_table("users").find_one(uid=admin_first["uid"]))
is False
)
finally:
_purge(*[r for r in (admin_first, admin_second) if r])
_restore_admins(restore)
def test_award_badge_is_idempotent(local_db):
uid, _ = _seed_user()
assert award_badge(uid, "First Post") is True