feat(demo): scheduled demo-account reset (5c)

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.
This commit is contained in:
npeter83 2026-06-19 15:25:13 +02:00
parent 7ca28189bb
commit 571f337fdf
6 changed files with 39 additions and 12 deletions

View file

@ -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)}