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
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
- User clicks "Sign in with SSO" on the login page
- Browser redirects to your identity provider (Google, Microsoft, etc.)
- After authentication, provider redirects back to
{APP_URL}/api/auth/sso/callback - PedsHub reads the
emailandnameclaims from the OIDC token - If no account exists, one is created automatically (role:
user, email verified) - 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.
- Go to Google Cloud Console
- Create a new project (or select existing)
- Go to APIs & Services > Credentials > Create Credentials > OAuth client ID
- Application type: Web application
- Authorized redirect URIs:
https://pedshub.com/api/auth/sso/callback - 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
- Go to Azure Portal > App registrations
- Click New registration
- Name: "PedsHub", Redirect URI:
https://pedshub.com/api/auth/sso/callback(type: Web) - Note the Application (client) ID and Directory (tenant) ID
- 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,profileare granted.
Keycloak
- In your Keycloak admin, go to Clients > Create client
- Client ID:
pedshub, Client type: OpenID Connect - Valid redirect URIs:
https://pedshub.com/api/auth/sso/callback - 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
- Go to Auth0 Dashboard > Applications
- Create a Regular Web Application
- In Settings, add Allowed Callback URL:
https://pedshub.com/api/auth/sso/callback - 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
- In Authentik admin, go to Applications > Providers > Create OAuth2/OpenID Provider
- Redirect URI:
https://pedshub.com/api/auth/sso/callback - 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_URLin.envmatches 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
namefrom the OIDC token, falling back topreferred_username, then the email prefix - Some providers require the
profilescope to include the name — make sureOIDC_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