diff --git a/AGENTS.md b/AGENTS.md index ca3aee8..6e93ec6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,9 +43,19 @@ requirements work - do not casually change them. weak spot is server-side N+1; do not call the database one row at a time from a field resolver. 4. **User input is never interpolated into SQL.** Use parameterised queries. -5. **Users are simulated** - a UUID in `localStorage`, sent with every - mutation. No authentication. This is allowed by the course and is enough to - persist reviews server-side. +5. **Users are simulated.** A UUID in `localStorage`, sent as an `X-User-Id` + header on **every** request, not just mutations. `myReview`, `isInCrate` and + `myCrate` are queries and need it just as much. The server reads the header + once into the GraphQL context, so no field in `schema.graphql` takes a + `userId` argument. No authentication. + - Missing or malformed id means **anonymous**: reads degrade to `null` or + `false`, mutations that need identity fail with a clear error. + - The context is **per request**. Hoisting the user id into a module-level + variable or a shared cache leaks one user's data into another. + - `displayName` is derived server-side from the UUID when the user row is + first created. That stops the client picking an arbitrary name, but it is + not an identity guarantee: the client can still send any UUID and write as + that user. See `docs/architecture.md` decision 5. 6. **Plain CSS.** No CSS framework, no component library. Smaller bundle for bærekraft, full control over focus rings and contrast for tilgjengelighet. 7. **Accessibility is not optional.** Every input needs a label or diff --git a/docs/architecture.md b/docs/architecture.md index 4692d8a..959dc7a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -81,8 +81,38 @@ simulated users. it with each request. The server reads it, so the `my...` fields in the schema need no user argument. -**Consequences:** Much less to build. There is no security, since anyone can -send another user's id, and clearing storage gives a new user. +**Transport.** One HTTP header, `X-User-Id`, set once by an Apollo link and +read once into the GraphQL context. Not a GraphQL variable: `myReview`, +`isInCrate` and `myCrate` would each need a `userId` argument, which would +thread `$userId` through every operation and make identity look like data when +it is transport. Not a cookie, because cookies imply sessions and CSRF and the +brief explicitly wants simulated users rather than login. Not an +`Authorization` header either, because nothing here is authenticated and +calling it that would be misleading. + +It goes on **every** request, not only mutations. Those three fields are +queries. + +**Missing or invalid.** Absent or malformed means anonymous. + +| State | `myReview` | `isInCrate` | `myCrate` | Mutations | +| ----------------- | ---------- | ----------- | ---------------- | ------------------------ | +| No header | `null` | `false` | empty connection | error: no client id | +| Malformed | `null` | `false` | empty connection | error: invalid client id | +| Valid, no row yet | `null` | `false` | empty connection | succeed, create the row | +| Valid, row exists | the review | as stored | the crate | succeed | + +Reads never fail because of identity state, so a bad header cannot take the +search page down. Writes require it and say so rather than attributing the +change to nobody. + +**Consequences:** Much less to build, and no security. Sending someone else's +UUID in `X-User-Id` writes as that user, so this separates visitors rather than +authenticating them. Deriving `displayName` from the UUID stops the client +choosing an arbitrary name string and stops a review claiming a display name +the sender did not hold, but it does **not** stop impersonation: the UUID is +the whole identity and it is client-supplied. Clearing storage gives a new +user. ## 6. Plain CSS, Apollo Client cache, URL as search state diff --git a/docs/data-model.md b/docs/data-model.md index 6bc4cd3..3708cb2 100644 --- a/docs/data-model.md +++ b/docs/data-model.md @@ -18,16 +18,25 @@ The two many-to-many relationships will use join tables in PostgreSQL in M2. ## User-generated data +- A **user** is a simulated identity: a UUID the client keeps in `localStorage` + and sends as an `X-User-Id` header on every request. There is no login. The + server derives a `displayName` from the UUID when it first creates the row, + so the client cannot choose an arbitrary name. It is still not + authentication, because the client can send any UUID. See + [`docs/architecture.md`](./architecture.md) decision 5. - A **review** belongs to one album and one simulated user. A user can have one review per album, so saving another review updates the existing one. Album - details can also return the current user's review to prefill an edit form. + details can also return the current user's review to prefill an edit form, + and each review carries the `User` who wrote it. - A **crate item** belongs to one simulated user and one album. It represents the user's personal wantlist. The schema has separate add and remove actions so repeated requests cannot accidentally switch the album back to the wrong state. The user identity and persistence of reviews and crate items are M2 work. The -schema already describes the operations the client will need. +schema already describes the operations the client will need. See +[`docs/architecture.md`](./architecture.md) decision 5 for how the identity +travels and what happens when it is missing. ## Search results diff --git a/schema.graphql b/schema.graphql index f8fc7f5..d7d1719 100644 --- a/schema.graphql +++ b/schema.graphql @@ -44,8 +44,14 @@ type Genre { name: String! } +type User { + id: ID! + displayName: String! +} + type Review { id: ID! + user: User! rating: Int! comment: String! createdAt: DateTime!