142 lines
5.5 KiB
Markdown
142 lines
5.5 KiB
Markdown
# Secure Student Invite Flow
|
|||
|
|
|
||
|
|
## Purpose
|
||
|
|
Document the current secure student invite architecture, what is implemented right now, and what should be implemented next to make the invite system excellent for a modern course platform.
|
||
|
|
|
||
|
|
## Scope
|
||
|
|
- Student invitations into a course
|
||
|
|
- Security controls around link generation and acceptance
|
||
|
|
- Teacher/admin management surface for invite links
|
||
|
|
|
||
|
|
## Current Flow (Implemented)
|
||
|
|
|
||
|
|
### 0. Public landing page enroll button (free courses)
|
||
|
|
1. User clicks Enroll on a free course landing page.
|
||
|
|
2. Frontend calls `POST /invite/student/public-link` with `courseId`.
|
||
|
|
3. Backend validates course is active, published, and accepting students.
|
||
|
|
4. Backend creates a one-time secure invite token (24h expiry).
|
||
|
|
5. Frontend redirects user to `/invite/s/:token`.
|
||
|
|
|
||
|
|
### 1. Teacher creates invite link
|
||
|
|
1. Teacher opens the invite modal in the course people page.
|
||
|
|
2. Frontend calls `POST /course/:courseId/invites`.
|
||
|
|
3. Backend creates a cryptographically random token.
|
||
|
|
4. Backend stores only `sha256(token)` in `course_invite.token_hash`.
|
||
|
|
5. Backend returns a single usable invite URL: `/invite/s/:token`.
|
||
|
|
|
||
|
|
### 2. Student opens invite link
|
||
|
|
1. Student hits invite page with token in URL.
|
||
|
|
2. Frontend calls `GET /invite/student/:token/preview`.
|
||
|
|
3. Backend hashes token and looks up invite by `token_hash`.
|
||
|
|
4. Backend returns invite + course + organization preview data and computed status.
|
||
|
|
|
||
|
|
### 3. Student accepts invite
|
||
|
|
1. Student clicks Join.
|
||
|
|
2. Frontend calls `POST /invite/student/:token/accept` (authenticated route).
|
||
|
|
3. Backend performs checks in a transaction:
|
||
|
|
- invite exists
|
||
|
|
- invite role is student
|
||
|
|
- invite is active (not expired/revoked/used up)
|
||
|
|
- course is active and published
|
||
|
|
- course allows new students
|
||
|
|
- optional email/domain restrictions pass
|
||
|
|
- if restricted invite, verified email is required
|
||
|
|
4. Backend idempotently handles already-joined students.
|
||
|
|
5. Backend inserts missing org membership + group membership.
|
||
|
|
6. Backend atomically increments invite usage with race-safe condition.
|
||
|
|
7. Backend sends welcome email to student and notification emails to teachers.
|
||
|
|
|
||
|
|
### 4. Teacher can revoke
|
||
|
|
1. Teacher calls `POST /course/:courseId/invites/:inviteId/revoke`.
|
||
|
|
2. Invite is marked revoked and immediately blocked from future joins.
|
||
|
|
|
||
|
|
## Functionalities Implemented
|
||
|
|
|
||
|
|
### Database
|
||
|
|
- New `course_invite` table in `packages/db/src/schema.ts`.
|
||
|
|
- Fields include:
|
||
|
|
- `tokenHash`, `expiresAt`, `maxUses`, `usedCount`, `isRevoked`
|
||
|
|
- `allowedEmails`, `allowedDomains`, `metadata`
|
||
|
|
- `createdByProfileId`, `lastUsedAt`, timestamps
|
||
|
|
- Indexes and uniqueness on token hash.
|
||
|
|
|
||
|
|
### API and Service Layer
|
||
|
|
- Course-level invite management routes:
|
||
|
|
- `GET /course/:courseId/invites`
|
||
|
|
- `POST /course/:courseId/invites`
|
||
|
|
- `POST /course/:courseId/invites/:inviteId/revoke`
|
||
|
|
- Public/auth invite routes:
|
||
|
|
- `POST /invite/student/public-link`
|
||
|
|
- `GET /invite/student/:token/preview`
|
||
|
|
- `POST /invite/student/:token/accept`
|
||
|
|
- Invite validation schemas added for params and payloads.
|
||
|
|
- Legacy invite fallback removed; only secure tokenized invites are supported (including landing-page enroll flow).
|
||
|
|
|
||
|
|
### Security Controls
|
||
|
|
- High-entropy token generation (`crypto.randomBytes`).
|
||
|
|
- Token hashing at rest (no raw token stored in DB).
|
||
|
|
- Expiry, max-use, and revocation enforcement.
|
||
|
|
- Public free-course enroll flow issues one-time, short-lived secure links instead of payload-derived links.
|
||
|
|
- Optional allowlist restrictions (emails/domains).
|
||
|
|
- Verified-email enforcement for restricted invites.
|
||
|
|
- Transactional acceptance with optimistic concurrency guard on `usedCount`.
|
||
|
|
- Idempotent join behavior to avoid duplicate consumption.
|
||
|
|
|
||
|
|
### Dashboard / UX
|
||
|
|
- Invite modal now requests server-generated secure links.
|
||
|
|
- Copy-link and QR features use secure link output.
|
||
|
|
- Invite join page uses backend preview + accept endpoints.
|
||
|
|
- Invite join page shows clear blocked states for expired/revoked/used-up/unavailable course.
|
||
|
|
|
||
|
|
## What I Recommend Next (Priority Order)
|
||
|
|
|
||
|
|
### P0: Must-have next
|
||
|
|
1. Invite management UI in course people page:
|
||
|
|
- list invites
|
||
|
|
- show status and uses left
|
||
|
|
- revoke from UI
|
||
|
|
- copy existing invite link metadata (without exposing raw token again)
|
||
|
|
2. Per-invite presets:
|
||
|
|
- one-time link
|
||
|
|
- multi-use link
|
||
|
|
- short expiry presets (24h, 7d, 30d)
|
||
|
|
3. Full audit trail:
|
||
|
|
- who created/revoked invite
|
||
|
|
- who joined with invite
|
||
|
|
- timestamped acceptance history
|
||
|
|
|
||
|
|
### P1: High impact
|
||
|
|
1. Direct email invites (personalized):
|
||
|
|
- issue invite per recipient
|
||
|
|
- send one-click email invites
|
||
|
|
- track delivery/open/accept
|
||
|
|
2. Bulk invite import:
|
||
|
|
- CSV of emails
|
||
|
|
- domain validation and duplicate detection
|
||
|
|
3. Abuse protection:
|
||
|
|
- rate limits on preview/accept
|
||
|
|
- IP/device anomaly detection
|
||
|
|
|
||
|
|
### P2: Advanced platform features
|
||
|
|
1. SSO/domain auto-enrollment rules:
|
||
|
|
- trusted domains auto-map to org/course policies
|
||
|
|
2. Invite analytics dashboard:
|
||
|
|
- conversion funnel (created -> opened -> accepted)
|
||
|
|
- per-course acceptance rates
|
||
|
|
3. Expiry automation:
|
||
|
|
- automatic cleanup/archive
|
||
|
|
- reminders before expiration
|
||
|
|
|
||
|
|
## Suggested Product Standard for an Outstanding Course Platform
|
||
|
|
- Secure by default (hashed tokens, expiry, revocation, least privilege).
|
||
|
|
- Clear invite lifecycle (active, expired, used up, revoked) visible to staff.
|
||
|
|
- Excellent failure UX for students (explicit reason + next action).
|
||
|
|
- Strong governance (audit logs and admin controls).
|
||
|
|
- Scalable operations (bulk invite, email campaigns, analytics).
|
||
|
|
|
||
|
|
## Validation Status
|
||
|
|
- `pnpm --filter @cio/db build` passed.
|
||
|
|
- `pnpm --filter @cio/utils build` passed.
|
||
|
|
- `pnpm --filter @cio/api build` passed.
|
||
|
|
- `pnpm --filter @cio/dashboard build` passed (with unrelated pre-existing Svelte warnings).
|