-
Notifications
You must be signed in to change notification settings - Fork 0
Enhance documentation and scripts for GradeSync setup and deployment #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,18 +2,73 @@ | |
|
|
||
| ## Source Files | ||
|
|
||
| - Middleware entry: [../../api/lib/authlib.mjs](../../api/lib/authlib.mjs) | ||
| - IAM policy: [../../api/lib/iam.mjs](../../api/lib/iam.mjs) | ||
| - Token helpers: [../../api/lib/jwtAuth.mjs](../../api/lib/jwtAuth.mjs) | ||
| - OAuth helper: [../../api/lib/googleAuthHelper.mjs](../../api/lib/googleAuthHelper.mjs) | ||
| | File | Purpose | | ||
| |------|---------| | ||
| | `api/lib/authlib.mjs` | Express middleware — `validateAdminMiddleware`, `validateStudentMiddleware` | | ||
| | `api/lib/iam.mjs` | Low-level permission check helpers (course-scoped reads) | | ||
| | `api/lib/jwtAuth.mjs` | Signs and verifies permission-snapshot JWTs | | ||
| | `api/lib/googleAuthHelper.mjs` | Verifies Google ID tokens, enforces `@berkeley.edu` domain | | ||
| | `api/lib/userlib.mjs` | Resolves user role from `users` + `course_permissions` tables | | ||
| | `api/v2/Routes/login/index.js` | `POST /api/v2/login` handler | | ||
| | `api/v2/Routes/isadmin/index.js` | `GET /api/v2/isadmin` handler | | ||
|
|
||
| ## Authentication Flow (step by step) | ||
|
|
||
| 1. Browser calls Google OAuth and receives an **ID token**. | ||
| 2. Frontend `POST /api/v2/login` with `{ token: "<google-id-token>" }`. | ||
| 3. `googleAuthHelper.mjs` verifies the token against Google's public keys. | ||
| - Rejects if the `hd` (hosted domain) field is not `berkeley.edu`. | ||
| - Rejects if the token is expired or signature invalid. | ||
| 4. `userlib.mjs` queries the `users` table by `email`. If the user does not exist yet, a new row is inserted. | ||
| 5. `userlib.mjs` queries `course_permissions` to build a list of `{ course_id, permission_level }` pairs. | ||
| 6. A **JWT** is signed with `JWT_SECRET` (from `.env`) and returned to the browser. | ||
|
Comment on lines
+22
to
+24
|
||
| - Payload includes `email`, `role`, and the `course_permissions` snapshot. | ||
| - Expiry is controlled by `JWT_EXPIRES_IN` (default `12h`). | ||
| 7. All subsequent requests must carry `Authorization: Bearer <jwt>`. | ||
|
|
||
| ## Middleware Behaviour | ||
|
|
||
| ### `validateAdminMiddleware` | ||
| - Verifies JWT signature and expiry. | ||
| - Checks that the user's `role` in the token is `super_admin`, `course_admin`, or `instructor`. | ||
| - For course-scoped routes, checks `course_permissions` for the target `course_id`. | ||
| - Returns `403` if any check fails. | ||
|
Comment on lines
+24
to
+35
|
||
|
|
||
| ### `validateStudentMiddleware` | ||
| - Verifies JWT signature and expiry. | ||
| - Queries `students` table to confirm the requesting email is enrolled in the target course. | ||
| - Returns `403` if not enrolled. | ||
|
|
||
| ## IAM Roles | ||
|
|
||
| | Role | Source of Truth | Permissions | | ||
| |------|----------------|-------------| | ||
| | `super_admin` | `gradeview.admins` in `config.json` (migrate to DB) | All courses, all actions, GradeSync admin | | ||
| | `course_admin` | `course_permissions.permission_level = 'owner'` | Manage sync and config for bound courses | | ||
|
Comment on lines
+46
to
+47
|
||
| | `instructor` | `course_permissions.permission_level = 'editor'` | View class roster and grades; no GradeSync admin | | ||
| | `ta` | `course_permissions.permission_level = 'viewer'` | Same as instructor | | ||
| | `student` | `students` table (`email + course_id`) | Own grades only, per enrolled course | | ||
|
|
||
| ## Key Rules | ||
|
|
||
| - Staff/admin authorization is DB-only (`users` + `course_permissions`). | ||
| - Student access is course-scoped. | ||
| - Super admin is the only global bypass. | ||
| - **DB is the authoritative source** for staff and student permissions. | ||
| - Config file `admins`/`instructors`/`tas` lists are **legacy** — migrate them into `users` + `course_permissions` rows and do not rely on them for runtime auth. | ||
| - Students are **not** in the `users` table; they are identified solely by their email in the `students` table. | ||
| - `super_admin` is the only role that can read across course boundaries. | ||
| - Every API query on grade data must include a `course_id` scope — never fetch grades without one. | ||
|
|
||
| ## Adding a New Staff Member | ||
|
|
||
| ## Related API Routes | ||
| ```sql | ||
| -- 1. Insert user (if they have not logged in yet) | ||
| INSERT INTO users (email, name, role) | ||
| VALUES ('newperson@berkeley.edu', 'New Person', 'instructor') | ||
| ON CONFLICT (email) DO NOTHING; | ||
|
|
||
| - Login: [../../api/v2/Routes/login/index.js](../../api/v2/Routes/login/index.js) | ||
| - Admin check: [../../api/v2/Routes/isadmin/index.js](../../api/v2/Routes/isadmin/index.js) | ||
| -- 2. Grant course permission | ||
| INSERT INTO course_permissions (course_id, user_id, permission_level, granted_by) | ||
| SELECT c.id, u.id, 'editor', (SELECT id FROM users WHERE email = 'admin@berkeley.edu') | ||
| FROM courses c, users u | ||
| WHERE c.gradescope_course_id = '1098053' | ||
| AND u.email = 'newperson@berkeley.edu'; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,16 +2,66 @@ | |||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ## Source Files | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| - Config routes: [../../api/v2/Routes/config/index.js](../../api/v2/Routes/config/index.js) | ||||||||||||||||||||||||||||||
| - Unified config helpers: [../../api/lib/unifiedConfig.mjs](../../api/lib/unifiedConfig.mjs) | ||||||||||||||||||||||||||||||
| - Runtime config template: [../../config.example.json](../../config.example.json) | ||||||||||||||||||||||||||||||
| | File | Purpose | | ||||||||||||||||||||||||||||||
| |------|---------| | ||||||||||||||||||||||||||||||
| | `api/lib/unifiedConfig.mjs` | Loads, caches, and exposes `config.json` to the API | | ||||||||||||||||||||||||||||||
| | `api/v2/Routes/config/index.js` | REST endpoints for reading/writing course config | | ||||||||||||||||||||||||||||||
| | `gradesync/api/config_manager.py` | Python equivalent — loads `config.json` for GradeSync | | ||||||||||||||||||||||||||||||
| | `config.example.json` | Full annotated template for `config.json` | | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ## Current Shape | ||||||||||||||||||||||||||||||
| ## Config File Location | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| - `gradeview`: auth/UI-level config | ||||||||||||||||||||||||||||||
| - `gradesync`: per-course sync settings + global sync settings | ||||||||||||||||||||||||||||||
| The unified runtime config is `config.json` at the **repository root**. It is volume-mounted read-only into both the `api` and `gradesync` containers: | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ## Notes | ||||||||||||||||||||||||||||||
| ```yaml | ||||||||||||||||||||||||||||||
| # docker-compose.yml | ||||||||||||||||||||||||||||||
| api: | ||||||||||||||||||||||||||||||
| volumes: | ||||||||||||||||||||||||||||||
| - ./config.json:/api/config/default.json:ro | ||||||||||||||||||||||||||||||
| gradesync: | ||||||||||||||||||||||||||||||
| volumes: | ||||||||||||||||||||||||||||||
| - ./config.json:/app/config.json:ro | ||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| - Keep permissions in DB tables, not in config files. | ||||||||||||||||||||||||||||||
| - Keep config format stable and versioned for safer rollout. | ||||||||||||||||||||||||||||||
| Changes to `config.json` require a container restart — there is no live reload. | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
| Changes to `config.json` require a container restart — there is no live reload. | |
| Reload behavior differs by service: GradeSync loads `/app/config.json` at startup, so changes require a `gradesync` container restart; the API reads `/api/config.json` from disk on each `loadUnifiedConfig()` call, so mounted file changes are picked up on subsequent requests. |
Copilot
AI
Apr 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The stated API loading order doesn’t match the code: api/server.js does not import unifiedConfig.mjs, and there is no getConfig() helper (routes call loadUnifiedConfig() directly). Update the steps here to reflect the actual call sites (e.g., api/v2/Routes/config/index.js) and function names.
| 1. `api/server.js` imports `unifiedConfig.mjs` at startup. | |
| 2. `unifiedConfig.mjs` reads `config/default.json` (which is the mounted `config.json`). | |
| 3. The config object is cached in memory for the lifetime of the process. | |
| 4. Routes that need config call `getConfig()` from `unifiedConfig.mjs`. | |
| 1. Route modules that need config import `loadUnifiedConfig()` from `api/lib/unifiedConfig.mjs` (for example, `api/v2/Routes/config/index.js`). | |
| 2. `loadUnifiedConfig()` reads `config/default.json` (which is the mounted `config.json`). | |
| 3. The config object is cached in memory for the lifetime of the process. | |
| 4. Subsequent route calls reuse that cached config via `loadUnifiedConfig()`. |
Copilot
AI
Apr 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The “Config vs. Database” table implies the OAuth client ID lives in config.json, but Google token verification reads it from the gradeview_config DB table (api/lib/googleAuthHelper.mjs). If config.json is only a seed/migration input, it would be clearer to mark the DB as the runtime source of truth and mention the migration step explicitly.
| | OAuth client ID | `config.json` | | |
| | Global admin emails | `config.json` → migrate to `users` table over time | | |
| | Course sync source IDs and credentials | `config.json` | | |
| | Per-course staff permissions | `course_permissions` DB table | | |
| | Student enrollment | `students` DB table | | |
| | Grade data | `assignments` + `submissions` DB tables | | |
| | OAuth client ID | Seed/bootstrap in `config.json`; runtime source of truth is the `gradeview_config` DB table | | |
| | Global admin emails | Seed/bootstrap in `config.json`; migrate/sync into the `users` table, which is the runtime authority | | |
| | Course sync source IDs and credentials | `config.json` | | |
| | Per-course staff permissions | `course_permissions` DB table | | |
| | Student enrollment | `students` DB table | | |
| | Grade data | `assignments` + `submissions` DB tables | | |
| If a setting exists in both `config.json` and a DB table, treat the DB row as the runtime source of truth. `config.json` is only the seed/bootstrap input for values that are migrated or synced into the database. |
Copilot
AI
Apr 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Step 6 references triggering an initial sync via POST /gradesync/sync/{course_id}, but GradeSync sync routes are implemented under /api/sync/{course_id} (so via reverse proxy: /gradesync/api/sync/{course_id}). Update the endpoint here to match the actual FastAPI routes.
| 6. Trigger an initial sync via the GradeSync admin UI or `POST /gradesync/sync/{course_id}`. | |
| 6. Trigger an initial sync via the GradeSync admin UI or `POST /gradesync/api/sync/{course_id}`. |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,18 +1,80 @@ | ||||||
| # Feature: Dev & Deployment | ||||||
|
|
||||||
| ## Local Development | ||||||
| ## Key Files | ||||||
|
|
||||||
| - Main setup: [../../README.md](../../README.md) | ||||||
| - Refresh script: [../../scripts/refresh.sh](../../scripts/refresh.sh) | ||||||
| - Dev compose: [../../docker-compose.dev.yml](../../docker-compose.dev.yml) | ||||||
| | File | Purpose | | ||||||
| |------|---------| | ||||||
| | `docker-compose.dev.yml` | Dev stack: bind-mounts, hot-reload, exposed debug ports | | ||||||
| | `docker-compose.yml` | Production stack: healthchecks, log rotation, TLS mounts | | ||||||
| | `Makefile` | Shortcuts for common operations | | ||||||
| | `scripts/dev-local.sh` | Run API + web natively; deps in Docker | | ||||||
| | `scripts/preflight.sh` | Full production smoke-test | | ||||||
| | `scripts/refresh.sh` | Pull latest images + restart | | ||||||
| | `scripts/deploy_to_gcp.sh` | One-shot GCE VM provisioning | | ||||||
|
|
||||||
| ## Production-ish Deployment | ||||||
| ## Local Development — Full Docker Mode | ||||||
|
|
||||||
| - Compose: [../../docker-compose.yml](../../docker-compose.yml) | ||||||
| - API Dockerfile: [../../api/Dockerfile](../../api/Dockerfile) | ||||||
| - Web Dockerfile: [../../website/server/Dockerfile](../../website/server/Dockerfile) | ||||||
| ```bash | ||||||
| # First-time setup | ||||||
| cp .env.example .env && cp config.example.json config.json | ||||||
| # (fill in .env and config.json) | ||||||
| docker compose -f docker-compose.dev.yml up --build | ||||||
| ``` | ||||||
|
|
||||||
| ## Rule of Thumb | ||||||
| All source directories are bind-mounted. Node.js services use `nodemon`; FastAPI uses `--reload`. Changes take effect without rebuilding. | ||||||
|
||||||
| All source directories are bind-mounted. Node.js services use `nodemon`; FastAPI uses `--reload`. Changes take effect without rebuilding. | |
| All source directories are bind-mounted. Node.js services use `nodemon`; GradeSync/FastAPI is currently started without `--reload`, so Python code changes require a container restart rather than a rebuild. |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,20 +1,92 @@ | ||||||
| # Feature: GradeSync | ||||||
|
|
||||||
| ## Docs | ||||||
| ## Related Docs | ||||||
|
|
||||||
| - Setup guide: [../../gradesync/SETUP_DEMO.md](../../gradesync/SETUP_DEMO.md) | ||||||
| - Demo guide: [../../gradesync/DEMO_COURSE_README.md](../../gradesync/DEMO_COURSE_README.md) | ||||||
| - Start here: [../../gradesync/START_HERE.md](../../gradesync/START_HERE.md) | ||||||
| - Demo course creation: [../../gradesync/DEMO_COURSE_README.md](../../gradesync/DEMO_COURSE_README.md) | ||||||
|
|
||||||
| ## Source Files | ||||||
| ## Key Source Files | ||||||
|
|
||||||
| - App entry: [../../gradesync/api/app.py](../../gradesync/api/app.py) | ||||||
| - Config manager: [../../gradesync/api/config_manager.py](../../gradesync/api/config_manager.py) | ||||||
| - DB models: [../../gradesync/api/core/models.py](../../gradesync/api/core/models.py) | ||||||
| - Sync service: [../../gradesync/api/sync/service.py](../../gradesync/api/sync/service.py) | ||||||
| | File | Purpose | | ||||||
| |------|---------| | ||||||
| | `gradesync/api/app.py` | FastAPI app factory, lifespan hooks, route registration | | ||||||
| | `gradesync/api/config_manager.py` | Reads `config.json` mounted at `/app/config.json` | | ||||||
| | `gradesync/api/schemas.py` | Pydantic models for all request/response bodies | | ||||||
| | `gradesync/api/core/db.py` | SQLAlchemy engine + async session factory | | ||||||
|
||||||
| | `gradesync/api/core/db.py` | SQLAlchemy engine + async session factory | | |
| | `gradesync/api/core/db.py` | SQLAlchemy engine + session factory | |
Copilot
AI
Apr 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The “Key Source Files” table lists gradesync/api/services/gradescope.py, prairielearn.py, and iclicker.py, but in the current codebase these are packages/directories (gradesync/api/services/gradescope/, .../prairielearn/, .../iclicker/) with entry points like sync.py / client.py. Update the file paths so readers can navigate to the real modules.
Copilot
AI
Apr 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The sync trigger examples use endpoints that do not exist in the FastAPI app. GradeSync defines sync routes under /api/sync/{course_id} (see gradesync/api/app.py), so through the reverse proxy the URL would be /gradesync/api/sync/{course_id} (or direct http://localhost:8001/api/sync/{course_id} in the full stack). Also, the example mentions an admin JWT header, but GradeSync routes are not currently protected by JWT auth, and there is no /status/{course_id} route. Please update the examples to match the implemented routes/auth model.
Copilot
AI
Apr 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doc instructs running python create_demo_course.py ..., but there is no create_demo_course.py in the repository (searching the repo returns no matches). Either add/restore the script, or update the documentation to reference the actual demo data workflow that exists today (e.g., manual sync against a sandbox course).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The source-file table and login-flow steps describe
POST /api/v2/loginwith a JSON body token, but the current implementation isGET /api/v2/loginwith the Google ID token in theAuthorization: Bearer ...header (seeapi/v2/Routes/login/index.jsandwebsite/src/views/login.js). Please update the docs to match the actual request method and token transport.