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
340 lines
12 KiB
Markdown
340 lines
12 KiB
Markdown
# 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 <command>
|
|
```
|
|
|
|
### 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.
|
|
|
|
### Quizzes
|
|
|
|
```bash
|
|
# 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` | 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"
|
|
```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 |
|
|
| `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](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
|
|
- Users can opt out of reminders in Settings > Notifications
|