Security hardening: go-live review fixes
- TV tokens upgraded from 4 to 6 digits; Regen Token button in Admin - Nginx rate limiting on TV dashboard and WebSocket endpoints - Login lockout after 5 failed attempts (15 min); clears on admin password reset - HSTS header added; CSP unsafe-inline removed from script-src; CORS restricted to explicit methods/headers - Dependency CVE fixes: PyJWT 2.12.0, aiomysql 0.3.0, cryptography 46.0.5, python-multipart 0.0.22 - datetime.utcnow() replaced with datetime.now(timezone.utc) throughout - SQL identifier whitelist for startup migration queries - README updated: security notes section, lockout docs, token regen, NPM proxy guidance Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
64
README.md
64
README.md
@@ -24,10 +24,10 @@ A self-hosted web app for managing homeschool schedules, tracking daily learning
|
||||
- **Timezone Support** — Set your local timezone in Admin → Settings. All activity log timestamps display in your timezone, including the TV dashboard clock. Midnight strike resets use this timezone.
|
||||
- **Password Change** — Users can change their own account password from Admin → Settings → Reset Password. The form requires the current password before accepting a new one.
|
||||
- **Multi-Child Support** — Manage multiple students under one parent account, each with their own color, schedule, and history.
|
||||
- **JWT Authentication** — Secure parent login with access tokens and httpOnly refresh cookies. TV dashboard is public (no login required). Disabled accounts receive a clear error message explaining the account is disabled rather than a generic "invalid credentials" response.
|
||||
- **JWT Authentication** — Secure parent login with access tokens and httpOnly refresh cookies. TV dashboard is public (no login required). Disabled accounts receive a clear error message explaining the account is disabled rather than a generic "invalid credentials" response. After **5 consecutive failed login attempts**, an account is locked for **15 minutes** — the error message includes the remaining wait time. Locks clear automatically after the cooldown, or immediately when a super admin resets the account's password.
|
||||
- **Super Admin Panel** — A separate admin interface (at `/super-admin`) for site-wide management. Log in with a dedicated admin username and password (set in `.env`). Provides full control over all registered parent accounts:
|
||||
- **Impersonate** — Enter any user's session to view and manage their data. An impersonation banner is shown at the top of every page with a one-click "Exit to Admin Panel" button.
|
||||
- **Reset Password** — Set a new password for any user without needing the current password.
|
||||
- **Reset Password** — Set a new password for any user without needing the current password. Also clears any active login lockout on the account.
|
||||
- **Disable / Enable** — Disable a user's login access. Disabled users cannot log in and see a specific error message. Re-enable at any time.
|
||||
- **Delete** — Permanently delete a user and all associated data (children, sessions, schedules, subjects, activity logs, etc.) with a confirmation dialog. This action cannot be undone.
|
||||
- **Last Active** — Shows the date each user last logged in or refreshed their session, displayed in that user's configured timezone. Shows "Never logged in" for accounts that have never authenticated.
|
||||
@@ -50,7 +50,7 @@ A self-hosted web app for managing homeschool schedules, tracking daily learning
|
||||
| Real-time | WebSockets via FastAPI |
|
||||
| Database | MySQL 8 |
|
||||
| ORM | SQLAlchemy 2.0 (async) |
|
||||
| Auth | JWT — python-jose + passlib/bcrypt |
|
||||
| Auth | JWT — PyJWT + passlib/bcrypt |
|
||||
| Orchestration | Docker Compose |
|
||||
|
||||
---
|
||||
@@ -82,11 +82,11 @@ homeschool/
|
||||
│ │ ├── rule.py # RuleItem (rules & expectations)
|
||||
│ │ ├── session_block_agenda.py # SessionBlockAgenda (per-session block overrides)
|
||||
│ │ ├── strike.py # StrikeEvent (strike history)
|
||||
│ │ └── user.py # User (incl. timezone, last_active_at)
|
||||
│ │ └── user.py # User (incl. timezone, last_active_at, failed_login_attempts, locked_until)
|
||||
│ ├── schemas/ # Pydantic request/response schemas
|
||||
│ ├── routers/ # API route handlers
|
||||
│ │ ├── auth.py # Login, register, refresh, logout, change-password
|
||||
│ │ ├── children.py # Children CRUD + strikes + midnight reset
|
||||
│ │ ├── children.py # Children CRUD + strikes + midnight reset + TV token regeneration
|
||||
│ │ ├── subjects.py
|
||||
│ │ ├── schedules.py
|
||||
│ │ ├── sessions.py # Timer actions + break timer events + block agenda upsert
|
||||
@@ -105,7 +105,7 @@ homeschool/
|
||||
│
|
||||
└── frontend/
|
||||
├── Dockerfile # Multi-stage: Node build → nginx serve
|
||||
├── nginx.conf # Proxy /api/ and /ws/ to backend
|
||||
├── nginx.conf # Proxy /api/ and /ws/ to backend; rate limiting, security headers
|
||||
├── index.html # Favicon set to house emoji via inline SVG
|
||||
└── src/
|
||||
├── composables/
|
||||
@@ -192,7 +192,7 @@ Open **http://localhost:8054/login** and register. This creates your admin accou
|
||||
6. **Admin** → Scroll to **Settings** (below Schedules) and select your local timezone. You can also change your account password here.
|
||||
7. **Admin** → Scroll to **Schedules** → Create a schedule template, add time blocks assigned to subjects. For any block that should include a break, check **Break** and enter the break duration in minutes.
|
||||
8. **Dashboard** (`/dashboard`) → Click "Start Day", choose a template
|
||||
9. **TV** → From the Dashboard, click **Open TV View** to get the TV URL for that child. Each child is assigned a permanent random 4-digit token (e.g. `http://your-lan-ip:8054/tv/4823`). Open that URL on the living room TV.
|
||||
9. **TV** → From the Dashboard, click **Open TV View** to get the TV URL for that child. Each child is assigned a permanent random 6-digit token (e.g. `http://your-lan-ip:8054/tv/482391`). Open that URL on the living room TV.
|
||||
|
||||
---
|
||||
|
||||
@@ -204,7 +204,7 @@ Open **http://localhost:8054/login** and register. This creates your admin accou
|
||||
|-----|-------------|
|
||||
| `/dashboard` | Overview, start/stop sessions, select and time blocks, set per-block agendas, issue behavior strikes, trigger TV overlays |
|
||||
| `/logs` | Browse timer and strike event history and manual notes; filter by child and date |
|
||||
| `/admin` | Manage children, subjects (with activity options), morning routine, break activities, rules & expectations, schedule templates, and account settings (timezone, password). Includes a Buy Me a Coffee support link at the top of the page. |
|
||||
| `/admin` | Manage children (incl. TV token display and regeneration), subjects (with activity options), morning routine, break activities, rules & expectations, schedule templates, and account settings (timezone, password). Includes a Buy Me a Coffee support link at the top of the page. |
|
||||
|
||||
### Super Admin Views
|
||||
|
||||
@@ -249,7 +249,7 @@ Pressing **Reset** after **Done** fully un-marks the block — removes the check
|
||||
|-----|-------------|
|
||||
| `/tv/:tvToken` | Full-screen display — greeting + morning routine, current block timer with subject activities, break timer with break activities, day progress bar, schedule sidebar, meeting warning toasts, meeting start overlay, rules/expectations overlay |
|
||||
|
||||
Each child is assigned a permanent random 4-digit token when created (e.g. `/tv/4823`). The token never changes and does not expose the internal database ID. Find the TV URL for a child by clicking **Open TV View** on the Dashboard. The page connects via WebSocket and updates automatically when a parent starts/stops/advances the timer from the Dashboard.
|
||||
Each child is assigned a permanent random 6-digit token when created (e.g. `/tv/482391`). The token does not expose the internal database ID. Find the TV URL by clicking **Open TV View** on the Dashboard, or view the token directly in **Admin → Children** next to each child's name. To generate a new token (e.g. if the old URL needs to be invalidated), click **Regen Token** in the child's row — the old URL stops working immediately. The page connects via WebSocket and updates automatically when a parent starts/stops/advances the timer from the Dashboard.
|
||||
|
||||
### API Documentation
|
||||
|
||||
@@ -271,11 +271,15 @@ docker compose up -d
|
||||
|
||||
No separate migration tool or manual steps are required.
|
||||
|
||||
### Upgrading from an older version (4-digit TV tokens)
|
||||
|
||||
TV tokens were changed from 4 digits to 6 digits to improve security. The startup migration does **not** automatically regenerate tokens for existing children — they will keep their old 4-digit token until you manually regenerate it. To do so, go to **Admin → Children**, find the child, and click **Regen Token**. The new 6-digit token is shown immediately in the row. Update the TV URL on your TV — the old URL stops working as soon as the token is regenerated.
|
||||
|
||||
---
|
||||
|
||||
## WebSocket Events
|
||||
|
||||
The TV dashboard connects to `ws://host/ws/{tv_token}` (using the child's 4-digit TV token, not the internal database ID) and receives JSON events:
|
||||
The TV dashboard connects to `ws://host/ws/{tv_token}` (using the child's 6-digit TV token, not the internal database ID) and receives JSON events:
|
||||
|
||||
| Event | Triggered by | Key payload fields |
|
||||
|-------|-------------|---------|
|
||||
@@ -347,6 +351,46 @@ The TV dashboard connects to `ws://host/ws/{tv_token}` (using the child's 4-digi
|
||||
|
||||
---
|
||||
|
||||
## Security Notes
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
Nginx enforces the following rate limits (per IP):
|
||||
|
||||
| Endpoint | Limit | Burst |
|
||||
|----------|-------|-------|
|
||||
| `/api/auth/login`, `/api/auth/register` | 5 req/min | 3 |
|
||||
| `/api/admin/login` | 5 req/min | 3 |
|
||||
| `/api/dashboard/*` (TV token lookup) | 10 req/min | 5 |
|
||||
| `/ws/*` (WebSocket handshake) | 10 req/min | 5 |
|
||||
|
||||
Requests over the limit receive a `429 Too Many Requests` response.
|
||||
|
||||
### Login Lockout
|
||||
|
||||
After 5 consecutive failed login attempts, a parent account is locked for 15 minutes. The lock clears automatically after the cooldown, or immediately when a super admin resets the user's password. The lockout threshold and duration are configured in `backend/app/routers/auth.py` (`_LOGIN_MAX_ATTEMPTS`, `_LOGIN_LOCKOUT_MINUTES`).
|
||||
|
||||
### Exposing via Reverse Proxy (Nginx Proxy Manager)
|
||||
|
||||
If exposing the app externally via NPM:
|
||||
|
||||
- Add your public domain to `CORS_ORIGINS` in `.env` and rebuild
|
||||
- Restrict `/super-admin` and `/api/admin` paths to trusted IPs via NPM's Advanced tab:
|
||||
|
||||
```nginx
|
||||
location ~* ^/(super-admin|api/admin) {
|
||||
allow 192.168.1.0/24;
|
||||
deny all;
|
||||
proxy_pass http://<upstream>:8054;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stopping and Restarting
|
||||
|
||||
```bash
|
||||
|
||||
Reference in New Issue
Block a user