Skip to content

Commit f5f6170

Browse files
committed
Merge dev into bootcamp; workshop model replaces bootcamp_material
Resolve the #390 overlap in favour of bootcamp_workshop. #390's routes, components and tests are removed; its table stays declared so drizzle-kit push does not stop at a DROP TABLE prompt, to be dropped separately. Club page keeps #390's syllabus with attendance badges and adds the welcome copy and weekly materials table, so enrolled members see both. Fixes from review: - Gate member reads and downloads on the workshop's own term, not currentTerm(), so a cohort keeps its material after the term rolls. - upsertSession takes a workshopId and uses that row's term and week, and the admin page calls it only when the date or room changed, so a TBA save no longer detaches the live session for that week. - Rescheduling no longer overwrites the event's title, and an omitted location leaves the event's room alone. - A workshop's week is fixed after creation; the session joins on it. - Drop the process-wide setMaxListeners(15).
2 parents c130374 + cd49243 commit f5f6170

56 files changed

Lines changed: 1792 additions & 1478 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apphosting.yaml‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,19 @@ env:
6262
# Cloud Storage bucket holding resume PDFs. Postgres keeps only the metadata
6363
# and the object key — 5000 resumes is 1.5 GB and this database is 0.5 GB.
6464
# The runtime service account needs objectAdmin on it:
65-
# gcloud storage buckets create gs://dsgt-resumes --location=us-central1 \
65+
# gcloud storage buckets create gs://dsgt-resumes --location=us-east4 \
6666
# --uniform-bucket-level-access --public-access-prevention
6767
# gcloud storage buckets add-iam-policy-binding gs://dsgt-resumes \
68-
# --member=serviceAccount:<app-hosting-runtime-sa> \
68+
# --member=serviceAccount:firebase-app-hosting-compute@dsgt-website.iam.gserviceaccount.com \
6969
# --role=roles/storage.objectAdmin
70+
# Both are already applied in dsgt-website. us-east4 matches the backend's
71+
# Cloud Run region, so reads and writes stay in-region.
7072
- variable: RESUME_BUCKET
7173
value: dsgt-resumes
72-
# Workshop ZIPs have a separate lifecycle and access policy from resumes.
73-
# Local development must use a separate bucket because E2E checks delete
74-
# deterministic object keys.
74+
# Cloud Storage bucket holding bootcamp workshop ZIPs — notebooks,
75+
# datasets. Same runtime service account, same objectAdmin grant, already
76+
# applied. Separate from the resume bucket on purpose: those are documents
77+
# no member may read, these are files every enrolled member may.
7578
- variable: BOOTCAMP_BUCKET
7679
value: dsgt-bootcamp
7780
# Consumer Gmail, which caps around 500 recipients a day — shared between

‎docs/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Members looking for a club-language overview should start at [Club project](./cl
1616
| [Security](./operations/security.md) | Auth gates, rate limits, CSP, input scrubbing |
1717
| [Testing](./operations/testing.md) | Vitest, Playwright, and what each suite protects |
1818
| [Resume book](./resume-book.md) | Member uploads, the two staff views, limits, and why files skip tRPC |
19+
| [Bootcamp materials](./bootcamp-materials.md) | Weekly workshop ZIPs: who can download one, publishing, session dates, storage |
1920
| [Glossary](./glossary.md) | Club vs hackathon vocabulary |
2021

2122
## Packages

‎docs/architecture.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Year-round DSGT operations. **Not** scoped to a hackathon row.
4242
- Membership (`member`, `membership_history`) — one paid year per person, defined by start/end dates
4343
- Club events and QR check-in (`event`, `event_check_in`)
4444
- Bootcamp sessions are club events with `bootcamp_week` + `bootcamp_term`
45+
- Bootcamp workshop material (`bootcamp_workshop`, one row per week) joins those events on `(term, week)`; the ZIPs are in Cloud Storage and downloads are gated on the workshop's own term — see [Bootcamp materials](./bootcamp-materials.md)
4546
- Initiatives (`initiative`, `initiative_application`) led by `project_leader`
4647
- Stripe payments and account linking
4748

‎docs/bootcamp-materials.md‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Bootcamp materials
2+
3+
Each week of a bootcamp has one workshop row: a title, a materials ZIP, a solution ZIP and a recording link. Staff manage the rows at `/admin/bootcamp` and publish each one when it's ready. Members download from `/club/bootcamp`. Nobody else can download, including members who didn't buy the bootcamp.
4+
5+
Sessions themselves are ordinary events carrying `bootcamp_week` and `bootcamp_term`; see [Architecture](./architecture.md). A workshop row finds its session by `(term, week)`. There is no foreign key, so deleting a session never deletes the notebooks.
6+
7+
## Where things are
8+
9+
| Piece | Path |
10+
| --- | --- |
11+
| Table (metadata only) | `packages/db/src/schemas/bootcamp.ts` (`bootcamp_workshop`) |
12+
| API | `packages/api/src/routers/bootcamp.ts` (`workshops`, `adminWorkshops`, `createWorkshop`, `updateWorkshop`, `upsertSession`, `setPublished`) |
13+
| Upload / serve / clear one ZIP | `sites/mainweb/app/(portal)/api/bootcamp/materials/[workshopId]/[kind]/route.ts` |
14+
| Delete a workshop | `sites/mainweb/app/(portal)/api/bootcamp/materials/[workshopId]/route.ts` |
15+
| ZIP rules (signature, cap, names) | `sites/mainweb/lib/bootcamp-file.ts` |
16+
| Download rule | `sites/mainweb/lib/bootcamp-route-rules.ts` |
17+
| Caller lookup | `sites/mainweb/lib/bootcamp-access.ts` |
18+
| Bucket client | `sites/mainweb/lib/bootcamp-storage.ts` |
19+
| Member table / staff modal | `sites/mainweb/components/portal/BootcampMaterialsTable.tsx`, `BootcampWorkshopModal.tsx` |
20+
21+
## Who can download a file
22+
23+
- Staff can download anything, drafts and past terms included.
24+
- A member can download a **published** file when their `member.bootcamp_term` equals the **workshop's term**, not the current term. Someone who bought the fall bootcamp keeps the fall notebooks in January. They don't get the spring ones.
25+
- Everyone else gets **404, not 403**, so a guessed workshop id can't confirm that a draft exists.
26+
27+
Publishing is its own procedure (`setPublished`), separate from saving. That keeps a row hidden until its uploads have landed, and a failed upload can't leave it visible.
28+
29+
## Session dates
30+
31+
The modal's date and room fields call `upsertSession`, but only when the officer changed one of them. So saving a TBA workshop never touches an event. Term and week come from the workshop row, never from the clock, so editing a past cohort can't reach the current one.
32+
33+
- **Setting a date** creates the week's event, or reschedules the existing one. The QR code is minted on insert only, so printed signs keep working. The event's title is left alone on reschedule.
34+
- **Clearing a date** detaches the event (clears `bootcamp_week`/`bootcamp_term`) rather than deleting it, because deleting an event destroys its check-ins.
35+
- A workshop's **week is fixed** once it's created, because moving it would leave its session behind. To renumber a workshop, delete it and create it again.
36+
37+
## Storage
38+
39+
ZIPs live in `gs://dsgt-bootcamp` under `bootcamp/<workshopId>/<kind>.zip`. Postgres holds the object key, file name and size. Objects are keyed by the workshop id, which never changes. A key based on `(term, week)` would let one row's upload overwrite another row's file.
40+
41+
Uploads bypass tRPC and stream straight into the bucket. `uploadProcedure` caps at 2MB and superjson base64-encodes the body. The first four bytes are held until the ZIP signature checks out, and the stream stops at 20MB, which leaves headroom under Cloud Run's 32MiB request limit.
42+
43+
Downloads are proxied, never redirected to a signed URL, which would leave the origin and the auth check behind. The bucket keeps uniform bucket-level access and public access prevention **enforced**. The App Hosting runtime service account has `roles/storage.objectAdmin` on it.
44+
45+
Credentials are Application Default Credentials. Locally, run `gcloud auth application-default login`. With `BOOTCAMP_BUCKET` unset, the file routes return 503.
46+
47+
## Known limitation
48+
49+
An upload and a removal of the same file can race, because the object write and the row update can't be one transaction. With a handful of officers, the impact is low.
50+
51+
## Legacy table
52+
53+
`bootcamp_material` (#390's per-event handouts) is still declared in the schema, but nothing reads or writes it. Deploys run `drizzle-kit push`, and removing the declaration would make push stop at a DROP TABLE prompt. Drop the table in a separate change.

‎docs/operations/environment.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ Without `DATABASE_URL`, `db` is null, sessions fall back to JWT, and tRPC proced
5656
| Variable | Default / notes |
5757
| --- | --- |
5858
| `RESUME_BUCKET` | Cloud Storage bucket holding resume PDFs (App Hosting sets `dsgt-resumes`). Unset means uploads return 503 rather than failing obscurely. Credentials are ADC — the runtime service account needs `roles/storage.objectAdmin`. See [Resume book](../resume-book.md) |
59-
| `BOOTCAMP_BUCKET` | Cloud Storage bucket holding workshop ZIPs (App Hosting sets `dsgt-bootcamp`). Use a separate development bucket locally because uploads and deletes use deterministic workshop-id keys. Unset means file routes return 503. Credentials are ADC; the active account needs `roles/storage.objectAdmin`. |
59+
| `BOOTCAMP_BUCKET` | Cloud Storage bucket holding bootcamp workshop ZIPs (App Hosting sets `dsgt-bootcamp`). Unset means uploads and downloads return 503. Same ADC and same `roles/storage.objectAdmin` as above. Downloads are proxied and gated on the caller's bootcamp term, so the bucket keeps public access prevention on |
6060

6161
## Security / proxy
6262

‎docs/operations/testing.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ Tests use `src/test/create-mock-context.ts` and `.internal-tests/_db-tx-mock.ts`
4343

4444
## Mainweb
4545

46-
`sites/mainweb/lib/*.test.ts` — phone formatting, hackathon slugs, `safe-callback`.
46+
`sites/mainweb/lib/*.test.ts` — phone formatting, hackathon slugs, `safe-callback`, bootcamp ZIP rules (`bootcamp-file.test.ts`), the download gate (`bootcamp-route-rules.test.ts`, where the null-term trap is held down) and the upload and download handlers (`bootcamp-routes.test.ts` — the handlers live under `app/`, which the root script does not glob).
4747

4848
## Hacklytics
4949

‎docs/packages/db.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Files in `src/schemas/`, re-exported from `schemas/index.ts`. `drizzle.config.ts
3131
| `judge.ts` | `judge`, `judge_assignment`, `judging_project`, `judge_vote`, `judge_queue`, `hackathon_result` |
3232
| `initiatives.ts` | `project_leader`, `initiative`, `initiative_application` |
3333
| `events.ts` | `event`, `event_check_in` |
34+
| `bootcamp.ts` | `bootcamp_workshop` (metadata; the ZIPs are in Cloud Storage). `bootcamp_material` is legacy and unused |
3435
| `stripe.ts` | `stripe_payment`, `user_account_link` |
3536
| `security.ts` | `audit_logs` (+ `security_severity` enum) |
3637
| `settings.ts` | `system_settings` (single row, `id = 'default'`) |

‎docs/sites/mainweb.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ Unauthenticated and authenticated product UI. `proxy.ts` marks these prefixes `p
5050
| `/admin/attendees` | Attendee tools |
5151
| `/admin/judging` | Judging admin (sync submissions + assign judges live here) |
5252
| `/admin/initiatives` | Initiative / proposal review |
53-
| `/admin/bootcamp` | Bootcamp attendance |
53+
| `/admin/bootcamp` | Bootcamp attendance grid and the weekly workshop table |
5454
| `/admin/staff` | Admin users |
5555
| `/admin/analytics` | Overview |
5656
| `/admin/audit` | Audit log |
@@ -65,6 +65,8 @@ Unauthenticated and authenticated product UI. `proxy.ts` marks these prefixes `p
6565
| `/api/auth/[...nextauth]` | NextAuth handlers |
6666
| `/api/auth/verify-email` | Email-code verification |
6767
| `/api/webhooks/stripe` | Stripe webhooks |
68+
| `/api/bootcamp/materials/[workshopId]` | Delete a workshop and its ZIPs (`DELETE`, staff) |
69+
| `/api/bootcamp/materials/[workshopId]/[kind]` | Upload (`POST`, staff), download (`GET`, gated on the workshop's term and publish state) or clear (`DELETE`, staff) the `materials` or `solution` ZIP |
6870

6971
## Client data
7072

‎package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515
"test": "vitest run packages/api packages/db sites/mainweb/lib"
1616
},
1717
"dependencies": {
18-
"next": "16.3.0",
18+
"next": "16.3.3",
1919
"typescript": "^6.0.2"
2020
},
2121
"devDependencies": {
2222
"turbo": "^2.9.14",
23-
"vitest": "^4.1.8"
23+
"vitest": "^4.1.11"
2424
},
2525
"pnpm": {
2626
"overrides": {

‎packages/api/package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
"drizzle-orm": "0.45.2",
3636
"image-size": "2.0.2",
3737
"prom-client": "15.1.3",
38-
"sanitize-html": "2.17.4",
38+
"sanitize-html": "2.17.7",
3939
"stripe": "^22.0.0",
4040
"superjson": "2.2.3",
4141
"zod": "3.25.53"
@@ -44,6 +44,6 @@
4444
"@query/tsconfig": "workspace:*",
4545
"@types/sanitize-html": "^2.16.0",
4646
"typescript": "6.0.2",
47-
"vitest": "^4.1.8"
47+
"vitest": "^4.1.11"
4848
}
4949
}

0 commit comments

Comments
 (0)