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
16 changes: 13 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 32 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 11 additions & 2 deletions docs/data-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down