wcz-layout
Version:
75 lines (60 loc) • 3.27 kB
Markdown
- Everything is public by default; protection is opt-in on both layers. The route guard only
hides UI, the middleware is the security boundary (see `routing` and `server-functions`).
- `requireAuth` redirects a signed-out user to `/auth/login?returnTo=…`; a signed-in user
without the permission gets an error, not a redirect.
- `hasPermission(user, key)` is for conditional UI only (navigation, buttons), never a guard.
- Read signed-in user from `Route.useRouteContext()` on client or `context.user` on server.
- `userMiddleware` gives `context.user: User | null` without throwing.
It is a single instance, deduped with `authMiddleware`; in server routes list both in the
same `middleware` array so the caller is resolved once.
- Permissions are evaluated per request, so moving an existing group between keys applies
immediately. A group name new to `permissions.ts` applies only after the user signs in
again (the cookie keeps only groups listed there).
- AWS users have no groups, so `requireAuth(key)` / `authMiddleware(key)` always reject them.
Use the bare form for routes AWS users must reach.
- Offline, guards use a cached user snapshot from `localStorage` — UI state only, the server still authorizes every call.
```ts
// src/lib/auth/permissions.ts — permission key -> group names (sAMAccountName)
import type { Permissions } from "wcz-layout/models";
export const permissions = {
admin: ["wcz-developers"],
} as const satisfies Permissions;
// src/lib/auth/scopes.ts — scope key -> one scope per API
import type { Scopes } from "wcz-layout/models";
export const scopes = {
graph: "User.Read", // bare string = Entra only
file: {
entra: "api://wistron.com/file-api/access_as_user",
aws: "https://file-api.wistron.com/access_as_user",
},
} as const satisfies Scopes;
```
| Gate | Route `beforeLoad` | Server function / API route |
| ----------------------- | ------------------ | --------------------------- |
| Anyone (default) | — | — or `userMiddleware` |
| Signed in, any provider | `requireAuth()` | `authMiddleware()` |
| Signed in + permission | `requireAuth(key)` | `authMiddleware(key)` |
From `wcz-layout/auth`, server-only (call inside a server function or server route):
- `getAccessToken(scopeKey)` — delegated token for the signed-in user from their session.
Requires a browser session (a Bearer caller has none). An expired session clears the cookie
and throws a 401 `Response`.
- `getAppToken(scopeKey)` — app-only Entra token (client credentials, `<resource>/.default`),
cached in memory. For background jobs and calls with no user.
- Downstream APIs must trust both issuers to accept AWS users; Cognito tokens have no `aud`
and are authorized by the `scope` claim.
```ts
import { createServerFn } from "@tanstack/react-start";
import { authMiddleware } from "wcz-layout/middleware";
export const getData = createServerFn()
.middleware([authMiddleware("admin")])
.handler(async () => {
// user is guaranteed to have "admin" permission
const user = context.user;
return { data: "Secret admin information" };
});
```