wcz-layout 7.6.1 → 7.6.2
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.
- package/dist/components/core/navigation/NavigationList.d.ts +4 -4
- package/dist/components/core/navigation/NavigationListItem.d.ts +3 -3
- package/dist/index.js.map +1 -1
- package/dist/models/Navigation.d.ts +23 -11
- package/package.json +8 -2
- package/skills/api-routes/SKILL.md +251 -0
- package/skills/auth/SKILL.md +268 -0
- package/skills/data-grid/SKILL.md +229 -0
- package/skills/database-schema/SKILL.md +182 -0
- package/skills/dialogs-notifications/SKILL.md +241 -0
- package/skills/forms-validation/SKILL.md +331 -0
- package/skills/forms-validation/references/field-components.md +212 -0
- package/skills/layout-navigation/SKILL.md +259 -0
- package/skills/project-initialization/SKILL.md +181 -0
- package/skills/project-structure/SKILL.md +157 -0
- package/skills/tanstack-db-collections/SKILL.md +270 -0
- package/skills/ui-pages/SKILL.md +278 -0
package/package.json
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wcz-layout",
|
|
3
|
-
"version": "7.6.
|
|
3
|
+
"version": "7.6.2",
|
|
4
4
|
"private": false,
|
|
5
|
+
"keywords": [
|
|
6
|
+
"tanstack-intent"
|
|
7
|
+
],
|
|
5
8
|
"files": [
|
|
6
|
-
"dist"
|
|
9
|
+
"dist",
|
|
10
|
+
"skills",
|
|
11
|
+
"!skills/_artifacts"
|
|
7
12
|
],
|
|
8
13
|
"type": "module",
|
|
9
14
|
"sideEffects": false,
|
|
@@ -87,6 +92,7 @@
|
|
|
87
92
|
},
|
|
88
93
|
"devDependencies": {
|
|
89
94
|
"@rolldown/plugin-babel": "^0.2.2",
|
|
95
|
+
"@tanstack/intent": "^0.0.23",
|
|
90
96
|
"@types/file-saver": "^2.0.7",
|
|
91
97
|
"@types/node": "^25.3.5",
|
|
92
98
|
"@types/react": "^19.2.14",
|
|
@@ -0,0 +1,251 @@
|
|
|
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.
|
|
@@ -0,0 +1,268 @@
|
|
|
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.
|