Merge feature/auth-5c-demo (demo security audit + scheduled reset)
Audit: no authorization/isolation gaps. Add an admin-tunable scheduled demo-reset job (reuses the manual reset; no-op if no demo account).
This commit is contained in:
commit
8c38626dc7
6 changed files with 39 additions and 12 deletions
|
|
@ -94,6 +94,8 @@ class Settings(BaseSettings):
|
||||||
autotag_interval_minutes: int = 30
|
autotag_interval_minutes: int = 30
|
||||||
subscriptions_resync_minutes: int = 360
|
subscriptions_resync_minutes: int = 360
|
||||||
playlist_sync_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 ---
|
# --- Maintenance / validation job ---
|
||||||
# Runs once a day by default (admin-tunable via the Scheduler dashboard). Grace periods
|
# 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
|
# are in days because that's the granularity that matters; the rolling re-validation
|
||||||
|
|
|
||||||
|
|
@ -270,14 +270,10 @@ def _seed_demo_playlists(db: Session, demo: User) -> int:
|
||||||
return created
|
return created
|
||||||
|
|
||||||
|
|
||||||
@router.post("/demo/reset")
|
def reset_demo_state(db: Session, demo: User) -> int:
|
||||||
def reset_demo(
|
"""Wipe the demo account's per-user state (watch/save/hide states, playlists, preferences)
|
||||||
_: User = Depends(admin_user), db: Session = Depends(get_db)
|
back to a clean baseline and re-seed a few sample playlists. Returns the seeded count.
|
||||||
) -> dict:
|
Shared by the admin's manual reset button and the scheduled demo reset (see scheduler)."""
|
||||||
"""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)
|
|
||||||
db.execute(delete(VideoState).where(VideoState.user_id == demo.id))
|
db.execute(delete(VideoState).where(VideoState.user_id == demo.id))
|
||||||
# Playlist items cascade on playlist delete (ondelete="CASCADE").
|
# Playlist items cascade on playlist delete (ondelete="CASCADE").
|
||||||
db.execute(delete(Playlist).where(Playlist.user_id == demo.id))
|
db.execute(delete(Playlist).where(Playlist.user_id == demo.id))
|
||||||
|
|
@ -285,4 +281,14 @@ def reset_demo(
|
||||||
db.commit()
|
db.commit()
|
||||||
seeded = _seed_demo_playlists(db, demo)
|
seeded = _seed_demo_playlists(db, demo)
|
||||||
db.commit()
|
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)}
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ JOB_INTERVALS: dict[str, int] = {
|
||||||
"subscriptions": settings.subscriptions_resync_minutes,
|
"subscriptions": settings.subscriptions_resync_minutes,
|
||||||
"playlist_sync": settings.playlist_sync_minutes,
|
"playlist_sync": settings.playlist_sync_minutes,
|
||||||
"maintenance": settings.maintenance_interval_minutes,
|
"maintenance": settings.maintenance_interval_minutes,
|
||||||
|
"demo_reset": settings.demo_reset_minutes,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Sane bounds for an admin-set interval (minutes).
|
# Sane bounds for an admin-set interval (minutes).
|
||||||
|
|
@ -253,6 +254,20 @@ def _maintenance_job() -> None:
|
||||||
_job("maintenance", work)
|
_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,
|
# 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").
|
# shared by start_scheduler (recurring registration) and trigger_job (manual "run now").
|
||||||
JOB_FUNCS: dict[str, Callable[[], None]] = {
|
JOB_FUNCS: dict[str, Callable[[], None]] = {
|
||||||
|
|
@ -264,6 +279,7 @@ JOB_FUNCS: dict[str, Callable[[], None]] = {
|
||||||
"subscriptions": _subscriptions_job,
|
"subscriptions": _subscriptions_job,
|
||||||
"playlist_sync": _playlist_sync_job,
|
"playlist_sync": _playlist_sync_job,
|
||||||
"maintenance": _maintenance_job,
|
"maintenance": _maintenance_job,
|
||||||
|
"demo_reset": _demo_reset_job,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,8 @@
|
||||||
"shorts": "Shorts-Klassifizierung",
|
"shorts": "Shorts-Klassifizierung",
|
||||||
"subscriptions": "Abo-Resync",
|
"subscriptions": "Abo-Resync",
|
||||||
"playlist_sync": "YouTube-Wiedergabelisten-Sync",
|
"playlist_sync": "YouTube-Wiedergabelisten-Sync",
|
||||||
"maintenance": "Wartung + Validierung"
|
"maintenance": "Wartung + Validierung",
|
||||||
|
"demo_reset": "Demo-Konto-Reset"
|
||||||
},
|
},
|
||||||
"queue": {
|
"queue": {
|
||||||
"recentPending": "Zu synchronisierende Kanäle",
|
"recentPending": "Zu synchronisierende Kanäle",
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,8 @@
|
||||||
"shorts": "Shorts classification",
|
"shorts": "Shorts classification",
|
||||||
"subscriptions": "Subscription resync",
|
"subscriptions": "Subscription resync",
|
||||||
"playlist_sync": "YouTube playlist sync",
|
"playlist_sync": "YouTube playlist sync",
|
||||||
"maintenance": "Maintenance + validation"
|
"maintenance": "Maintenance + validation",
|
||||||
|
"demo_reset": "Demo account reset"
|
||||||
},
|
},
|
||||||
"queue": {
|
"queue": {
|
||||||
"recentPending": "Channels to sync",
|
"recentPending": "Channels to sync",
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,8 @@
|
||||||
"shorts": "Shorts-besorolás",
|
"shorts": "Shorts-besorolás",
|
||||||
"subscriptions": "Feliratkozások újraszinkronja",
|
"subscriptions": "Feliratkozások újraszinkronja",
|
||||||
"playlist_sync": "YouTube lejátszási listák szinkronja",
|
"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": {
|
"queue": {
|
||||||
"recentPending": "Szinkronra váró csatorna",
|
"recentPending": "Szinkronra váró csatorna",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue