# PedsHub — Admin Guide ## Deployment ```bash # 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: ```bash docker compose exec backend python -m app.cli ``` ### User Management ```bash # 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 ```bash # 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. ### Courses & Quizzes ```bash # List all courses docker compose exec backend python -m app.cli list-courses # 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 ```bash # 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 ```bash 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/`. ```bash # 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` | Take quizzes, study flashcards, enroll in courses, create quizzes from question bank | | `moderator` | All user abilities + create courses, upload PDFs, manage documents | | `admin` | All moderator abilities + admin dashboard, user management, model config, system settings | ## Common Issues ### Document stuck in "processing" ```bash 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: ```bash docker compose logs backend --tail=10 | grep "startup complete" ``` ### Celery task not running ```bash # 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 ```bash docker compose exec backend python -m app.cli reset-password user@example.com newpassword ``` ### User can't log in (unverified email) ```bash 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 | | `TURNSTILE_SITE_KEY` / `TURNSTILE_SECRET_KEY` | Cloudflare Turnstile 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](https://console.cloud.google.com/apis/credentials) 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](https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade) 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](https://manage.auth0.com/#/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: ```bash 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 and course quizzes - Users can opt out of reminders in Settings > Notifications