From 571f337fdfb840f3e6e745ece7e85a279a345021 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Fri, 19 Jun 2026 15:25:13 +0200 Subject: [PATCH] feat(demo): scheduled demo-account reset (5c) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A security audit of every mutating endpoint found no isolation gaps — all routes scope by current_user, admin routes are gated, and the demo account is sandboxed in its own user_id space (can't reach others' data, escalate, or hit admin routes). The remaining demo concern is communal pollution of its own shared state, so add an automatic reset: a new 'demo_reset' scheduler job (admin-tunable interval, default 12h) reuses the manual reset logic, and no-ops without ever creating a demo account if none exists. Verified: a triggered run wiped the demo's video states and re-seeded its sample playlists. --- backend/app/config.py | 2 ++ backend/app/routes/admin.py | 24 +++++++++++++-------- backend/app/scheduler.py | 16 ++++++++++++++ frontend/src/i18n/locales/de/scheduler.json | 3 ++- frontend/src/i18n/locales/en/scheduler.json | 3 ++- frontend/src/i18n/locales/hu/scheduler.json | 3 ++- 6 files changed, 39 insertions(+), 12 deletions(-) diff --git a/backend/app/config.py b/backend/app/config.py index ee8aa4a..da51958 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -94,6 +94,8 @@ class Settings(BaseSettings): autotag_interval_minutes: int = 30 subscriptions_resync_minutes: int = 360 playlist_sync_minutes: int = 360 + # How often the shared demo account is auto-reset to a clean baseline (communal sandbox). + demo_reset_minutes: int = 720 # --- Maintenance / validation job --- # Runs once a day by default (admin-tunable via the Scheduler dashboard). Grace periods # are in days because that's the granularity that matters; the rolling re-validation diff --git a/backend/app/routes/admin.py b/backend/app/routes/admin.py index 7151788..176c1b9 100644 --- a/backend/app/routes/admin.py +++ b/backend/app/routes/admin.py @@ -270,14 +270,10 @@ def _seed_demo_playlists(db: Session, demo: User) -> int: return created -@router.post("/demo/reset") -def reset_demo( - _: User = Depends(admin_user), db: Session = Depends(get_db) -) -> dict: - """Wipe the shared demo account's per-user state (watch/save/hide states, playlists, - preferences) back to a clean baseline and re-seed a few sample playlists. There's no - automatic reset — this is the admin's manual 'clean up the sandbox' button.""" - demo = get_or_create_demo_user(db) +def reset_demo_state(db: Session, demo: User) -> int: + """Wipe the demo account's per-user state (watch/save/hide states, playlists, preferences) + back to a clean baseline and re-seed a few sample playlists. Returns the seeded count. + Shared by the admin's manual reset button and the scheduled demo reset (see scheduler).""" db.execute(delete(VideoState).where(VideoState.user_id == demo.id)) # Playlist items cascade on playlist delete (ondelete="CASCADE"). db.execute(delete(Playlist).where(Playlist.user_id == demo.id)) @@ -285,4 +281,14 @@ def reset_demo( db.commit() seeded = _seed_demo_playlists(db, demo) db.commit() - return {"reset": True, "playlists_seeded": seeded} + return seeded + + +@router.post("/demo/reset") +def reset_demo( + _: User = Depends(admin_user), db: Session = Depends(get_db) +) -> dict: + """Manually clean up the shared demo sandbox back to a baseline. A scheduled job does the + same automatically (admin-tunable interval); this is the admin's on-demand button.""" + demo = get_or_create_demo_user(db) + return {"reset": True, "playlists_seeded": reset_demo_state(db, demo)} diff --git a/backend/app/scheduler.py b/backend/app/scheduler.py index 8c08c40..e726929 100644 --- a/backend/app/scheduler.py +++ b/backend/app/scheduler.py @@ -56,6 +56,7 @@ JOB_INTERVALS: dict[str, int] = { "subscriptions": settings.subscriptions_resync_minutes, "playlist_sync": settings.playlist_sync_minutes, "maintenance": settings.maintenance_interval_minutes, + "demo_reset": settings.demo_reset_minutes, } # Sane bounds for an admin-set interval (minutes). @@ -253,6 +254,20 @@ def _maintenance_job() -> None: _job("maintenance", work) +def _demo_reset_job() -> None: + # Auto-clean the shared demo sandbox. No-op (and never creates one) if there's no demo + # account. Lazy import avoids a routes<->scheduler import cycle. + def work(db): + from app.models import User + demo = db.query(User).filter(User.is_demo.is_(True)).one_or_none() + if demo is None: + return {"skipped": "no demo account"} + from app.routes.admin import reset_demo_state + return {"playlists_seeded": reset_demo_state(db, demo)} + + _job("demo_reset", work) + + # job_id -> wrapper. The single source of truth for which jobs exist and how to run one, # shared by start_scheduler (recurring registration) and trigger_job (manual "run now"). JOB_FUNCS: dict[str, Callable[[], None]] = { @@ -264,6 +279,7 @@ JOB_FUNCS: dict[str, Callable[[], None]] = { "subscriptions": _subscriptions_job, "playlist_sync": _playlist_sync_job, "maintenance": _maintenance_job, + "demo_reset": _demo_reset_job, } diff --git a/frontend/src/i18n/locales/de/scheduler.json b/frontend/src/i18n/locales/de/scheduler.json index d1a451f..a7da5c2 100644 --- a/frontend/src/i18n/locales/de/scheduler.json +++ b/frontend/src/i18n/locales/de/scheduler.json @@ -76,7 +76,8 @@ "shorts": "Shorts-Klassifizierung", "subscriptions": "Abo-Resync", "playlist_sync": "YouTube-Wiedergabelisten-Sync", - "maintenance": "Wartung + Validierung" + "maintenance": "Wartung + Validierung", + "demo_reset": "Demo-Konto-Reset" }, "queue": { "recentPending": "Zu synchronisierende Kanäle", diff --git a/frontend/src/i18n/locales/en/scheduler.json b/frontend/src/i18n/locales/en/scheduler.json index f9b0073..fabe864 100644 --- a/frontend/src/i18n/locales/en/scheduler.json +++ b/frontend/src/i18n/locales/en/scheduler.json @@ -76,7 +76,8 @@ "shorts": "Shorts classification", "subscriptions": "Subscription resync", "playlist_sync": "YouTube playlist sync", - "maintenance": "Maintenance + validation" + "maintenance": "Maintenance + validation", + "demo_reset": "Demo account reset" }, "queue": { "recentPending": "Channels to sync", diff --git a/frontend/src/i18n/locales/hu/scheduler.json b/frontend/src/i18n/locales/hu/scheduler.json index 7a8bc36..5e35c7a 100644 --- a/frontend/src/i18n/locales/hu/scheduler.json +++ b/frontend/src/i18n/locales/hu/scheduler.json @@ -76,7 +76,8 @@ "shorts": "Shorts-besorolás", "subscriptions": "Feliratkozások újraszinkronja", "playlist_sync": "YouTube lejátszási listák szinkronja", - "maintenance": "Karbantartás + ellenőrzés" + "maintenance": "Karbantartás + ellenőrzés", + "demo_reset": "Demo fiók visszaállítása" }, "queue": { "recentPending": "Szinkronra váró csatorna",