wcz-layout 8.1.0 → 8.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of wcz-layout might be problematic. Click here for more details.

Files changed (36) hide show
  1. package/dist/components.js +2 -2
  2. package/dist/components.js.map +1 -1
  3. package/dist/data/client.d.ts +1 -1
  4. package/dist/data/server.d.ts +1 -1
  5. package/dist/models.d.ts +4 -4
  6. package/dist/{peoplesoft-CFgBFvG-.d.ts → peoplesoft-kaMETchL.d.ts} +5 -5
  7. package/package.json +154 -154
  8. package/skills/client-db/SKILL.md +111 -0
  9. package/skills/db-schema/SKILL.md +69 -0
  10. package/skills/forms/SKILL.md +85 -0
  11. package/skills/routes/SKILL.md +127 -0
  12. package/skills/server/SKILL.md +114 -0
  13. package/skills/start/SKILL.md +46 -0
  14. package/skills/start/steps/01-git-setup.md +10 -0
  15. package/skills/start/steps/02-dependency-check.md +11 -0
  16. package/skills/start/steps/03-env-setup.md +16 -0
  17. package/skills/start/steps/04-project-name-setup.md +14 -0
  18. package/skills/start/steps/05-database-setup.md +37 -0
  19. package/skills/start/steps/06-entra-setup.md +59 -0
  20. package/skills/start/steps/07-vault-setup.md +56 -0
  21. package/skills/start/steps/08-generate-favicon.md +15 -0
  22. package/skills/start/steps/09-commit.md +15 -0
  23. package/skills/tables/SKILL.md +184 -0
  24. package/skills/api-routes/SKILL.md +0 -251
  25. package/skills/auth/SKILL.md +0 -268
  26. package/skills/data-grid/SKILL.md +0 -229
  27. package/skills/database-schema/SKILL.md +0 -182
  28. package/skills/dialogs-notifications/SKILL.md +0 -241
  29. package/skills/forms-validation/SKILL.md +0 -331
  30. package/skills/forms-validation/references/field-components.md +0 -212
  31. package/skills/layout-navigation/SKILL.md +0 -259
  32. package/skills/project-initialization/SKILL.md +0 -181
  33. package/skills/project-structure/SKILL.md +0 -157
  34. package/skills/tanstack-db-collections/SKILL.md +0 -270
  35. package/skills/template-init.md +0 -146
  36. package/skills/ui-pages/SKILL.md +0 -278
@@ -0,0 +1,184 @@
1
+ ---
2
+ name: tables
3
+ description: MUI X DataGrid Premium patterns — typed GridColDef, custom cell renderers (ChipInputCell, EditableColumnHeader) and row selection with toolbar actions.
4
+ ---
5
+
6
+ ## Rules
7
+
8
+ - Wrap DataGrid in `Fullscreen` when it is the only content on the page. Embedded grids use `autoHeight` instead.
9
+ - Always type `GridColDef<RowType>` with the row interface.
10
+ - Use `const { t } = useTranslation()` from `wcz-layout/hooks` for all `headerName` values.
11
+ - For editable columns, pair `editable: true` with `renderHeader: EditableColumnHeader`.
12
+ - For enums use `type: "singleSelect"` with `valueOptions` as `{ value, label }` objects
13
+ - Use properties: `showToolbar`, `loading`, `ignoreDiacritics`, `cellSelection`, `disableRowSelectionOnClick`.
14
+ - Use custom `slots={{ toolbar: DataGridToolbar }}` from `src/components` if available.
15
+
16
+ ## File Placement
17
+
18
+ ```
19
+ src/server/db/migrations/ — migrations
20
+ src/server/db/schemas/ — tables, enums, relations
21
+ src/lib/schemas/ — Zod schemas
22
+ wcz-layout/utils — shared utils from npm package
23
+ wcz-layout/components — shared components from npm package
24
+ @tanstack/react-db — TanStack DB Collections
25
+ ```
26
+
27
+ ## Examples
28
+
29
+ ```ts
30
+ // imports
31
+ import { useLiveQuery } from "@tanstack/react-db";
32
+ import { t } from "wcz-layout/utils";
33
+ import { ChipInputCell, Fullscreen, EditableColumnHeader } from "wcz-layout/components";
34
+ import { useTranslation } from "wcz-layout/hooks";
35
+ import { featureCollection } from "~/db-collections/featureCollection";
36
+ import { DataGridToolbar } from "~/components/DataGridToolbar";
37
+ import { statusEnum } from "~/server/db/schemas/feature";
38
+
39
+ // ChipInputCell (Renders array or single values as MUI Chips)
40
+ // Simple array of strings
41
+ { renderCell: (params) => <ChipInputCell params={params} /> }
42
+
43
+ // Complex objects with custom label extraction
44
+ {
45
+ renderCell: (params) => (
46
+ <ChipInputCell
47
+ params={params}
48
+ getLabel={(item) => item.displayName}
49
+ slotProps={{ color: "primary", size: "small" }}
50
+ />
51
+ ),
52
+ }
53
+
54
+ // EditableColumnHeader (Shows an edit icon next to the column name for editable columns)
55
+ {
56
+ field: "name",
57
+ headerName: t("Name"),
58
+ editable: true,
59
+ renderHeader: EditableColumnHeader,
60
+ }
61
+
62
+ // Enum column
63
+ {
64
+ field: "status",
65
+ headerName: t("Status"),
66
+ width: 160,
67
+ type: "singleSelect",
68
+ valueOptions: statusEnum.enumValues.map((status) => ({
69
+ value: status,
70
+ label: t(`Status.${status}`),
71
+ })),
72
+ },
73
+
74
+ // Integration with TanStack DB Collections
75
+ function ExampleGrid() {
76
+ const { data, isLoading } = useLiveQuery((q) =>
77
+ q
78
+ .from({ library: libraryCollection })
79
+ .orderBy(({ library }) => library.title, "desc"),
80
+ );
81
+
82
+ return (
83
+ <Fullscreen>
84
+ <DataGridPremium
85
+ rows={data}
86
+ columns={columns}
87
+ showToolbar
88
+ loading={isLoading}
89
+ ignoreDiacritics
90
+ cellSelection
91
+ disableRowSelectionOnClick
92
+ onRowDoubleClick={({ row }) => navigate({ to: "/feature/$id", params: { id: row.id } })}
93
+ slots={{ toolbar: DataGridToolbar }}
94
+ slotProps={{
95
+ toolbar: {
96
+ title: t("List"),
97
+ },
98
+ }}
99
+ />
100
+ </Fullscreen>
101
+ );
102
+ }
103
+
104
+ // CRUD operations
105
+ const [rowSelectionModel, setRowSelectionModel] = useState<GridRowSelectionModel>({
106
+ type: "include",
107
+ ids: new Set(),
108
+ });
109
+
110
+ const handleOnDelete = createOptimisticAction<Array<string>>({
111
+ onMutate: (ids) => {
112
+ ids.forEach((id) => {
113
+ featureCollection.delete(id);
114
+ });
115
+ },
116
+ mutationFn: async (ids) => {
117
+ deleteFeatures({ data: ids });
118
+ await featureCollection.utils.refetch();
119
+ },
120
+ });
121
+
122
+ return (
123
+ <DataGridPremium
124
+ // other props
125
+ rowSelectionModel={rowSelectionModel}
126
+ onRowSelectionModelChange={(newRowSelectionModel) =>
127
+ setRowSelectionModel(newRowSelectionModel)
128
+ }
129
+ slots={{ toolbar: DataGridToolbar }}
130
+ slotProps={{
131
+ toolbar: {
132
+ title: t("Feature.List"),
133
+ actions: [
134
+ <Tooltip key="create" title={t("Create")}>
135
+ <RouterIconButton to="/features/create">
136
+ <Add fontSize="small" />
137
+ </RouterIconButton>
138
+ </Tooltip>,
139
+ rowSelectionModel.ids.size === 1 && (
140
+ <Tooltip key="edit" title={t("Edit")}>
141
+ <RouterIconButton
142
+ to="/features/edit/$id"
143
+ params={{ id: Array.from(rowSelectionModel.ids)[0].toString() }}
144
+ >
145
+ <Edit fontSize="small" />
146
+ </RouterIconButton>
147
+ </Tooltip>
148
+ ),
149
+ rowSelectionModel.ids.size > 0 && (
150
+ <Tooltip key="delete" title={t("Delete")}>
151
+ <IconButton
152
+ onClick={async () => {
153
+ const confirmed = await confirm(
154
+ t("DeleteConfirmation", { count: rowSelectionModel.ids.size }),
155
+ );
156
+ if (confirmed) {
157
+ try {
158
+ const transaction = handleOnDelete(
159
+ Array.from(rowSelectionModel.ids).map((id) => id.toString()),
160
+ );
161
+ await transaction.isPersisted.promise;
162
+ setRowSelectionModel({ type: "include", ids: new Set() });
163
+ } catch (error) {
164
+ if (error instanceof Error) alert(error.message);
165
+ }
166
+ }
167
+ }}
168
+ >
169
+ <Badge
170
+ badgeContent={rowSelectionModel.ids.size}
171
+ invisible={rowSelectionModel.ids.size <= 1}
172
+ color="error"
173
+ >
174
+ <Delete fontSize="small" />
175
+ </Badge>
176
+ </IconButton>
177
+ </Tooltip>
178
+ ),
179
+ ],
180
+ },
181
+ }}
182
+ />
183
+ );
184
+ ```
@@ -1,251 +0,0 @@
1
- ---
2
- name: api-routes
3
- description: >
4
- Create TanStack Start file-based API routes with createFileRoute or
5
- createServerFn. Wire authorizationMiddleware(permissionKey) and
6
- validationMiddleware(schema) in server.middleware. Use createHandlers
7
- for CRUD with Drizzle queries. Response.json patterns. Covers the
8
- { middleware, handler } object shape for handlers needing validation.
9
- Activate when creating or modifying API endpoints or server functions.
10
- type: core
11
- library: wcz-layout
12
- library_version: "7.6.1"
13
- requires:
14
- - database-schema
15
- sources:
16
- - "wcz-layout:src/middleware/authMiddleware.ts"
17
- - "wcz-layout:src/middleware/validationMiddleware.ts"
18
- - "wcz-layout:src/routes/api/"
19
- ---
20
-
21
- # API Routes & Server Functions
22
-
23
- ## Setup
24
-
25
- API routes live under `src/routes/api/` and follow TanStack Router file-based conventions:
26
-
27
- ```
28
- src/routes/api/
29
- ├── health.ts # GET /api/health
30
- └── todos/
31
- ├── index.ts # GET /api/todos, POST /api/todos
32
- └── $id.ts # GET /api/todos/:id, PUT /api/todos/:id, DELETE /api/todos/:id
33
- ```
34
-
35
- ## Core Patterns
36
-
37
- ### Basic CRUD API route
38
-
39
- ```typescript
40
- // src/routes/api/todos/index.ts
41
- import { createFileRoute } from "@tanstack/react-router";
42
- import { createHandlers } from "@tanstack/start/api";
43
- import { authorizationMiddleware, validationMiddleware } from "wcz-layout/middleware";
44
- import { todoTable } from "~/lib/db/schemas/todo";
45
- import { TodoInsertSchema } from "~/schemas/todo";
46
-
47
- export const Route = createFileRoute("/api/todos")({
48
- server: {
49
- middleware: [authorizationMiddleware("all")],
50
- handlers: createHandlers({
51
- GET: async ({ context }) => {
52
- const items = await context.db.select().from(todoTable);
53
- return Response.json(items);
54
- },
55
- POST: {
56
- middleware: [validationMiddleware(TodoInsertSchema)],
57
- handler: async ({ context }) => {
58
- const [item] = await context.db.insert(todoTable).values(context.data).returning();
59
- return Response.json(item, { status: 201 });
60
- },
61
- },
62
- }),
63
- },
64
- });
65
- ```
66
-
67
- ### Route with path parameters
68
-
69
- ```typescript
70
- // src/routes/api/todos/$id.ts
71
- import { createFileRoute } from "@tanstack/react-router";
72
- import { createHandlers } from "@tanstack/start/api";
73
- import { eq } from "drizzle-orm";
74
- import { authorizationMiddleware, validationMiddleware } from "wcz-layout/middleware";
75
- import { todoTable } from "~/lib/db/schemas/todo";
76
- import { TodoSchema } from "~/schemas/todo";
77
-
78
- export const Route = createFileRoute("/api/todos/$id")({
79
- server: {
80
- middleware: [authorizationMiddleware("all")],
81
- handlers: createHandlers({
82
- GET: async ({ context, params }) => {
83
- const [item] = await context.db.select().from(todoTable).where(eq(todoTable.id, params.id));
84
- if (!item) return new Response(null, { status: 404 });
85
- return Response.json(item);
86
- },
87
- PUT: {
88
- middleware: [validationMiddleware(TodoSchema)],
89
- handler: async ({ context, params }) => {
90
- const [item] = await context.db
91
- .update(todoTable)
92
- .set(context.data)
93
- .where(eq(todoTable.id, params.id))
94
- .returning();
95
- return Response.json(item);
96
- },
97
- },
98
- DELETE: async ({ context, params }) => {
99
- await context.db.delete(todoTable).where(eq(todoTable.id, params.id));
100
- return new Response(null, { status: 204 });
101
- },
102
- }),
103
- },
104
- });
105
- ```
106
-
107
- ### Server functions (RPC-style)
108
-
109
- ```typescript
110
- import { createServerFn } from "@tanstack/start";
111
-
112
- const getTodos = createServerFn({ method: "GET" })
113
- .middleware([authorizationMiddleware("all")])
114
- .handler(async ({ context }) => {
115
- return context.db.select().from(todoTable);
116
- });
117
- ```
118
-
119
- Server functions are typed end-to-end. Use them for operations that don't need REST semantics.
120
-
121
- ### Middleware composition
122
-
123
- `authorizationMiddleware(permissionKey)` already includes `authenticationMiddleware` internally. It verifies the JWT token and injects `context.user` with a `User` object that has `hasPermission()`. If the user lacks the permission, it returns 403.
124
-
125
- `validationMiddleware(schema)` parses `request.json()` against the Zod schema and injects `context.data` with the typed result. On failure, returns 400 with the first field error.
126
-
127
- ## Common Mistakes
128
-
129
- ### HIGH Chaining authenticationMiddleware then authorizationMiddleware
130
-
131
- Wrong:
132
-
133
- ```typescript
134
- middleware: [authenticationMiddleware(), authorizationMiddleware("admin")],
135
- ```
136
-
137
- Correct:
138
-
139
- ```typescript
140
- middleware: [authorizationMiddleware("admin")],
141
- ```
142
-
143
- `authorizationMiddleware` already composes `authenticationMiddleware` internally. Chaining both duplicates JWT verification.
144
-
145
- Source: maintainer interview
146
-
147
- Cross-skill: See also skills/auth/SKILL.md § Common Mistakes
148
-
149
- ### HIGH Forgetting validationMiddleware for mutations
150
-
151
- Wrong:
152
-
153
- ```typescript
154
- POST: async ({ request, context }) => {
155
- const data = await request.json();
156
- await context.db.insert(todoTable).values(data).returning();
157
- },
158
- ```
159
-
160
- Correct:
161
-
162
- ```typescript
163
- POST: {
164
- middleware: [validationMiddleware(TodoInsertSchema)],
165
- handler: async ({ context }) => {
166
- const [item] = await context.db
167
- .insert(todoTable)
168
- .values(context.data)
169
- .returning();
170
- return Response.json(item, { status: 201 });
171
- },
172
- },
173
- ```
174
-
175
- POST/PUT handlers without `validationMiddleware` accept unvalidated JSON. The middleware parses `request.json()` with Zod and injects typed `context.data`.
176
-
177
- Source: wcz-layout:src/middleware/validationMiddleware.ts
178
-
179
- ### HIGH Returning data without Response.json wrapper
180
-
181
- Wrong:
182
-
183
- ```typescript
184
- GET: async ({ context }) => {
185
- return await context.db.select().from(todoTable);
186
- },
187
- ```
188
-
189
- Correct:
190
-
191
- ```typescript
192
- GET: async ({ context }) => {
193
- const items = await context.db.select().from(todoTable);
194
- return Response.json(items);
195
- },
196
- ```
197
-
198
- TanStack Start API route handlers must return `Response` objects. Returning raw objects doesn't set content-type headers correctly.
199
-
200
- Source: consumer project example
201
-
202
- ### MEDIUM Using plain function for POST instead of middleware object
203
-
204
- Wrong:
205
-
206
- ```typescript
207
- POST: async ({ request, context }) => {
208
- // no way to attach validationMiddleware
209
- },
210
- ```
211
-
212
- Correct:
213
-
214
- ```typescript
215
- POST: {
216
- middleware: [validationMiddleware(TodoInsertSchema)],
217
- handler: async ({ context }) => {
218
- // context.data is typed from schema
219
- },
220
- },
221
- ```
222
-
223
- When a handler needs per-method middleware (like validation), it must use the `{ middleware, handler }` object shape, not a plain async function.
224
-
225
- Source: consumer project example
226
-
227
- ### MEDIUM Using invalid permission key string
228
-
229
- `authorizationMiddleware(permissionKey)` is typed to `keyof typeof permissions` from your app's `permissions.ts`. Using a string not defined there causes a compile error.
230
-
231
- Source: wcz-layout:src/middleware/authMiddleware.ts
232
-
233
- ### HIGH Tension: Simplicity vs. full-stack rigor
234
-
235
- Quick prototypes may skip middleware to move fast. Always include `authorizationMiddleware` and `validationMiddleware` from the start — retrofitting security later is error-prone.
236
-
237
- See also: skills/project-initialization/SKILL.md § Common Mistakes
238
-
239
- ### HIGH Tension: Client-side first vs. server-side data
240
-
241
- `defaultSsr: false` means pages render on the client. Do not use SSR data fetching patterns (loader + dehydrate). API routes serve data to TanStack DB collections which manage client-side state.
242
-
243
- See also: skills/tanstack-db-collections/SKILL.md § Core Patterns
244
-
245
- ---
246
-
247
- See also:
248
-
249
- - skills/tanstack-db-collections/SKILL.md — Collections consume these API routes via axios.
250
- - skills/auth/SKILL.md — Every API route uses authorizationMiddleware.
251
- - skills/database-schema/SKILL.md — Schemas feed into validationMiddleware.
@@ -1,268 +0,0 @@
1
- ---
2
- name: auth
3
- description: >
4
- MSAL/Entra ID authentication and AD-group authorization. Use
5
- authorizationMiddleware(permissionKey) which includes authentication.
6
- Configure permissions.ts and scopes.ts for type-safe keys via
7
- virtual:wcz-layout. Client: getAccessToken(scopeKey). Server:
8
- getAppToken(scopeKey), getTokenOnBehalfOf(userToken, scopeKey).
9
- Route guards with requirePermission. Public routes via LayoutOptions.
10
- serverFnAccessTokenMiddleware in start.ts. Activate when configuring
11
- authentication, authorization, or token acquisition.
12
- type: core
13
- library: wcz-layout
14
- library_version: "7.6.1"
15
- sources:
16
- - "wcz-layout:src/middleware/authMiddleware.ts"
17
- - "wcz-layout:src/lib/auth/msalClient.ts"
18
- - "wcz-layout:src/lib/auth/msalServer.ts"
19
- - "wcz-layout:src/lib/auth/permissions.ts"
20
- - "wcz-layout:src/lib/auth/scopes.ts"
21
- ---
22
-
23
- # Authentication & Authorization
24
-
25
- ## Setup
26
-
27
- Two configuration files define the app's security surface. Both are consumed by the `viteWczLayout()` plugin via `virtual:wcz-layout` to generate TypeScript types:
28
-
29
- ```typescript
30
- // src/lib/auth/permissions.ts
31
- export const permissions = {
32
- admin: ["wcz-developers"],
33
- all: ["wcz-all-employees", "wscz-all-employees"],
34
- } as const;
35
- // keyof typeof permissions → "admin" | "all"
36
- ```
37
-
38
- ```typescript
39
- // src/lib/auth/scopes.ts
40
- export const scopes = {
41
- graph: ["User.Read"],
42
- api: ["api://wistron.com/your-app/access_as_user"],
43
- } as const;
44
- // keyof typeof scopes → "graph" | "api"
45
- ```
46
-
47
- Permission keys map to AD group names. A user has a permission if their JWT `groups` claim contains at least one group from the array.
48
-
49
- ## Core Patterns
50
-
51
- ### API route authorization
52
-
53
- ```typescript
54
- import { authorizationMiddleware } from "wcz-layout/middleware";
55
-
56
- export const Route = createFileRoute("/api/todos")({
57
- server: {
58
- middleware: [authorizationMiddleware("all")],
59
- handlers: createHandlers({
60
- GET: async ({ context }) => {
61
- // context.user is typed as User with hasPermission()
62
- return Response.json(await context.db.select().from(todoTable));
63
- },
64
- }),
65
- },
66
- });
67
- ```
68
-
69
- `authorizationMiddleware(permissionKey)`:
70
-
71
- 1. Verifies the JWT Bearer token (includes `authenticationMiddleware` internally)
72
- 2. Builds a `User` object with `hasPermission(key)` method
73
- 3. Returns 401 if token is invalid, 403 if user lacks the specified permission
74
- 4. Injects `context.user: User`
75
-
76
- ### Public (authentication-only) route
77
-
78
- For routes that need authentication but no specific permission:
79
-
80
- ```typescript
81
- import { authenticationMiddleware } from "wcz-layout/middleware";
82
-
83
- export const Route = createFileRoute("/api/profile")({
84
- server: {
85
- middleware: [authenticationMiddleware()],
86
- handlers: createHandlers({
87
- GET: async ({ context }) => {
88
- // context.user — any authenticated user
89
- return Response.json(context.user);
90
- },
91
- }),
92
- },
93
- });
94
- ```
95
-
96
- For optional authentication (public endpoints that may have user context):
97
-
98
- ```typescript
99
- middleware: [authenticationMiddleware({ optional: true })],
100
- // context.user is User | null
101
- ```
102
-
103
- ### Route-level permission guard
104
-
105
- ```typescript
106
- import { requirePermission } from "wcz-layout/utils";
107
-
108
- export const Route = createFileRoute("/admin/")({
109
- beforeLoad: requirePermission("admin"),
110
- component: AdminPage,
111
- });
112
- ```
113
-
114
- `requirePermission` runs in `beforeLoad` and redirects unauthorized users.
115
-
116
- ### Client-side token acquisition
117
-
118
- ```typescript
119
- import { getAccessToken } from "wcz-layout/utils";
120
-
121
- // In an axios interceptor or client-side code
122
- const token = await getAccessToken("api");
123
- ```
124
-
125
- `getAccessToken` is a client-only function (uses MSAL `PublicClientApplication`). It acquires tokens silently and triggers interaction listeners on `InteractionRequiredAuthError`.
126
-
127
- ### Server-side token acquisition
128
-
129
- ```typescript
130
- import { getAppToken, getTokenOnBehalfOf } from "wcz-layout/utils";
131
-
132
- // App-only token (client credentials flow — no user context)
133
- const appToken = await getAppToken("graph");
134
-
135
- // On-behalf-of flow (exchange user token for downstream API token)
136
- const oboToken = await getTokenOnBehalfOf(userBearerToken, "graph");
137
- ```
138
-
139
- Both are server-only functions using MSAL `ConfidentialClientApplication`.
140
-
141
- ### Global server function middleware
142
-
143
- ```typescript
144
- // src/start.ts
145
- import { serverFnAccessTokenMiddleware } from "wcz-layout/middleware";
146
-
147
- export const startInstance = createStart(() => ({
148
- defaultSsr: false,
149
- functionMiddleware: [serverFnAccessTokenMiddleware("api")],
150
- }));
151
- ```
152
-
153
- `serverFnAccessTokenMiddleware` attaches the Bearer token to all server function calls globally. Without it, server functions won't include authorization headers.
154
-
155
- ### Getting the current user
156
-
157
- ```typescript
158
- import { getUser } from "wcz-layout/utils";
159
-
160
- // Isomorphic — works on client (returns User | null) and server (returns null)
161
- const user = getUser();
162
- ```
163
-
164
- On the client, `getUser` reads the active MSAL account's ID token claims.
165
-
166
- ## Common Mistakes
167
-
168
- ### HIGH Chaining authenticationMiddleware before authorizationMiddleware
169
-
170
- Wrong:
171
-
172
- ```typescript
173
- middleware: [authenticationMiddleware(), authorizationMiddleware("admin")],
174
- ```
175
-
176
- Correct:
177
-
178
- ```typescript
179
- middleware: [authorizationMiddleware("admin")],
180
- ```
181
-
182
- `authorizationMiddleware` already composes `authenticationMiddleware` internally. Chaining both duplicates JWT verification.
183
-
184
- Source: maintainer interview
185
-
186
- Cross-skill: See also skills/api-routes/SKILL.md § Common Mistakes
187
-
188
- ### CRITICAL Calling getAccessToken on the server
189
-
190
- Wrong:
191
-
192
- ```typescript
193
- // In a server handler or serverFn
194
- const token = await getAccessToken("api");
195
- ```
196
-
197
- Correct:
198
-
199
- ```typescript
200
- // Server-side — use app token or OBO
201
- const token = await getAppToken("api");
202
- // or
203
- const token = await getTokenOnBehalfOf(userToken, "api");
204
- ```
205
-
206
- `getAccessToken` is `createClientOnlyFn` — it uses MSAL `PublicClientApplication` which only exists in the browser. Calling it on the server throws.
207
-
208
- Source: wcz-layout:src/lib/auth/msalClient.ts
209
-
210
- ### HIGH Forgetting serverFnAccessTokenMiddleware in start.ts
211
-
212
- Wrong:
213
-
214
- ```typescript
215
- // src/start.ts
216
- export const startInstance = createStart(() => ({
217
- defaultSsr: false,
218
- // Missing functionMiddleware
219
- }));
220
- ```
221
-
222
- Correct:
223
-
224
- ```typescript
225
- // src/start.ts
226
- export const startInstance = createStart(() => ({
227
- defaultSsr: false,
228
- functionMiddleware: [serverFnAccessTokenMiddleware("api")],
229
- }));
230
- ```
231
-
232
- Without `serverFnAccessTokenMiddleware`, server function calls won't include the Authorization header, causing 401 errors.
233
-
234
- Source: wcz-layout:src/start.ts
235
-
236
- ### MEDIUM Using string literals for permission/scope keys
237
-
238
- Wrong:
239
-
240
- ```typescript
241
- authorizationMiddleware("superadmin"); // not in permissions.ts → compile error
242
- getAccessToken("custom-api"); // not in scopes.ts → compile error
243
- ```
244
-
245
- Correct:
246
-
247
- ```typescript
248
- // First add to permissions.ts / scopes.ts, then use the typed key
249
- authorizationMiddleware("admin");
250
- getAccessToken("api");
251
- ```
252
-
253
- Permission and scope keys are typed from the `as const` exports via `virtual:wcz-layout`. Using arbitrary strings causes TypeScript compile errors.
254
-
255
- Source: wcz-layout:src/types/wcz-layout.d.ts
256
-
257
- ### HIGH Missing Entra redirect URI configuration
258
-
259
- The MSAL `redirectUri` defaults to `"/"`. The Entra app registration must include this exact redirect URI, or the authentication flow fails silently with a redirect error.
260
-
261
- Source: wcz-layout:src/routes/runbook/entra-id.tsx
262
-
263
- ---
264
-
265
- See also:
266
-
267
- - skills/api-routes/SKILL.md — Every API route uses authorizationMiddleware.
268
- - skills/layout-navigation/SKILL.md — publicRoutes bypass auth; permission guards need auth context.