Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 0 additions & 15 deletions .github/workflows/weekly-audit-log-cleanup.yml

This file was deleted.

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,4 @@ graphify-out/cost.json
# `*.tsbuildinfo` above does not match these, so they were tracked and every
# build dirtied the working tree.
.cache/
bash.exe.stackdump
4 changes: 2 additions & 2 deletions GCP_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ This will pull the following from GCP:

## 4. Running the App

To run the entire stack (Main Web + Discord Bot) in development mode:
To run every workspace in development mode:

```bash
pnpm dev:full
pnpm dev
```

## 5. Troubleshooting
Expand Down
96 changes: 94 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,14 @@ in `drizzle.config.ts`.
| `members.ts` | `user_profile`, `member`, `membership_history` |
| `admins.ts` | `admin` |
| `hackathons.ts` | `hackathon`, `hackathon_team`, `hackathon_participant`, `hackathon_project`, `hackathon_event`, `hackathon_event_attendee` |
| `judge.ts` | `judge`, `judge_assignment`, `judging_project`, `judge_vote`, `judge_queue`, `hackathon_map` |
| `judge.ts` | `judge`, `judge_assignment`, `judging_project`, `judge_vote`, `judge_queue` |
| `initiatives.ts` | `project_leader`, `initiative`, `initiative_application` |
| `events.ts` | `event`, `event_check_in` |
| `stripe.ts` | `stripe_payment`, `user_account_link` |
| `security.ts` | `audit_logs` (+ `security_severity` enum) |
| `settings.ts` | `system_settings` |

26 tables in total. Two entities anchor the graph:
Two entities anchor the graph:

- **`user`** — every identity-bearing table cascades from it: `account`,
`session`, `admin`, `user_profile`, `member`, `judge`, `event`,
Expand All @@ -62,6 +63,97 @@ in `drizzle.config.ts`.
Nearly all foreign keys are `onDelete: "cascade"`, so deleting a user or a
hackathon removes its dependent rows rather than orphaning them.

### Club and hackathon are separate

Two aspects share the database and touch nowhere:

- **Hackathon** — editions, registration, teams, project submission, judging.
Everything here hangs off a `hackathon` row.
- **Club** — `initiative`, its applications, and the `project_leader` role.
Deliberately **not** scoped to a hackathon. A club project runs whenever
somebody leads one, and leading is a standing appointment rather than a
yearly re-grant. Nothing in this half is ever judged; judges only score
`hackathon_project`.

`member` is the one crossing case: a paid year still hangs off an edition, so
membership resolves the current hackathon even though initiatives do not.

#### One-off step — only for a database that already has the edition-scoped tables

**Check first:**

```sql
SELECT to_regclass('public.project_leader');
```

If that returns `NULL`, this database has never had the club tables. Skip
everything below — `migrate:push` simply creates them in the current shape, and
the statements here would error on tables that do not exist.

If it returns a table name, `migrate:push` cannot work the change out on its
own. `project_leader` moved from `unique(user_id, hackathon_id)` to
`unique(user_id)`, so anybody appointed in more than one edition has more than
one row; drizzle-kit fails building the new index partway and leaves the schema
half-applied. Run this against that database **once, before** the push. Every
statement is guarded, so it is safe to re-run.

```sql
BEGIN;

-- Collapse duplicate leader appointments to one row per person. Keeps the
-- oldest row, so created_at still reads as when they were first appointed, and
-- keeps the role switched on if ANY of their rows was active — dropping an
-- active appointment here silently locks a leader out of their own initiatives.
WITH ranked AS (
SELECT
id,
user_id,
bool_or(is_active) OVER (PARTITION BY user_id) AS any_active,
row_number() OVER (PARTITION BY user_id ORDER BY created_at ASC, id ASC) AS rn
FROM project_leader
)
UPDATE project_leader AS pl
SET is_active = ranked.any_active
FROM ranked
WHERE pl.id = ranked.id
AND ranked.rn = 1
AND pl.is_active IS DISTINCT FROM ranked.any_active;

DELETE FROM project_leader
WHERE id IN (
SELECT id FROM (
SELECT
id,
row_number() OVER (PARTITION BY user_id ORDER BY created_at ASC, id ASC) AS rn
FROM project_leader
) dupes
WHERE rn > 1
);

-- Drop the edition columns and everything hanging off them.
ALTER TABLE project_leader
DROP CONSTRAINT IF EXISTS unique_project_leader_per_hackathon;
DROP INDEX IF EXISTS project_leader_hackathon_id_idx;
ALTER TABLE project_leader DROP COLUMN IF EXISTS hackathon_id;

DROP INDEX IF EXISTS initiative_hackathon_id_idx;
ALTER TABLE initiative DROP COLUMN IF EXISTS hackathon_id;

-- The constraint the new schema expects. Added here rather than left to push,
-- so a collision surfaces inside this transaction where it rolls back.
ALTER TABLE project_leader
DROP CONSTRAINT IF EXISTS unique_project_leader;
ALTER TABLE project_leader
ADD CONSTRAINT unique_project_leader UNIQUE (user_id);

COMMIT;
```

Initiatives themselves are untouched. Rows that were invisible because they
belonged to a past edition become visible again — that is the point, they were
club projects an edition rollover hid. Archive any that should not come back
from the leader screen afterwards.

### Working with the schema

```bash
Expand Down
13 changes: 13 additions & 0 deletions apphosting.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,16 @@ env:
value: datascience.gt@gmail.com
- variable: CRON_SECRET
secret: CRON_SECRET
# Flood-protection thresholds, sized per instance for a full venue.
# These are the ceiling for one signed-in person, not for the building —
# the limiter keys on user id when somebody is signed in. The short block
# duration bounds a false positive to a page refresh rather than locking
# an attendee out for five minutes in the middle of a workshop.
- variable: DDOS_BURST_THRESHOLD
value: "3000"
- variable: DDOS_MAX_REQUESTS_PER_MINUTE
value: "20000"
- variable: DDOS_SUSPICIOUS_THRESHOLD
value: "14000"
- variable: DDOS_BLOCK_DURATION_MS
value: "30000"
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"lint": "turbo run lint",
"format": "prettier --write .",
"typecheck": "turbo run typecheck",
"test": "vitest run packages/api"
"test": "vitest run packages/api packages/db sites/mainweb/lib"
},
"dependencies": {
"next": "16.3.0",
Expand Down
Loading
Loading