@chidchanun/bcp 0.2.4 → 0.2.6

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.
@@ -4,6 +4,11 @@ import {
4
4
  type AuthSession,
5
5
  type AuthUser,
6
6
  } from "./auth.js";
7
+ import {
8
+ hasPermission,
9
+ type AuthorizationMatch,
10
+ type PermissionRequirement,
11
+ } from "./authorization.js";
7
12
  import {
8
13
  redirect,
9
14
  type RedirectStatus,
@@ -18,21 +23,29 @@ export interface AuthGuardData<
18
23
 
19
24
  export interface RequireAuthOptions
20
25
  extends AuthOptions {
21
- redirectTo?:
22
- string |
23
- URL |
24
- null;
26
+ redirectTo?: string | URL | null;
27
+ redirectStatus?: RedirectStatus;
28
+ }
29
+
30
+ export interface RequireGuestOptions
31
+ extends AuthOptions {
32
+ redirectTo?: string | URL | null;
25
33
  redirectStatus?: RedirectStatus;
26
34
  }
27
35
 
28
36
  export interface RequireRoleOptions
29
37
  extends RequireAuthOptions {
30
38
  roleField?: string;
31
- match?: "any" | "all";
32
- forbiddenRedirectTo?:
33
- string |
34
- URL |
35
- null;
39
+ match?: AuthorizationMatch;
40
+ forbiddenRedirectTo?: string | URL | null;
41
+ forbiddenRedirectStatus?: RedirectStatus;
42
+ }
43
+
44
+ export interface RequirePermissionOptions
45
+ extends RequireAuthOptions {
46
+ permissionField?: string;
47
+ match?: AuthorizationMatch;
48
+ forbiddenRedirectTo?: string | URL | null;
36
49
  forbiddenRedirectStatus?: RedirectStatus;
37
50
  }
38
51
 
@@ -47,16 +60,20 @@ export type AuthGuardResult<
47
60
  | AuthGuardData<TUser, TData>
48
61
  | Response;
49
62
 
63
+ export type GuestGuardResult =
64
+ | Record<string, never>
65
+ | Response;
66
+
50
67
  export type AuthGuardFunction<
51
68
  TUser extends AuthUser = AuthUser,
52
69
  TData extends object = Record<string, never>
53
70
  > = () => Promise<
54
- AuthGuardResult<
55
- TUser,
56
- TData
57
- >
71
+ AuthGuardResult<TUser, TData>
58
72
  >;
59
73
 
74
+ export type GuestGuardFunction =
75
+ () => Promise<GuestGuardResult>;
76
+
60
77
  export async function requireAuth<
61
78
  TUser extends AuthUser = AuthUser,
62
79
  TData extends object = Record<string, never>
@@ -69,23 +86,18 @@ export async function requireAuth<
69
86
  ...authOptions
70
87
  } = options;
71
88
  const session =
72
- await auth<
73
- TUser,
74
- TData
75
- >(
89
+ await auth<TUser, TData>(
76
90
  authOptions
77
91
  );
78
92
 
79
93
  if (!session) {
80
94
  if (
81
- redirectTo ===
82
- null
95
+ redirectTo === null
83
96
  ) {
84
97
  return new Response(
85
98
  "Unauthorized",
86
99
  {
87
- status:
88
- 401,
100
+ status: 401,
89
101
  }
90
102
  );
91
103
  }
@@ -97,11 +109,47 @@ export async function requireAuth<
97
109
  }
98
110
 
99
111
  return {
100
- auth:
101
- session,
112
+ auth: session,
102
113
  };
103
114
  }
104
115
 
116
+ export async function requireGuest<
117
+ TUser extends AuthUser = AuthUser,
118
+ TData extends object = Record<string, never>
119
+ >(
120
+ options: RequireGuestOptions = {}
121
+ ): Promise<GuestGuardResult> {
122
+ const {
123
+ redirectTo = "/",
124
+ redirectStatus = 303,
125
+ ...authOptions
126
+ } = options;
127
+ const session =
128
+ await auth<TUser, TData>(
129
+ authOptions
130
+ );
131
+
132
+ if (!session) {
133
+ return {};
134
+ }
135
+
136
+ if (
137
+ redirectTo === null
138
+ ) {
139
+ return new Response(
140
+ "Already authenticated",
141
+ {
142
+ status: 409,
143
+ }
144
+ );
145
+ }
146
+
147
+ return redirect(
148
+ redirectTo,
149
+ redirectStatus
150
+ );
151
+ }
152
+
105
153
  export async function requireRole<
106
154
  TUser extends AuthUser = AuthUser,
107
155
  TData extends object = Record<string, never>
@@ -121,16 +169,12 @@ export async function requireRole<
121
169
  ...authOptions
122
170
  } = options;
123
171
  const authenticated =
124
- await requireAuth<
125
- TUser,
126
- TData
127
- >(
172
+ await requireAuth<TUser, TData>(
128
173
  authOptions
129
174
  );
130
175
 
131
176
  if (
132
- authenticated instanceof
133
- Response
177
+ authenticated instanceof Response
134
178
  ) {
135
179
  return authenticated;
136
180
  }
@@ -155,27 +199,59 @@ export async function requireRole<
155
199
  )
156
200
  );
157
201
 
158
- if (!allowed) {
159
- if (
160
- forbiddenRedirectTo !==
161
- null
162
- ) {
163
- return redirect(
164
- forbiddenRedirectTo,
165
- forbiddenRedirectStatus
166
- );
167
- }
202
+ return finalizeAuthorization(
203
+ allowed,
204
+ authenticated,
205
+ forbiddenRedirectTo,
206
+ forbiddenRedirectStatus
207
+ );
208
+ }
168
209
 
169
- return new Response(
170
- "Forbidden",
210
+ export async function requirePermission<
211
+ TUser extends AuthUser = AuthUser,
212
+ TData extends object = Record<string, never>
213
+ >(
214
+ requiredPermission:
215
+ PermissionRequirement,
216
+ options:
217
+ RequirePermissionOptions = {}
218
+ ): Promise<AuthGuardResult<TUser, TData>> {
219
+ const {
220
+ permissionField =
221
+ "permissions",
222
+ match = "any",
223
+ forbiddenRedirectTo = null,
224
+ forbiddenRedirectStatus = 303,
225
+ ...authOptions
226
+ } = options;
227
+ const authenticated =
228
+ await requireAuth<TUser, TData>(
229
+ authOptions
230
+ );
231
+
232
+ if (
233
+ authenticated instanceof Response
234
+ ) {
235
+ return authenticated;
236
+ }
237
+
238
+ const allowed =
239
+ hasPermission(
240
+ authenticated.auth.user,
241
+ requiredPermission,
171
242
  {
172
- status:
173
- 403,
243
+ field:
244
+ permissionField,
245
+ match,
174
246
  }
175
247
  );
176
- }
177
248
 
178
- return authenticated;
249
+ return finalizeAuthorization(
250
+ allowed,
251
+ authenticated,
252
+ forbiddenRedirectTo,
253
+ forbiddenRedirectStatus
254
+ );
179
255
  }
180
256
 
181
257
  export function createAuthGuard<
@@ -185,10 +261,19 @@ export function createAuthGuard<
185
261
  options: RequireAuthOptions = {}
186
262
  ): AuthGuardFunction<TUser, TData> {
187
263
  return () =>
188
- requireAuth<
189
- TUser,
190
- TData
191
- >(
264
+ requireAuth<TUser, TData>(
265
+ options
266
+ );
267
+ }
268
+
269
+ export function createGuestGuard<
270
+ TUser extends AuthUser = AuthUser,
271
+ TData extends object = Record<string, never>
272
+ >(
273
+ options: RequireGuestOptions = {}
274
+ ): GuestGuardFunction {
275
+ return () =>
276
+ requireGuest<TUser, TData>(
192
277
  options
193
278
  );
194
279
  }
@@ -201,31 +286,42 @@ export function createRoleGuard<
201
286
  options: RequireRoleOptions = {}
202
287
  ): AuthGuardFunction<TUser, TData> {
203
288
  return () =>
204
- requireRole<
205
- TUser,
206
- TData
207
- >(
289
+ requireRole<TUser, TData>(
208
290
  requiredRole,
209
291
  options
210
292
  );
211
293
  }
212
294
 
295
+ export function createPermissionGuard<
296
+ TUser extends AuthUser = AuthUser,
297
+ TData extends object = Record<string, never>
298
+ >(
299
+ requiredPermission:
300
+ PermissionRequirement,
301
+ options:
302
+ RequirePermissionOptions = {}
303
+ ): AuthGuardFunction<TUser, TData> {
304
+ return () =>
305
+ requirePermission<TUser, TData>(
306
+ requiredPermission,
307
+ options
308
+ );
309
+ }
310
+
213
311
  export function getGuardAuth<
214
312
  TUser extends AuthUser = AuthUser,
215
313
  TData extends object = Record<string, never>
216
314
  >(
217
- guardData:
218
- Readonly<{
219
- auth?: unknown;
220
- }>
315
+ guardData: Readonly<{
316
+ auth?: unknown;
317
+ }>
221
318
  ): AuthSession<TUser, TData> | null {
222
319
  const value =
223
320
  guardData.auth;
224
321
 
225
322
  if (
226
323
  !value ||
227
- typeof value !==
228
- "object" ||
324
+ typeof value !== "object" ||
229
325
  Array.isArray(
230
326
  value
231
327
  )
@@ -235,24 +331,17 @@ export function getGuardAuth<
235
331
 
236
332
  const session =
237
333
  value as Partial<
238
- AuthSession<
239
- TUser,
240
- TData
241
- >
334
+ AuthSession<TUser, TData>
242
335
  >;
243
336
 
244
337
  if (
245
- typeof session.sid !==
246
- "string" ||
247
- session.sid.trim().length ===
248
- 0 ||
249
- typeof session.iat !==
250
- "number" ||
338
+ typeof session.sid !== "string" ||
339
+ session.sid.trim().length === 0 ||
340
+ typeof session.iat !== "number" ||
251
341
  !Number.isFinite(
252
342
  session.iat
253
343
  ) ||
254
- typeof session.exp !==
255
- "number" ||
344
+ typeof session.exp !== "number" ||
256
345
  !Number.isFinite(
257
346
  session.exp
258
347
  ) ||
@@ -264,18 +353,47 @@ export function getGuardAuth<
264
353
  }
265
354
 
266
355
  return value as
267
- AuthSession<
268
- TUser,
269
- TData
270
- >;
356
+ AuthSession<TUser, TData>;
357
+ }
358
+
359
+ function finalizeAuthorization<
360
+ TUser extends AuthUser,
361
+ TData extends object
362
+ >(
363
+ allowed: boolean,
364
+ authenticated:
365
+ AuthGuardData<TUser, TData>,
366
+ forbiddenRedirectTo:
367
+ string | URL | null,
368
+ forbiddenRedirectStatus:
369
+ RedirectStatus
370
+ ): Promise<AuthGuardResult<TUser, TData>> | AuthGuardData<TUser, TData> | Response {
371
+ if (allowed) {
372
+ return authenticated;
373
+ }
374
+
375
+ if (
376
+ forbiddenRedirectTo !== null
377
+ ) {
378
+ return redirect(
379
+ forbiddenRedirectTo,
380
+ forbiddenRedirectStatus
381
+ );
382
+ }
383
+
384
+ return new Response(
385
+ "Forbidden",
386
+ {
387
+ status: 403,
388
+ }
389
+ );
271
390
  }
272
391
 
273
392
  function normalizeRequiredRoles(
274
393
  value: RequiredRole
275
394
  ): string[] {
276
395
  const roles =
277
- typeof value ===
278
- "string"
396
+ typeof value === "string"
279
397
  ? [
280
398
  value,
281
399
  ]
@@ -289,12 +407,10 @@ function normalizeRequiredRoles(
289
407
  );
290
408
 
291
409
  if (
292
- normalized.length ===
293
- 0 ||
410
+ normalized.length === 0 ||
294
411
  normalized.some(
295
412
  (role) =>
296
- role.length ===
297
- 0
413
+ role.length === 0
298
414
  )
299
415
  ) {
300
416
  throw new TypeError(
@@ -324,14 +440,12 @@ function readAssignedRoles(
324
440
 
325
441
  const value =
326
442
  (
327
- user as
328
- unknown as
443
+ user as unknown as
329
444
  Record<string, unknown>
330
445
  )[field];
331
446
 
332
447
  if (
333
- typeof value ===
334
- "string"
448
+ typeof value === "string"
335
449
  ) {
336
450
  const role =
337
451
  value.trim();
@@ -353,8 +467,7 @@ function readAssignedRoles(
353
467
  (
354
468
  role
355
469
  ): role is string =>
356
- typeof role ===
357
- "string"
470
+ typeof role === "string"
358
471
  )
359
472
  .map(
360
473
  (role) =>
@@ -373,8 +486,7 @@ function isGuardAuthUser(
373
486
  ): value is AuthUser {
374
487
  if (
375
488
  !value ||
376
- typeof value !==
377
- "object" ||
489
+ typeof value !== "object" ||
378
490
  Array.isArray(
379
491
  value
380
492
  )
@@ -389,13 +501,10 @@ function isGuardAuthUser(
389
501
  ).id;
390
502
 
391
503
  return (
392
- typeof id ===
393
- "string" &&
394
- id.trim().length >
395
- 0
504
+ typeof id === "string" &&
505
+ id.trim().length > 0
396
506
  ) || (
397
- typeof id ===
398
- "number" &&
507
+ typeof id === "number" &&
399
508
  Number.isFinite(
400
509
  id
401
510
  )