50 lines
2.3 KiB
Markdown
50 lines
2.3 KiB
Markdown
# Per-user data (server-side — mandatory)
|
|
|
|
Pair auth with the DB (see the `neon` skill). A regular Postgres driver has full
|
|
DB access, so **every** server function that touches per-user data must verify
|
|
the caller and scope rows to them. Use the prewired **`authMiddleware`**: it
|
|
resolves the same-origin session to a verified `context.userId` (and rejects
|
|
scripted cross-site/sibling requests) — no token threading:
|
|
|
|
```ts
|
|
import { createServerFn } from "@tanstack/react-start";
|
|
import { getSql } from "@/lib/db";
|
|
import { authMiddleware } from "@/lib/auth/middleware";
|
|
|
|
export const listTodos = createServerFn({ method: "GET" })
|
|
.middleware([authMiddleware])
|
|
.handler(async ({ context }) => {
|
|
const sql = await getSql();
|
|
// Type the row shape — a server fn's return must be provably serializable.
|
|
return sql<{ id: number; title: string; done: boolean }>`select id, title, done from todos where user_id = ${context.userId} order by id desc`;
|
|
});
|
|
|
|
// Inputs go through `.validator()` (the current API); the client passes `{ data }`:
|
|
export const addTodo = createServerFn({ method: "POST" })
|
|
.validator((title: string) => title.trim())
|
|
.middleware([authMiddleware])
|
|
.handler(async ({ context, data: title }) => {
|
|
if (!title) return;
|
|
const sql = await getSql();
|
|
await sql`insert into todos (user_id, title) values (${context.userId}, ${title})`;
|
|
});
|
|
// mutations must scope writes too: `... where id = ${id} and user_id = ${context.userId}`
|
|
```
|
|
|
|
Call these from **client code** (effects, event handlers, React Query) — that's
|
|
where `Sec-Fetch-Site: same-origin` holds:
|
|
|
|
```ts
|
|
useEffect(() => { listTodos().then(setTodos).catch(() => setTodos([])); }, []);
|
|
```
|
|
|
|
Semantics: signed out → the middleware throws `UnauthorizedError` (message
|
|
`"Unauthorized"`, `status` 401 — match it to send the visitor to sign-in), in the
|
|
live preview too (real auth). With auth disabled (`VITE_AUTH_ENABLED=false`) it
|
|
resolves the dev user (`"dev-user"`) in dev and preview only — the deployed flag
|
|
comes from the deployer (today always `"true"`), so deployed it rejects every
|
|
visitor — which is why an app without sign-in must not use the middleware at all
|
|
(see the `neon` skill). Keep `user_id` columns
|
|
`TEXT` (Better Auth uses text ids; the disabled dev user is `'dev-user'`). Never
|
|
trust a client-supplied user id — only the middleware / `requireUserId()` result.
|