Skip to content
Merged
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
10 changes: 10 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ ownerid|isAdmin|timestamp|signature

Every later request validates this cookie in the DAL, so most requests need no portal round-trip. The callback also stores the raw portal token in a second cookie, `prfc_portal_token`, which the member-roster read (`listmembers`) sends back to the portal. The public referral URL carries the same 8-hex signature scheme in a `cs` parameter, over `name|email|code` with the same secret, verified before any referral is accepted.

<img src="figures/auth-flow.png" alt="Token authentication sequence from portal to callback to 303 redirect to cookie to DAL" width="800" />

The callback answers with a 303, not the framework-default 307, so the `SameSite=Lax` cookie survives the cross-site hop to `/home`. The [Auth Patterns ADR](decisions/auth-patterns.md) explains why.

### Routing and the Auth Boundary

Next.js 16 uses `src/proxy.ts`, not `middleware.ts`. The proxy checks for the auth cookie on protected paths and redirects when it is missing, which is a fast UX guard. The real security boundary is `verifySession()` in `src/lib/dal.ts`, which verifies the HMAC and expiry, and `requireAdmin()` gates admin-only features. See the [Auth Patterns ADR](decisions/auth-patterns.md).
Expand All @@ -97,6 +101,12 @@ We memoize reads per request with React `cache()`, so one request makes at most

The Prisma schema defines the app's tables. Referral PII and SMS/email PII are encrypted at the service layer with AES-256-GCM, alongside HMAC blind-index columns for lookup without decryption.

<img src="figures/data-model.png" alt="Entity-relationship diagram of the related tables: contact groups, messages, and events with their members, recipients, invitees, and RSVPs" width="760" />

The four remaining tables - `Referral`, `SmsConsent`, `EmailSuppression`, and `UserPreference` - have no foreign keys. They key off the portal `memberId` rather than a local table, so they sit in their own diagram below.

<img src="figures/data-model-standalone.png" alt="The four standalone tables with no relations: Referral, SmsConsent, EmailSuppression, and UserPreference" width="900" />

| Model | Purpose |
| --------------------------------------------- | --------------------------------------------------------------- |
| `Referral` | Public referral submissions (encrypted PII) |
Expand Down
20 changes: 20 additions & 0 deletions docs/figures/auth-flow.mmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
sequenceDiagram
actor Member
participant Portal as PRFC Member Portal
participant Callback as POST /api/auth/callback
participant Proxy as src/proxy.ts
participant DAL as src/lib/dal.ts (verifySession)

Member->>Portal: log in, click "Open PRFC Outreach"
Portal->>+Callback: POST token (cross-site form)
Callback->>+Portal: validatetoken(token)
Portal-->>-Callback: ownerid, isAdmin, secondsleft
Note over Callback: mint prfc_auth cookie<br/>HMAC over ownerid|isAdmin|timestamp
Callback-->>-Member: 303 See Other -> /home + Set-Cookie
Note over Member,Callback: 303 makes the browser GET /home,<br/>so the SameSite=Lax cookie rides the top-level navigation.<br/>A 307 would re-POST and the cookie would be dropped.
Member->>+Proxy: GET /home (Cookie: prfc_auth)
Note over Proxy: cookie present on protected path? yes
Proxy->>+DAL: page/action calls verifySession()
Note over DAL: validateToken: HMAC + 1-hour expiry check
DAL-->>-Proxy: session (ownerid, isAdmin)
Proxy-->>-Member: dashboard renders
Binary file added docs/figures/auth-flow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 26 additions & 0 deletions docs/figures/data-model-standalone.mmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
erDiagram
Referral {
int id PK
string memberEmail "encrypted PII"
string prospectEmail "encrypted PII"
string referralCode
bool redeemed
}
SmsConsent {
int id PK
int memberId "portal member id"
string phone "encrypted PII"
string phoneHash "blind index"
}
EmailSuppression {
int id PK
string email "encrypted PII"
string emailHash "blind index, unique"
EmailSuppressionReason reason
}
UserPreference {
int id PK
int memberId UK "portal member id"
string photoUrl "Vercel Blob URL"
datetime lastNotificationSeenAt
}
Binary file added docs/figures/data-model-standalone.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
62 changes: 62 additions & 0 deletions docs/figures/data-model.mmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
erDiagram
ContactGroup ||--o{ ContactGroupMember : "has"
ContactGroup ||--o{ MessageGroup : "targeted by"
ContactGroup |o--o{ Event : "scopes"
Message ||--o{ MessageGroup : "targets"
Message ||--o{ MessageRecipient : "sent to"
Event ||--o{ EventInvitee : "invites"
Event ||--o{ EventRsvp : "receives"

ContactGroup {
int id PK
int ownerid "portal member id"
string name
string description
}
ContactGroupMember {
int id PK
int groupId FK
int memberId "portal member id"
bool notifyEmail
bool notifySms
datetime unsubscribedAt "null until opt-out"
}
Message {
int id PK
int senderId "portal member id"
string subject
bool isBlast
int emailCount
}
MessageGroup {
int id PK
int messageId FK
int groupId FK
}
MessageRecipient {
int id PK
int messageId FK
int memberId "portal member id"
string channel "email or sms"
string status "queued sent delivered failed"
}
Event {
int id PK
int groupId FK "nullable"
int ownerid "portal member id"
string title
datetime startDate
datetime endDate
EventType eventType
}
EventInvitee {
int id PK
int eventId FK
int memberId "portal member id"
}
EventRsvp {
int id PK
int eventId FK
int memberId "portal member id"
RsvpStatus status
}
Binary file added docs/figures/data-model.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions docs/future-work.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,19 @@ The repo shows a long list of Dependabot alerts, and almost all of them are stal
The co-op runs the app against MariaDB 5.5.62, which sits below the version Prisma documents as supported. We tested it and it works. But three constraints follow from that version, and the next team should keep them in mind.

First, the schema is hand-created from our DDL with every `DATETIME(3)` changed to plain `TIMESTAMP`, so the production build drops `prisma migrate deploy` and there is no migration ledger on that server. Second, `TIMESTAMP` truncates sub-second precision and rejects dates past 2038-01-19, which is the column's ceiling. Third, the server has SSL disabled, so `DATABASE_URL` must not include `?ssl=true`. Add it and every connection fails with `ER_SERVER_SSL_DISABLED` and every database page returns 500.

## Repository access and handoff

This repo lives in the `hack4impact-calpoly` GitHub org, and GitHub does not let anyone self-assign admin. Each year, the Hack4Impact Cal Poly president adds the two incoming tech leads as admin collaborators on this repo.

The `develop` branch requires a reviewed pull request and a passing CI build to merge, and only repo admins can bypass that.

## Remaining co-op handoff items

Three operational items sit on the co-op's side, none of them in this codebase:

- The member portal's Referrals-tab link still points at the retired referral app and signs the `cs` parameter with the old scheme, so it errors when a member clicks it. Repoint it at the signed prfc-outreach referral URL.
- Profile photo upload needs a Vercel Blob store created in the project's Storage tab, which injects `BLOB_READ_WRITE_TOKEN`. Without it, uploads fail with "Vercel Blob: No token found."
- One test referral row remains in the production referral table and should be deleted.

The co-op owns the production accounts (the Vercel project, the MariaDB database, Brevo, Upstash). Their credentials and the account-ownership map stay outside version control, with the departing lead.
Loading