pdf-quiz-generator/ADMIN.md
Daniel 1f2770257f
Some checks failed
Tests / backend (push) Failing after 9s
Tests / frontend (push) Successful in 29s
Tests / e2e (push) Failing after 27s
docs: a Help page for the people who use the site, a Handbook for whoever runs it
They were one page, and it served neither. Somebody wanting to know what
"Studying for" does had to scroll past what the tutor is prompted with
and what re-embedding breaks.

/help — every signed-in account. Signing in through PedsHub SSO, what
"Studying for" scopes, Qbank against Sessions against Collections, study
and exam mode, where performance comes from, Reading and Cards and study
plans, "Make a deck", and how to report a bad question. An educator also
gets a section of their own: what a moderator has, what a grant gives
and what it does not, that nothing in the bank belongs to anybody, how a
draft becomes an article, how a plan is built, and what a moderator
cannot do. A learner never sees that half.

/handbook — administrators only now, with the same FAQ plus the rest.
The Settings card that pointed at it is admin-only to match, and
RequireAuth learned an admin door, which it did not have.

Help is in the account menu rather than the footer: a question you have
while working is answered from where you are.

And the repo docs describe the site that exists. CLAUDE.md lost the LMS
section — courses, modules, lessons, enrolments, all removed months ago
and still documented — and gained the permission model and the sign-in
flow. ADMIN.md's role table said moderators create courses; it now says
what the three roles actually reach, where roles come from, and that the
bank has no owners.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
2026-09-13 14:12:47 +02:00

12 KiB

PedsHub — Admin Guide

Deployment

# Start everything
docker compose up -d

# Rebuild after code changes (restart alone won't pick up changes)
docker compose build --no-cache backend celery frontend
docker compose up -d backend celery frontend --force-recreate

# View logs
docker compose logs backend --tail=50
docker compose logs celery --tail=50
docker compose logs frontend --tail=50

CLI Management Tool

All commands run inside the backend container:

docker compose exec backend python -m app.cli <command>

User Management

# List all users
docker compose exec backend python -m app.cli list-users

# Reset a user's password
docker compose exec backend python -m app.cli reset-password user@example.com newpassword

# Change user role (admin / moderator / user)
docker compose exec backend python -m app.cli set-role user@example.com moderator

# Force-verify a user's email (skip email confirmation)
docker compose exec backend python -m app.cli verify-email user@example.com

# Delete a user (interactive confirmation)
docker compose exec backend python -m app.cli delete-user user@example.com

# Export users to CSV
docker compose exec backend python -m app.cli export-users > users.csv

Documents

# List all documents with status
docker compose exec backend python -m app.cli list-docs

# Fix documents stuck in 'processing' (>30 min)
docker compose exec backend python -m app.cli fix-stuck-docs

# Requeue a specific document for processing
docker compose exec backend python -m app.cli reprocess-doc 32

Why documents get stuck: If the Celery worker restarts while processing a PDF, the task is lost and the document stays at "processing" forever. fix-stuck-docs resets these to "ready" so they can be reprocessed.

Quizzes

# List quizzes (active only)
docker compose exec backend python -m app.cli list-quizzes

# List all quizzes including deleted
docker compose exec backend python -m app.cli list-quizzes --all

Maintenance

# Platform statistics
docker compose exec backend python -m app.cli stats

# Clean up orphaned Redis keys (quiz progress with no expiry)
docker compose exec backend python -m app.cli cleanup-redis

Database

Direct Access

docker compose exec postgres psql -U pedquiz -d pedquiz

Backups

Automated daily backups run via the db-backup service (prodrigestivill/postgres-backup-local). Retention: 14 daily, 4 weekly, 6 monthly. Stored in ./backups/.

# Check backup status
docker compose logs db-backup --tail=10

# List backups
ls -la backups/

# Manual backup
docker compose exec postgres pg_dump -U pedquiz pedquiz > manual-backup.sql

# Restore from backup
cat backups/daily/pedquiz-YYYYMMDD-HHMMSS.sql.gz | gunzip | \
  docker compose exec -T postgres psql -U pedquiz -d pedquiz

User Roles

Role Capabilities
user Sit sessions, study shared decks, read articles, keep collections and folders. No bank content unless a CategoryGrant says otherwise.
moderator The whole bank — every question, article, category, deck, image library — plus the editorial queue. Nothing that configures the site.
admin Everything, including models, people, site policy and the Handbook.

Roles come from the identity provider. With OIDC_ROLE_CLAIM set, group membership at sso.pedshub.com decides the role and is reapplied at every sign-in; pedshub-admins and pedshub-moderators are the groups. The in-app role endpoints answer 409 while that is on, because a role set here would be overwritten at the next sign-in. The sync refuses to demote the last admin.

Grants are per-user and in-app. /access gives somebody a category branch, an image library or a folder; it makes them an editor of what is inside and adds the Questions and Images menu entries. Never self-assignable, never from a claim.

The bank has no owners. Questions, articles, categories, decks, documents and shared quizzes carry user_id = NULL. Authorship confers no rights. Attempts, notes, favourites, collections, folders and personal sittings stay with their person.

Signing in

SSO only, via Authentik at sso.pedshub.com. No sign-up form, no invite codes, no email sign-in codes — the provider does all three. settings:sso_only in Redis closes every password door. Accounts are created on first sign-in, matched by email address; a different address is a different account.

Common Issues

Document stuck in "processing"

docker compose exec backend python -m app.cli fix-stuck-docs

Cause: Celery worker restarted mid-task. The fix resets status to "ready".

DDL race condition on startup

One or two backend workers may fail on startup with Application startup failed. This is normal — multiple uvicorn workers race to run ALTER TABLE migrations, and losers get a deadlock error. The surviving workers handle all traffic. Check with:

docker compose logs backend --tail=10 | grep "startup complete"

Celery task not running

# Check Celery is connected
docker compose logs celery --tail=5

# Check if tasks are registered
docker compose exec celery celery -A app.tasks inspect registered

# Check active tasks (don't restart if tasks are running!)
docker compose exec celery celery -A app.tasks inspect active

Password reset without email

docker compose exec backend python -m app.cli reset-password user@example.com newpassword

User can't log in (unverified email)

docker compose exec backend python -m app.cli verify-email user@example.com

Environment

Key settings in backend/.env:

Variable Purpose
APP_URL Base URL for email links (e.g. https://pedshub.com)
SECRET_KEY JWT signing key — change in production
MAIL_FROM Sender email for verification/reset emails
LITELLM_API_BASE LiteLLM proxy URL for AI features
LITELLM_API_KEY API key for LiteLLM proxy
CAP_SITE_KEY / CAP_SECRET_KEY Cap bot protection
BBB_SERVER_URL / BBB_SECRET BigBlueButton integration for live sessions
OIDC_PROVIDER_URL OIDC discovery URL (see SSO section below)
OIDC_CLIENT_ID OAuth client ID from your identity provider
OIDC_CLIENT_SECRET OAuth client secret
OIDC_PROVIDER_NAME Display name on login button (default: "SSO")

SSO / OIDC Setup

PedsHub supports any OpenID Connect provider. When configured, a "Sign in with {provider}" button appears on the login page. Users who sign in via SSO are auto-created with a verified email — no separate registration or email verification needed.

How it works

  1. User clicks "Sign in with SSO" on the login page
  2. Browser redirects to your identity provider (Google, Microsoft, etc.)
  3. After authentication, provider redirects back to {APP_URL}/api/auth/sso/callback
  4. PedsHub reads the email and name claims from the OIDC token
  5. If no account exists, one is created automatically (role: user, email verified)
  6. User gets a JWT and is logged in

Required env vars

Add these to backend/.env:

OIDC_PROVIDER_URL=https://accounts.google.com
OIDC_CLIENT_ID=your-client-id-here
OIDC_CLIENT_SECRET=your-client-secret-here
OIDC_PROVIDER_NAME=Google
OIDC_SCOPES=openid email profile

Then rebuild: docker compose build --no-cache backend celery && docker compose up -d backend celery --force-recreate

SSO-only mode

In Admin > Settings, toggle "SSO-Only Login" to disable password login entirely. Users will only see the SSO button. Admins can still reset passwords via CLI for emergency access.


Google

  1. Go to Google Cloud Console
  2. Create a new project (or select existing)
  3. Go to APIs & Services > Credentials > Create Credentials > OAuth client ID
  4. Application type: Web application
  5. Authorized redirect URIs: https://pedshub.com/api/auth/sso/callback
  6. Copy the Client ID and Client Secret
OIDC_PROVIDER_URL=https://accounts.google.com
OIDC_CLIENT_ID=123456789.apps.googleusercontent.com
OIDC_CLIENT_SECRET=GOCSPX-xxxxxxxx
OIDC_PROVIDER_NAME=Google

You may need to enable the "Google+ API" or configure the OAuth consent screen first.

Microsoft / Azure AD

  1. Go to Azure Portal > App registrations
  2. Click New registration
  3. Name: "PedsHub", Redirect URI: https://pedshub.com/api/auth/sso/callback (type: Web)
  4. Note the Application (client) ID and Directory (tenant) ID
  5. Go to Certificates & secrets > New client secret — copy the value
OIDC_PROVIDER_URL=https://login.microsoftonline.com/{tenant-id}/v2.0
OIDC_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
OIDC_CLIENT_SECRET=your-client-secret
OIDC_PROVIDER_NAME=Microsoft

Replace {tenant-id} with your Azure AD tenant ID. For personal Microsoft accounts, use common instead.

Under API permissions, ensure openid, email, and profile are granted.

Keycloak

  1. In your Keycloak admin, go to Clients > Create client
  2. Client ID: pedshub, Client type: OpenID Connect
  3. Valid redirect URIs: https://pedshub.com/api/auth/sso/callback
  4. Copy the client secret from the Credentials tab
OIDC_PROVIDER_URL=https://keycloak.example.com/realms/your-realm
OIDC_CLIENT_ID=pedshub
OIDC_CLIENT_SECRET=your-client-secret
OIDC_PROVIDER_NAME=Keycloak

Auth0

  1. Go to Auth0 Dashboard > Applications
  2. Create a Regular Web Application
  3. In Settings, add Allowed Callback URL: https://pedshub.com/api/auth/sso/callback
  4. Note the Domain, Client ID, and Client Secret
OIDC_PROVIDER_URL=https://your-tenant.auth0.com
OIDC_CLIENT_ID=your-client-id
OIDC_CLIENT_SECRET=your-client-secret
OIDC_PROVIDER_NAME=Auth0

Authentik

  1. In Authentik admin, go to Applications > Providers > Create OAuth2/OpenID Provider
  2. Redirect URI: https://pedshub.com/api/auth/sso/callback
  3. Create an Application linked to this provider
OIDC_PROVIDER_URL=https://auth.example.com/application/o/pedshub
OIDC_CLIENT_ID=your-client-id
OIDC_CLIENT_SECRET=your-client-secret
OIDC_PROVIDER_NAME=Authentik

Troubleshooting SSO

"SSO login failed" after redirect:

  • Check that APP_URL in .env matches your actual domain (with https)
  • Verify the redirect URI in your provider matches exactly: {APP_URL}/api/auth/sso/callback
  • Check backend logs: docker compose logs backend --tail=30 | grep -i "sso\|oidc\|oauth"

User created but wrong name:

  • The app reads name from the OIDC token, falling back to preferred_username, then the email prefix
  • Some providers require the profile scope to include the name — make sure OIDC_SCOPES=openid email profile

Existing user can't SSO:

  • If a user registered with email/password and later tries SSO with the same email, it works — SSO login finds the existing account by email and logs them in (doesn't create a duplicate)

SSO-only mode lockout:

  • If SSO breaks while in SSO-only mode, use the CLI to disable it:
    docker compose exec backend python3 -c "
    import redis; r = redis.from_url('redis://redis:6379/0', decode_responses=True)
    r.set('settings:sso_only', 'false')
    print('SSO-only mode disabled')
    "
    

Security Notes

  • Passwords are hashed with bcrypt (one-way, irreversible)
  • JWT tokens auto-refresh via sliding expiration (12h age or <1h remaining)
  • Rate limiting on login (10 attempts per IP per 15 min) via Redis
  • Email verification required for new accounts (SSO users are auto-verified)
  • Quiz reminders skip deleted quizzes
  • Users can opt out of reminders in Settings > Notifications