diff --git a/FEATURES.md b/FEATURES.md new file mode 100644 index 0000000..78ce1f0 --- /dev/null +++ b/FEATURES.md @@ -0,0 +1,289 @@ +# AskEasy — Feature List + +A comprehensive list of every feature in the AskEasy platform. + +--- + +## Authentication & Authorization + +### Authentication +- **Shibboleth SSO** — Production login via UofT's SAML identity provider (reads `utorid`, `displayname`, `email` headers from Apache mod_shib) +- **Dev login** — Local development uses `DEV_UTORID`, `DEV_NAME`, `DEV_EMAIL` environment variables +- **Session cookies** — iron-session sealed httpOnly cookies +- **Open redirect protection** — Post-login redirects restricted to same-origin relative paths + +### Role System +- **Two-tier roles**: + - **Global role** — determined from `whitelist.txt` on every login (PROFESSOR or STUDENT) + - **Per-course role** — stored in `CourseEnrollment` (PROFESSOR, TA, or STUDENT) +- **Whitelist** — plain text file of UTORids; case-insensitive; supports legacy `utorid,PROFESSOR` format +- **Effective permissions** — course/session actions use the per-course enrollment role, not the global role + +### Endpoints +| Endpoint | Description | +|----------|-------------| +| `GET /api/auth/session` | Establishes session from Shibboleth/dev headers | +| `GET /api/auth/me` | Returns current user info (userId, utorid, name, email, role) | +| `POST /api/auth/logout` | Destroys session cookie | + +--- + +## Course Management + +### Creation +- Professors create courses with a course code, name, and optional section +- **Semester auto-detection** from current date (Jan–Apr = Winter, May–Aug = Summer, Sep–Dec = Fall) +- **CSV enrollment** — upload a CSV with columns: `utorid`, `givenName`, `surname`, `Email` (optional); rows with "Missing UTORid" or "ERROR" are skipped +- **TA assignment** — professors can designate TAs during course creation + +### Operations +- **Rename** — professor can update course code and/or semester +- **Delete** — cascading deletion (questions, answers, upvotes, slide sets, sessions, enrollments); blocked if an ACTIVE session exists + +### Student & TA Management +- **View roster** — returns students and TAs with name and UTORid +- **Add individuals** — add one or more UTORids; returns added, already-enrolled, and invalid lists +- **Batch sync** — full replace of the STUDENT roster from a new CSV; preserves TAs and professor +- **Remove** — remove a single student by UTORid +- **Auto-creation** — users not yet in the database are created automatically on enrollment +- **CSV diff preview** — before applying a sync, shows counts of students to add, remove, and unchanged + +--- + +## Session Management + +### Lifecycle +- **Statuses**: ACTIVE, ENDED +- **Creation** — professor creates a session with a title (3–100 characters); starts as ACTIVE immediately +- **Manual end** — professor ends the session; broadcasts `session:ended` to all connected clients; cleans up Q&A data and slide files +- **Auto-end** — sessions with no question activity for 2 hours are automatically ended + +### Join Codes +- **Format** — 6-character uppercase alphanumeric code +- **Case-insensitive lookup** +- **Regeneration** — professor can regenerate the code (rate limit: 5 per hour) +- **Join flow** — students enter the code to join; auto-enrolled in the course if not already a member +- **Ended sessions** — attempting to join returns 410 Gone + +### Activity Tracking +- `lastActivityAt` updated on every question creation +- Used by the auto-end check (cutoff = 2 hours of inactivity) + +### Cron Cleanup +- `GET /api/cron/cleanup-sessions` — secured by `CRON_SECRET` bearer token +- Finds and ends all stale ACTIVE sessions in parallel +- Returns `{ended: N, failed: M}` + +--- + +## Live Q&A Room + +### Questions +- **Create** — 5–500 characters; optional anonymous flag and visibility setting +- **Visibility** — PUBLIC (everyone) or INSTRUCTOR_ONLY (TAs and professors only) +- **Upvote** — toggle per user; updates count in real time +- **Resolve** — marks question as RESOLVED; students can resolve their own, TAs/professors can resolve any +- **Unresolve** — TAs/professors can reopen a resolved question +- **Delete** — professors can delete any question; TAs can delete student questions; students cannot delete +- **Filtering** — by status: All, Unresolved, Resolved +- **Search** — case-insensitive substring match on question content +- **Sorting** — newest first (default) or by vote count +- **Pagination** — cursor-based, 20 per page (max 50) + +### Answers +- **Create** — 1–1,000 characters; optional anonymous flag +- **Upvote** — toggle per user; updates count in real time +- **Delete** — same permission rules as questions +- **Accepted answers** — `isAccepted` field; accepted answers sort first and display a checkmark icon +- **Thread states** — default (shows accepted/best answers), expanded (all replies), collapsed (hidden) + +### Anonymous Posting +- Questions and answers can be posted anonymously +- **Students** see "Anonymous" as the author +- **TAs and professors** receive a separate `author:revealed` event showing the real identity and role + +### Answer Mode Restriction +- Professor can toggle between "all" (everyone can answer) and "instructors_only" (only TAs/professors can answer) +- **Exception**: the question author can always answer their own question regardless of mode +- **Default**: instructors only +- Stored in Redis with 24-hour TTL; late joiners sync on connect + +--- + +## Slide Viewer + +### Upload +- **PDF only** — validated by MIME type, magic bytes, and parseability +- **Size limits** — 1 KB to 50 MB +- Professor-only; session must be ACTIVE + +### Viewing +- Served inline with `Content-Disposition: inline` and 1-hour cache +- Auth-gated: must be enrolled in the session's course + +### Real-Time Sync +- Professor changes the page index; broadcast to all participants via `slide:changed` +- Late joiners call `slide:sync` to get the current page +- New upload triggers `slides:available` notification to the room + +### Split View +- Resizable panel layout — Q&A chat and slide viewer side by side +- Panels adapt based on screen size (mobile detection via `useMediaQuery`) + +--- + +## Chat History Export + +- **Format** — plain text (`.txt`, UTF-8) +- **Content includes**: + - Session title header with date/time + - Each question with timestamp, author (name + UTORid or "Anonymous"), and content + - Each answer indented under its question with the same format + - Separator lines between questions +- **Filename** — `{sessionTitle}_chat.txt` (special characters replaced with underscores) +- **Trigger** — modal appears when professor ends the session, offering: + - "Download chat history & end" + - "End without downloading" + +--- + +## UI / UX + +- **Resizable split view** — drag to resize Q&A panel vs. slide panel +- **Responsive layout** — adapts to mobile via media query detection +- **Keyboard shortcut** — Ctrl+Enter submits a question or answer +- **Filter tabs** — All / Unresolved / Resolved +- **Live search** — filter questions by text with a clear button +- **Thread collapse/expand** — chevron toggle per question thread +- **Loading skeletons** — placeholder UI while data loads +- **Toast notifications** — success/error feedback (e.g., "Course updated successfully") +- **Undo** — unresolve action available as an undo button on resolved questions + +--- + +## Rate Limits + +All rate limits are per-user, enforced via Redis counters. + +| Action | Limit | Window | +|--------|-------|--------| +| Question creation | 10 | 60 s | +| Question upvote | 30 | 60 s | +| Question resolve/unresolve | 20 | 60 s | +| Answer creation | 15 | 60 s | +| Answer upvote | 30 | 60 s | +| Join code lookup | 30 | 60 s | +| Join code registration | 10 | 60 s | +| Join code regeneration | 5 | 1 hour | + +If Redis is unavailable, rate limiting fails closed (blocks all requests). + +--- + +## Permissions Matrix + +### Course Operations + +| Action | Student | TA | Professor | +|--------|:-------:|:--:|:---------:| +| Create course | | | Yes | +| View own courses | Yes | Yes | Yes | +| Rename course | | | Yes (owner) | +| Delete course | | | Yes (owner) | +| View roster | | | Yes (owner) | +| Add/remove students | | | Yes (owner) | +| Sync CSV roster | | | Yes (owner) | + +### Session Operations + +| Action | Student | TA | Professor | +|--------|:-------:|:--:|:---------:| +| Create session | | | Yes | +| Join via code | Yes | Yes | N/A | +| End session | | | Yes (creator) | +| Regenerate join code | | | Yes (creator) | +| Upload slides | | | Yes | +| Control slide page | | | Yes | + +### Question Operations + +| Action | Student | TA | Professor | +|--------|:-------:|:--:|:---------:| +| Ask question | Yes | Yes | Yes | +| Upvote | Yes | Yes | Yes | +| Resolve own | Yes | Yes | Yes | +| Resolve others' | | Yes | Yes | +| Unresolve | | Yes | Yes | +| Delete (student Qs) | | Yes | Yes | +| Delete (TA Qs) | | | Yes | +| See INSTRUCTOR_ONLY | | Yes | Yes | + +### Answer Operations + +| Action | Student | TA | Professor | +|--------|:-------:|:--:|:---------:| +| Answer (open mode) | Yes | Yes | Yes | +| Answer (restricted mode) | Own Q only | Yes | Yes | +| Upvote | Yes | Yes | Yes | +| Delete (student As) | | Yes | Yes | +| Delete (TA As) | | | Yes | + +--- + +## Content Constraints + +| Item | Min | Max | +|------|-----|-----| +| Question | 5 chars | 500 chars | +| Answer | 1 char | 1,000 chars | +| Session title | 3 chars | 100 chars | +| Slide file | 1 KB | 50 MB | + +--- + +## Socket.IO Events + +### Client → Server + +| Event | Payload | +|-------|---------| +| `question:create` | `{content, sessionId, visibility?, isAnonymous?}` | +| `question:upvote` | `{questionId}` | +| `question:resolve` | `{questionId}` | +| `question:unresolve` | `{questionId}` | +| `question:delete` | `{questionId, sessionId}` | +| `answer:create` | `{questionId, content, isAnonymous?}` | +| `answer:upvote` | `{answerId}` | +| `answer:delete` | `{answerId, sessionId}` | +| `answer-mode:change` | `{sessionId, mode}` | +| `answer-mode:sync` | `{sessionId}` | +| `slide:change` | `{sessionId, pageIndex}` | +| `slides:uploaded` | `{sessionId, slideSetId}` | +| `slide:sync` | `{sessionId}` | + +### Server → Client + +| Event | Description | +|-------|-------------| +| `question:created` | New question (author redacted if anonymous) | +| `question:updated` | Upvote count changed | +| `question:resolved` | Status → RESOLVED | +| `question:unresolved` | Status → OPEN | +| `question:deleted` | Question removed | +| `question:author:revealed` | Anonymous author disclosed (instructors only) | +| `answer:created` | New answer (author redacted if anonymous) | +| `answer:updated` | Upvote count changed | +| `answer:deleted` | Answer removed | +| `answer:author:revealed` | Anonymous author disclosed (instructors only) | +| `answer-mode:changed` | Answer restriction toggled | +| `slide:changed` | Page index updated | +| `slides:available` | New slide set uploaded | +| `slide:sync` | Current page index (to requesting socket only) | +| `session:ended` | Session has ended | +| `question:error` | Error on question operation | +| `answer:error` | Error on answer operation | +| `slide:error` | Error on slide operation | + +### Room Names +- `session:{sessionId}` — all participants +- `session:{sessionId}:instructors` — TAs and professors only diff --git a/README.md b/README.md index c64dceb..56b2252 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,65 @@ # AskEasy -A real-time classroom Q&A platform that makes it easy for students to ask questions during lectures — and for instructors to keep the conversation organized. Built for the University of Toronto with Shibboleth (UTORid) authentication. +A real-time classroom Q&A platform built for live lectures at the University of Toronto. Students post questions anonymously or publicly, upvote what matters most, and get answers from instructors — all updating instantly during class. Professors see exactly what the room is confused about, right now. -## The Problem +## Why AskEasy? -In large lecture halls, many students hesitate to raise their hand or wait for a microphone. Questions go unasked, concepts stay unclear, and instructors lose visibility into what the class is struggling with. Existing tools like Piazza are designed for asynchronous discussion, not live, in-lecture interaction. +In large lecture halls, most students never raise their hand. Questions go unasked, concepts go unclarified, and instructors are left guessing what landed and what didn't. Tools like Piazza are built for asynchronous discussion — not for the 50 minutes you're actually in the room. -## The Solution +AskEasy is built for that moment. It gives every lecture a live Q&A room where the most important questions surface automatically through upvoting, anonymous posting removes the social barrier to asking, and professors can present slides side-by-side with the chat without switching windows. -AskEasy gives every lecture a live Q&A room where students can post questions (anonymously if they prefer), upvote the most pressing ones, and get answers from instructors or peers — all in real time via WebSockets. Professors and TAs see what the class needs help with *right now*, and can present slides side-by-side with the chat. +--- -## Features +## Architecture -- **Real-time Q&A** — Questions, answers, and upvotes update instantly via Socket.IO with Redis pub/sub -- **Role-based access** — Professors, TAs, and Students each see what's relevant to them; TAs are assigned per-course -- **Anonymous posting** — Students can ask questions and post answers anonymously -- **Upvoting** — The most important questions and best answers rise to the top -- **Slide viewer** — Professors can upload PDFs and present slides alongside the live chat in a resizable split view -- **Join codes** — Students join a session with a short code, no enrollment setup needed -- **Question filtering** — Filter by status (open, answered, resolved) and visibility (public, instructor-only) -- **Chat history export** — Professors can download the full Q&A transcript as a `.txt` file when ending a session -- **Course management** — Create courses, manage enrollments, start/schedule/end sessions -- **Shibboleth SSO** — Authenticates via UofT's identity provider; instructor whitelist controls professor access +``` +┌─────────────────────────────────────────────────────────────┐ +│ Production │ +│ │ +│ Browser ──HTTPS──▶ Apache + mod_shib ──localhost──▶ App │ +│ │ │ +│ ▼ │ +│ U of T IdP (SAML) │ +└─────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────┐ +│ Application Layer │ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ Node.js Custom Server (server.ts) │ │ +│ │ │ │ +│ │ ┌─────────────────┐ ┌────────────────────────┐ │ │ +│ │ │ Next.js App │ │ Socket.IO Server │ │ │ +│ │ │ (App Router) │ │ (real-time events) │ │ │ +│ │ │ - Pages │ │ - questions │ │ │ +│ │ │ - API routes │ │ - answers/upvotes │ │ │ +│ │ │ - Auth │ │ - slide sync │ │ │ +│ │ └────────┬────────┘ └──────────┬─────────────┘ │ │ +│ └────────────┼──────────────────────── ┼───────────────┘ │ +└────────────────┼─────────────────────────┼──────────────────┘ + │ │ + ┌────────▼────────┐ ┌──────────▼──────────┐ + │ PostgreSQL 16 │ │ Redis 7 │ + │ (via Prisma) │ │ - Socket.IO pub/sub │ + │ │ │ - Rate limiting │ + │ Users, Courses │ │ - Answer mode TTL │ + │ Sessions, Q&A │ │ - Session data │ + │ Upvotes, Slides │ └─────────────────────┘ + └──────────────────┘ +``` + +### How the pieces connect + +| Component | Role | +|-----------|------| +| **Custom server (`server.ts`)** | Single Node.js process that boots both Next.js and Socket.IO on the same port. Strips Shibboleth headers from non-localhost connections to prevent spoofing. | +| **Next.js App Router** | Serves all pages and REST API routes (`/api/*`). Server Components fetch from PostgreSQL via Prisma; API routes handle auth, course/session management, and slide uploads. | +| **Socket.IO** | Handles all real-time events (questions, answers, upvotes, slide page changes). Uses a Redis adapter so multiple app instances share the same pub/sub channel. | +| **PostgreSQL + Prisma** | Single source of truth for all persistent data. Prisma handles the schema, migrations, and typed queries. | +| **Redis** | Three jobs: Socket.IO pub/sub adapter, rate-limit counters (per-user sliding windows), and ephemeral answer-mode state (24-hour TTL). | +| **Apache + mod_shib** *(prod only)* | Terminates TLS, enforces Shibboleth SSO, and injects `utorid`/`mail`/`cn` headers before proxying to the app. | + +--- ## Tech Stack @@ -36,15 +74,69 @@ AskEasy gives every lecture a live Q&A room where students can post questions (a | Testing | Vitest, Testing Library | | Containerization | Docker & Docker Compose | -## Prerequisites +--- + +## Environment Variables + +### `.env` — used by Docker Compose and production + +```bash +# PostgreSQL +DATABASE_URL=postgresql://postgres:@postgres:5432/ask_easy +POSTGRES_USER=postgres +POSTGRES_PASSWORD= +POSTGRES_DB=ask_easy + +# Redis +REDIS_URL=redis://:@redis:6379 +REDIS_PASSWORD= + +# Session encryption key — generate with: openssl rand -hex 32 +SESSION_SECRET=<64-char-hex> + +# Cron job auth (for /api/cron/cleanup-sessions) +CRON_SECRET= +``` + +### `.env.local` — local dev only (overrides hosts to `localhost`) + +```bash +DATABASE_URL=postgresql://postgres:postgres@localhost:5432/ask_easy +REDIS_URL=redis://:changeme@localhost:6379 + +# Fake SSO identity for local login +DEV_UTORID=yourutorid +DEV_NAME=Your Name +DEV_EMAIL=your.email@mail.utoronto.ca +DEV_ROLE=PROFESSOR # or STUDENT +``` + +> **Note:** In Docker Compose the database and Redis hosts are the service names (`postgres`, `redis`). In `pnpm dev` they must be `localhost` because the app runs outside Docker. + +| Variable | Required | Description | +|----------|:--------:|-------------| +| `DATABASE_URL` | Yes | Prisma connection string | +| `POSTGRES_USER` / `PASSWORD` / `DB` | Yes | Postgres container credentials | +| `REDIS_URL` | Yes | Redis connection (include password if set) | +| `REDIS_PASSWORD` | Yes (Docker) | Passed to the Redis container | +| `SESSION_SECRET` | Yes | 64-char hex key for iron-session cookie encryption | +| `CRON_SECRET` | Prod | Bearer token for the cleanup-sessions cron endpoint | +| `DEV_UTORID` | Dev | Fake UTORid injected when Shibboleth is not present | +| `DEV_NAME` | Dev | Display name for the fake dev user | +| `DEV_EMAIL` | Dev | Email for the fake dev user | +| `DEV_ROLE` | Dev | `PROFESSOR` or `STUDENT` — overrides whitelist lookup | + +--- + +## Running Locally (Development) + +### Prerequisites - [Node.js](https://nodejs.org/) v20+ - [pnpm](https://pnpm.io/) v8+ - [Docker](https://www.docker.com/) and Docker Compose -## Getting Started - -### 1. Clone and install +### 1. Clone and install dependencies ```bash git clone https://github.com/jadenScali/ask_easy.git @@ -52,49 +144,83 @@ cd ask_easy pnpm install ``` -### 2. Configure environment variables +### 2. Configure environment + +Copy the example and edit as needed: ```bash -cp .env.example .env +cp .env .env.local ``` -For native development (`pnpm dev`), also create `.env.local` and point hosts at `localhost`: +Set `DEV_UTORID`, `DEV_NAME`, and `DEV_ROLE` in `.env.local` to control which user you log in as during development. Set `DEV_ROLE=PROFESSOR` to access course management features. + +### 3. Start the database and Redis +```bash +docker-compose up -d postgres redis ``` -DATABASE_URL=postgresql://postgres:postgres@localhost:5432/ask_easy -REDIS_URL=redis://:changeme@localhost:6379 + +### 4. Set up the database schema + +```bash +pnpm db:setup # generates Prisma client and pushes schema +``` + +### 5. Start the dev server + +```bash +pnpm dev +``` + +Open [http://localhost:3000](http://localhost:3000). The app auto-reloads on changes. + +--- + +## Running in Production + +Production uses a pre-built Docker image from Docker Hub layered with the `docker-compose.prod.yml` override. This mounts the auth route, server entry, whitelist, and uploads directory from the host so they can be updated without rebuilding the image. + +### 1. Configure `.env` + +Create `.env` in the project root with production values (see [Environment Variables](#environment-variables) above). Use the Docker service names as hosts: + ``` +DATABASE_URL=postgresql://postgres:@postgres:5432/ask_easy +REDIS_URL=redis://:@redis:6379 +``` + +### 2. Pull and start -| File | Used by | Hosts | -|------|---------|-------| -| `.env` | Docker Compose | `@postgres`, `@redis` (service names) | -| `.env.local` | `pnpm dev` | `@localhost` | +```bash +docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d +``` -### 3. Start services +This starts three containers: `app` (Next.js on port 3000, bound to `127.0.0.1`), `postgres`, and `redis`. The app is not publicly exposed — Apache sits in front of it. -**Option A — Full stack (app + database + Redis):** +### 3. Apply database migrations ```bash -docker-compose up +docker exec ask_easy-app-1 npx prisma migrate deploy ``` -Open [http://localhost:3000](http://localhost:3000). +### 4. Set up Apache + Shibboleth (first time) -**Option B — Database & Redis only (for local dev):** +Speak to UofT IT Admin. + +### Updating the running app ```bash -docker-compose up -d postgres redis -pnpm db:setup # generate Prisma client + push schema -pnpm dev +docker-compose -f docker-compose.yml -f docker-compose.prod.yml pull app +docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d app ``` -> **Note:** `pnpm db:seed` currently resets all tables (deletes all data). There is no sample data seeder yet — create test users by logging in through the app. +--- ## Available Scripts | Script | Description | |--------|-------------| -| `pnpm dev` | Start development server | +| `pnpm dev` | Start development server with hot reload | | `pnpm build` | Build for production | | `pnpm start` | Start production server | | `pnpm lint` | Run ESLint | @@ -102,39 +228,42 @@ pnpm dev | `pnpm test` | Run unit tests (Vitest) | | `pnpm test:integration` | Run integration tests | | `pnpm db:setup` | Generate Prisma client + push schema | -| `pnpm db:seed` | Reset database (clears all tables) | | `pnpm db:migrate` | Run database migrations | | `pnpm db:studio` | Open Prisma Studio GUI | +| `pnpm db:seed` | Reset database (clears all tables — destructive) | + +--- ## Project Structure ``` src/ ├── app/ # Next.js App Router pages & API routes -│ ├── api/ # REST endpoints (auth, courses, sessions, questions) +│ ├── api/ # REST endpoints (auth, courses, sessions, questions, cron) │ ├── classes/ # Course listing & management UI │ ├── create-class/ # Course creation flow -│ └── room/ # Live session room (chat + slide viewer) +│ ├── room/ # Live session room (chat + slide viewer) +│ └── admin/ # Admin dashboard (data overview, table wipe) ├── components/ui/ # Shared UI components (Radix-based) -├── lib/ # Server utilities (auth, caching, validation, Prisma) -├── socket/ # Socket.IO server setup, handlers, and middleware -├── services/ # Business logic services +├── lib/ # Server utilities (auth, caching, validation, Prisma, Redis) +├── socket/ # Socket.IO server setup, event handlers, middleware +├── services/ # Business logic (sessions, questions, answers, slides) └── utils/ # Shared types and helpers prisma/ ├── schema.prisma # Database schema ├── migrations/ # Migration history -└── seed.ts # Development seed data +└── seed.ts # Resets all tables (dev use only) ``` +--- + ## Team Built by the AskEasy team at **GDG on Campus — UTM** (University of Toronto Mississauga). +- Marwan Yousef - Jaden Scali - Phineas Truong - Jack Le - Jad El Asmar -- Manjyot Birdi -- Marwan Yousef - - +- Manjyot Birdi \ No newline at end of file diff --git a/docs/ADMIN-GUIDE.md b/docs/ADMIN-GUIDE.md new file mode 100644 index 0000000..466d5fe --- /dev/null +++ b/docs/ADMIN-GUIDE.md @@ -0,0 +1,143 @@ +# AskEasy — Administrator Guide + +This guide is for professors and system administrators who need to manage the platform — handling course cleanup at the end of a semester, controlling who has professor access, and accessing the database directly if needed. + +--- + +## Role Control Files + +Two plain-text files on the server control who can access what. Both live in the project root directory (`~/AskEasy/`). + +### `whitelist.txt` — who gets the Professor role + +Any UTORid listed here is assigned the **PROFESSOR** role when they log in. Everyone else gets the **STUDENT** role by default. + +``` +# One UTORid per line. Lines starting with # are ignored. +scalijad +phintruong +yousef10 +``` + +When a professor logs in, the app reads this file and sets their role for that session. The role is re-checked on every login, so changes take effect the next time the user logs in. + +**To add a new professor:** + +```bash +ssh @askeasy.utm.utoronto.ca +echo "newutorid" >> ~/AskEasy/whitelist.txt +docker restart ask_easy-app-1 +``` + +> The restart is needed because the app reads the whitelist at startup and caches it in memory. + +**TAs are not listed here.** TAs are assigned per-course by professors through the app UI (see below). A TA has elevated permissions only within the specific course they're assigned to. + +--- + +## Managing Courses Through the App + +Professors manage everything through the **Manage Lecture** modal on the `/classes` page. Click "Manage Lecture" on any course card to open it. It has four tabs: + +### Students tab + +- View the full student roster (searchable by name or UTORid) +- **Remove** individual students by hovering their row and clicking the remove icon +- **Add students** by typing one or more UTORids (comma, space, or newline separated) +- **Sync roster from CSV** — upload a class list CSV exported from ACORN/ROSI. The app shows a preview of who will be added and removed before applying. TAs are not affected by a sync. + +### TAs tab + +- View and remove current TAs +- Add new TAs by UTORid — same input format as students + +### Rename tab + +- Update the course code and/or semester label + +### Delete tab + +- Permanently deletes the course and everything under it: all sessions, questions, answers, upvotes, and uploaded slides +- Requires typing the course code to confirm +- **Blocked if the course has an active session** — end the session first + +--- + +## End of Semester Cleanup + +### Step 1 — Delete courses through the app + +For each course you want to retire, go to `/classes`, open "Manage Lecture", go to the **Delete** tab, type the course code, and confirm. This cascades through the database and removes all associated sessions, Q&A data, enrollments, and slide records. + +### Step 2 — Remove uploaded slide files from disk + +Course deletion removes the database records for slides, but the PDF files themselves stay on disk. To free up space: + +```bash +ssh @askeasy.utm.utoronto.ca +rm -rf ~/AskEasy/uploads/* +``` + +--- + +## Direct Database Access (VM Method) + +If you need to inspect data directly or run a query the app UI doesn't support: + +```bash +# SSH into the server +ssh @askeasy.utm.utoronto.ca + +# Open a psql shell inside the Postgres container +docker exec -it ask_easy-postgres-1 psql -U postgres -d ask_easy +``` + +Useful psql commands: + +| Command | What it does | +|---------|-------------| +| `\dt` | List all tables | +| `\q` | Exit psql | +| `SELECT * FROM "User";` | See all users who have logged in | +| `SELECT * FROM "Course";` | See all courses | +| `SELECT * FROM "Session";` | See all sessions | + +### Database tables + +| Table | What it stores | +|-------|---------------| +| `User` | Everyone who has logged in (UTORid, name, email, global role) | +| `Course` | Courses created by professors | +| `CourseEnrollment` | Which users are in which courses (STUDENT / TA / PROFESSOR) | +| `Session` | Live Q&A sessions within a course | +| `Question` | Questions asked during sessions | +| `Answer` | Answers to questions | +| `QuestionUpvote` / `AnswerUpvote` | Upvote records | +| `SlideSet` | Uploaded PDF metadata (files live in `uploads/`) | + +--- + +## Shibboleth SSO + +### How login works + +1. User visits `https://askeasy.utm.utoronto.ca` +2. Apache checks for a Shibboleth session. If there isn't one, it redirects to the U of T login page +3. User logs in with their UTORid and password (same as Quercus, ACORN, etc.) +4. The U of T identity provider sends back a SAML assertion. mod_shib validates it and injects `utorid`, `mail`, and `cn` headers into the request +5. Apache proxies the request to the Next.js app, which reads those headers and creates an encrypted session cookie (8-hour TTL) + +### Header spoofing prevention + +- **Apache** strips any client-supplied identity headers before mod_shib injects the real ones +- **The app** (`src/server.ts`) also strips these headers from any connection that isn't coming from localhost + +### Key files on the VM + +| File | Purpose | +|------|---------| +| `/etc/apache2/sites-enabled/askeasy.conf` | Apache vhost — TLS, reverse proxy, Shibboleth directives | +| `/etc/shibboleth/shibboleth2.xml` | Shibboleth SP config — entity ID, IdP endpoint, metadata | +| `/etc/shibboleth/utorauth_metadata_verify.crt` | U of T metadata signing certificate | +| `/etc/letsencrypt/live/askeasy.utm.utoronto.ca/` | TLS certificates (auto-renewed by certbot) | +| `~/AskEasy/whitelist.txt` | UTORids that receive the PROFESSOR role |