@crombolo/api-management 0.0.0-stage → 0.9.1

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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +69 -0
  3. package/package.json +32 -4
  4. package/src/authorization.ts +453 -0
  5. package/src/handlers/content-type.ts +784 -0
  6. package/src/handlers/data-type.ts +267 -0
  7. package/src/handlers/dictionary.ts +245 -0
  8. package/src/handlers/document-blueprint.ts +193 -0
  9. package/src/handlers/document.ts +961 -0
  10. package/src/handlers/editor-services.ts +59 -0
  11. package/src/handlers/element.ts +492 -0
  12. package/src/handlers/file-systems.ts +207 -0
  13. package/src/handlers/import.ts +46 -0
  14. package/src/handlers/localization.ts +116 -0
  15. package/src/handlers/log-viewer.ts +125 -0
  16. package/src/handlers/manifest.ts +15 -0
  17. package/src/handlers/media.ts +453 -0
  18. package/src/handlers/member-group.ts +87 -0
  19. package/src/handlers/member.ts +189 -0
  20. package/src/handlers/models-builder.ts +29 -0
  21. package/src/handlers/package.ts +189 -0
  22. package/src/handlers/public-access.ts +107 -0
  23. package/src/handlers/published-cache.ts +27 -0
  24. package/src/handlers/redirects.ts +78 -0
  25. package/src/handlers/references.ts +44 -0
  26. package/src/handlers/server.ts +25 -0
  27. package/src/handlers/settings-stubs.ts +76 -0
  28. package/src/handlers/start-node-trees.ts +84 -0
  29. package/src/handlers/template-query.ts +191 -0
  30. package/src/handlers/template.ts +150 -0
  31. package/src/handlers/temporary-file.ts +62 -0
  32. package/src/handlers/user.ts +604 -0
  33. package/src/index.ts +17 -0
  34. package/src/ports-content.ts +564 -0
  35. package/src/ports-files.ts +51 -0
  36. package/src/ports-logs.ts +38 -0
  37. package/src/ports-members.ts +111 -0
  38. package/src/ports-models-builder.ts +26 -0
  39. package/src/ports-packages.ts +58 -0
  40. package/src/ports-users.ts +230 -0
  41. package/src/ports.ts +225 -0
  42. package/src/router.ts +216 -0
  43. package/src/routes.ts +99 -0
  44. package/src/server-events.ts +71 -0
  45. package/README.md +0 -3
@@ -0,0 +1,604 @@
1
+ /**
2
+ * Users, user groups, user data, the current user, and the anonymous invite
3
+ * and password-reset endpoints. Statuses map to responses as Umbraco's
4
+ * controllers map them, so the backoffice shows the same messages.
5
+ */
6
+ import {
7
+ invalidSkipTake,
8
+ notFound,
9
+ paged,
10
+ parseSkipTake,
11
+ problemDetails,
12
+ problemResponse,
13
+ } from '@crombolo/core'
14
+ import type { CurrentUserPort, UserItemPort } from '../ports.ts'
15
+ import type {
16
+ UserDataPort,
17
+ UserFilter,
18
+ UserGroupInput,
19
+ UserGroupPort,
20
+ UserGroupStatus,
21
+ UserOrder,
22
+ UserPort,
23
+ UserStatus,
24
+ UserUpdateInput,
25
+ } from '../ports-users.ts'
26
+ import type { ManagementApiRouter, Principal, RequestContext } from '../router.ts'
27
+ import { created } from './content-type.ts'
28
+
29
+ const V1 = '/umbraco/management/api/v1'
30
+
31
+ const bad = (title: string, detail: string, operationStatus: string, status = 400) =>
32
+ problemResponse(problemDetails({ title, detail, status, operationStatus }))
33
+
34
+ const USER_PROBLEMS: Partial<Record<UserStatus, [number, string, string]>> = {
35
+ UserNotFound: [404, 'The user was not found', 'The specified user was not found.'],
36
+ MissingUserGroup: [404, 'Missing User Group', 'The specified user group was not found.'],
37
+ NoUserGroup: [400, 'No User Group Specified', 'A user must be assigned to at least one group.'],
38
+ DuplicateUserName: [400, 'Duplicate Username', 'The username is already in use.'],
39
+ DuplicateEmail: [400, 'Duplicate Email', 'The email is already in use.'],
40
+ InvalidEmail: [400, 'Invalid email', 'The email is invalid.'],
41
+ InvalidUserName: [400, 'Invalid username', 'The username is invalid.'],
42
+ UserNameIsNotEmail: [400, 'Invalid Username', 'The username must be the same as the email.'],
43
+ CannotDeleteSelf: [400, 'Cannot delete', 'A user cannot delete itself.'],
44
+ CannotDisableSelf: [400, 'Cannot disable', 'A user cannot disable itself.'],
45
+ CannotDeleteUserWithLoginHistory: [
46
+ 400,
47
+ 'Cannot delete user',
48
+ 'The user has logged in and cannot be deleted; disable it instead.',
49
+ ],
50
+ CannotDisableInvitedUser: [
51
+ 400,
52
+ 'Cannot disable invited user',
53
+ 'An invited user cannot be disabled.',
54
+ ],
55
+ CannotInvite: [
56
+ 500,
57
+ 'Cannot send user invitation',
58
+ 'This site cannot send e-mail, so users cannot be invited. Configure an e-mail provider — CROMBOLO_EMAIL_PROVIDER, or `email` in crombolo.config.ts — or create the user and give them a password instead.',
59
+ ],
60
+ InvalidPassword: [
61
+ 400,
62
+ 'Invalid password',
63
+ 'The password does not meet the password requirements.',
64
+ ],
65
+ SelfOldPasswordRequired: [
66
+ 400,
67
+ 'Old password required',
68
+ 'The old password is required to change your own password.',
69
+ ],
70
+ InvalidIsoCode: [400, 'Invalid ISO code', 'The specified ISO code is invalid.'],
71
+ ContentStartNodeNotFound: [
72
+ 400,
73
+ 'Content Start Node not found',
74
+ 'Some content start nodes were not found.',
75
+ ],
76
+ MediaStartNodeNotFound: [
77
+ 400,
78
+ 'Media Start Node not found',
79
+ 'Some media start nodes were not found.',
80
+ ],
81
+ ElementStartNodeNotFound: [
82
+ 400,
83
+ 'Element Start Node not found',
84
+ 'Some element start nodes were not found.',
85
+ ],
86
+ InvalidInviteToken: [
87
+ 400,
88
+ 'Invalid verification token',
89
+ 'The specified verification token is invalid.',
90
+ ],
91
+ InvalidResetCode: [400, 'Invalid reset code', 'The reset code is invalid or has expired.'],
92
+ NotInInviteState: [400, 'Invalid user state', 'The user is not in the invited state.'],
93
+ AvatarFileNotFound: [400, 'Avatar file not found', 'The file key did not resolve in to a file.'],
94
+ InvalidAvatar: [400, 'Invalid avatar', 'The selected avatar is invalid.'],
95
+ InvalidUserType: [400, 'Invalid user type', 'Only API users can have client credentials.'],
96
+ DuplicateClientId: [400, 'Duplicate client ID', 'The client ID is already in use.'],
97
+ InvalidClientId: [
98
+ 400,
99
+ 'Invalid client ID',
100
+ 'The client ID must be prefixed with "crombolo-back-office-".',
101
+ ],
102
+ }
103
+
104
+ /** A user status as the response Umbraco sends; undefined for success. */
105
+ export function userStatusResponse(status: UserStatus): Response | undefined {
106
+ if (status === 'Success') return undefined
107
+ // Umbraco answers these with bare status codes
108
+ if (status === 'Unauthorized') return new Response(null, { status: 401 })
109
+ if (status === 'Forbidden') return new Response(null, { status: 403 })
110
+ const [code, title, detail] = USER_PROBLEMS[status] ?? [400, 'Unknown failure', status]
111
+ return bad(title, detail, status, code)
112
+ }
113
+
114
+ const GROUP_PROBLEMS: Partial<Record<UserGroupStatus, [number, string, string]>> = {
115
+ NotFound: [404, 'The user group could not be found', 'The user group could not be found.'],
116
+ UserNotFound: [404, 'User key not found', 'The user key was not found.'],
117
+ DuplicateAlias: [409, 'Duplicate alias', 'A user group already exists with the specified alias.'],
118
+ CanNotUpdateAliasIsSystemUserGroup: [
119
+ 400,
120
+ 'System user group',
121
+ 'The alias of a system user group cannot be changed.',
122
+ ],
123
+ IsSystemUserGroup: [400, 'System user group', 'A system user group cannot be deleted.'],
124
+ DocumentStartNodeKeyNotFound: [
125
+ 404,
126
+ 'Document start node key not found',
127
+ 'The document start node could not be found.',
128
+ ],
129
+ MediaStartNodeKeyNotFound: [
130
+ 404,
131
+ 'Media start node key not found',
132
+ 'The media start node could not be found.',
133
+ ],
134
+ ElementStartNodeKeyNotFound: [
135
+ 404,
136
+ 'Element start node key not found',
137
+ 'The element start node could not be found.',
138
+ ],
139
+ LanguageNotFound: [404, 'Language not found', 'A language could not be found.'],
140
+ NameTooLong: [400, 'Name too long', 'The user group name must be 200 characters or less.'],
141
+ AliasTooLong: [400, 'Alias too long', 'The user group alias must be 200 characters or less.'],
142
+ MissingName: [400, 'Missing user group name.', 'The user group name is required.'],
143
+ AdminGroupCannotBeEmpty: [
144
+ 400,
145
+ 'Admin group cannot be empty',
146
+ 'The admin group must have at least one user.',
147
+ ],
148
+ }
149
+
150
+ function groupStatusResponse(status: UserGroupStatus): Response | undefined {
151
+ if (status === 'Success') return undefined
152
+ if (status === 'Forbidden') return new Response(null, { status: 403 })
153
+ if (status === 'Unauthorized')
154
+ return problemResponse(
155
+ problemDetails({
156
+ title: 'Unauthorized access',
157
+ detail:
158
+ 'The performing user does not have the necessary access to perform this operation. Check the log for details.',
159
+ status: 401,
160
+ operationStatus: 'Unauthorized',
161
+ }),
162
+ )
163
+ const [code, title, detail] = GROUP_PROBLEMS[status] ?? [400, 'Unknown failure', status]
164
+ return bad(title, detail, status, code)
165
+ }
166
+
167
+ const ok = () => new Response(null, { status: 200 })
168
+
169
+ async function body(ctx: RequestContext): Promise<Record<string, unknown>> {
170
+ return ((await ctx.request.json().catch(() => ({}))) ?? {}) as Record<string, unknown>
171
+ }
172
+
173
+ const refId = (value: unknown): string | null => {
174
+ const id = (value as { id?: unknown } | null | undefined)?.id
175
+ return typeof id === 'string' ? id : null
176
+ }
177
+ const refIds = (value: unknown): string[] =>
178
+ Array.isArray(value) ? value.map(refId).filter((id): id is string => id !== null) : []
179
+ const str = (value: unknown) => (typeof value === 'string' ? value : '')
180
+
181
+ function skipTake(ctx: RequestContext): { skip: number; take: number } | Response {
182
+ const parsed = parseSkipTake(ctx.url.searchParams)
183
+ return parsed.ok ? parsed.value : problemResponse(invalidSkipTake())
184
+ }
185
+
186
+ const me = (ctx: RequestContext) => ctx.principal as Principal
187
+
188
+ function readGroup(b: Record<string, unknown>): UserGroupInput {
189
+ return {
190
+ key: typeof b.id === 'string' ? b.id : undefined,
191
+ name: str(b.name).trim(),
192
+ alias: str(b.alias).trim(),
193
+ description: typeof b.description === 'string' ? b.description : null,
194
+ icon: typeof b.icon === 'string' ? b.icon : null,
195
+ sections: Array.isArray(b.sections) ? b.sections.filter((s) => typeof s === 'string') : [],
196
+ languages: Array.isArray(b.languages) ? b.languages.filter((s) => typeof s === 'string') : [],
197
+ hasAccessToAllLanguages: b.hasAccessToAllLanguages === true,
198
+ documentStartNode: refId(b.documentStartNode),
199
+ documentRootAccess: b.documentRootAccess === true,
200
+ mediaStartNode: refId(b.mediaStartNode),
201
+ mediaRootAccess: b.mediaRootAccess === true,
202
+ elementStartNode: refId(b.elementStartNode),
203
+ elementRootAccess: b.elementRootAccess === true,
204
+ fallbackPermissions: Array.isArray(b.fallbackPermissions)
205
+ ? b.fallbackPermissions.filter((s) => typeof s === 'string')
206
+ : [],
207
+ permissions: (Array.isArray(b.permissions)
208
+ ? b.permissions
209
+ : []) as UserGroupInput['permissions'],
210
+ }
211
+ }
212
+
213
+ export function registerUserHandlers(
214
+ router: ManagementApiRouter,
215
+ currentUser: CurrentUserPort,
216
+ items?: UserItemPort,
217
+ ports: { users?: UserPort; groups?: UserGroupPort; userData?: UserDataPort } = {},
218
+ ): void {
219
+ router.handle('GetUserCurrent', async (ctx) => Response.json(await currentUser.get(me(ctx))))
220
+
221
+ if (items)
222
+ router.handle('GetItemUser', async (ctx) => {
223
+ const found = await items.items(ctx.url.searchParams.getAll('id'))
224
+ return Response.json(
225
+ found.map((user) => ({
226
+ id: user.key,
227
+ name: user.name,
228
+ avatarUrls: user.avatarUrls,
229
+ kind: user.kind ?? 'Default',
230
+ flags: [],
231
+ })),
232
+ )
233
+ })
234
+
235
+ const { users, groups, userData } = ports
236
+ if (users) registerUsers(router, users)
237
+ if (groups) registerGroups(router, groups)
238
+ if (userData) registerUserData(router, userData)
239
+ }
240
+
241
+ function registerUsers(router: ManagementApiRouter, users: UserPort): void {
242
+ const status = (s: UserStatus) => userStatusResponse(s) ?? ok()
243
+
244
+ router.handle('GetUserConfiguration', () => Response.json(users.configuration()))
245
+ router.handle('GetUserCurrentConfiguration', () => Response.json(users.currentConfiguration()))
246
+ router.handle('GetSecurityConfiguration', () =>
247
+ Response.json({ passwordConfiguration: users.passwordConfiguration() }),
248
+ )
249
+
250
+ router.handle('GetUser', async (ctx) => {
251
+ const paging = skipTake(ctx)
252
+ if (paging instanceof Response) return paging
253
+ return Response.json(await users.list(me(ctx), paging.skip, paging.take))
254
+ })
255
+
256
+ router.handle('GetFilterUser', async (ctx) => {
257
+ const paging = skipTake(ctx)
258
+ if (paging instanceof Response) return paging
259
+ const q = ctx.url.searchParams
260
+ const query: UserFilter = {
261
+ filter: q.get('filter') ?? undefined,
262
+ groupKeys: q.getAll('userGroupIds'),
263
+ states: q.getAll('userStates'),
264
+ orderBy: (q.get('orderBy') ?? 'UserName') as UserOrder,
265
+ direction: q.get('orderDirection') === 'Descending' ? 'Descending' : 'Ascending',
266
+ ...paging,
267
+ }
268
+ return Response.json(await users.filter(me(ctx), query))
269
+ })
270
+
271
+ router.handle('GetUserById', async (ctx) => {
272
+ const found = await users.get(me(ctx), ctx.params.id as string)
273
+ return found.status === 'Success' ? Response.json(found.value) : status(found.status)
274
+ })
275
+
276
+ router.handle('GetUserBatch', async (ctx) => {
277
+ const found = await users.batch(me(ctx), ctx.url.searchParams.getAll('id'))
278
+ return Response.json({ total: found.length, items: found })
279
+ })
280
+
281
+ const readInput = (b: Record<string, unknown>) => ({
282
+ key: typeof b.id === 'string' ? b.id : undefined,
283
+ kind: b.kind === 'Api' ? ('Api' as const) : ('Default' as const),
284
+ email: str(b.email).trim(),
285
+ userName: str(b.userName).trim(),
286
+ name: str(b.name).trim(),
287
+ groupKeys: refIds(b.userGroupIds),
288
+ })
289
+
290
+ router.handle('PostUser', async (ctx) => {
291
+ const result = await users.create(me(ctx), readInput(await body(ctx)))
292
+ if (result.status !== 'Success') return status(result.status)
293
+ return created(`${ctx.url.origin}${V1}/user/${result.value}`, result.value)
294
+ })
295
+
296
+ router.handle('PostUserInvite', async (ctx) => {
297
+ const b = await body(ctx)
298
+ const result = await users.invite(
299
+ me(ctx),
300
+ readInput(b),
301
+ typeof b.message === 'string' ? b.message : null,
302
+ )
303
+ if (result.status !== 'Success') return status(result.status)
304
+ return created(`${ctx.url.origin}${V1}/user/${result.value}`, result.value)
305
+ })
306
+
307
+ router.handle('PostUserInviteResend', async (ctx) => {
308
+ const b = await body(ctx)
309
+ return status(
310
+ await users.resendInvite(
311
+ me(ctx),
312
+ refId(b.user) ?? '',
313
+ typeof b.message === 'string' ? b.message : null,
314
+ ),
315
+ )
316
+ })
317
+
318
+ const passwordConfiguration = () =>
319
+ Response.json({ passwordConfiguration: users.passwordConfiguration() })
320
+
321
+ router.handle('PostUserInviteVerify', async (ctx) => {
322
+ const b = await body(ctx)
323
+ const result = await users.verifyInvite(refId(b.user) ?? '', str(b.token))
324
+ return userStatusResponse(result) ?? passwordConfiguration()
325
+ })
326
+
327
+ router.handle('PostUserInviteCreatePassword', async (ctx) => {
328
+ const b = await body(ctx)
329
+ return status(
330
+ await users.createInitialPassword(refId(b.user) ?? '', str(b.token), str(b.password)),
331
+ )
332
+ })
333
+
334
+ router.handle('PutUserById', async (ctx) => {
335
+ const b = await body(ctx)
336
+ const input: UserUpdateInput = {
337
+ email: str(b.email).trim(),
338
+ userName: str(b.userName).trim(),
339
+ name: str(b.name).trim(),
340
+ languageIsoCode: str(b.languageIsoCode),
341
+ groupKeys: refIds(b.userGroupIds),
342
+ documentStartNodeKeys: refIds(b.documentStartNodeIds),
343
+ hasDocumentRootAccess: b.hasDocumentRootAccess === true,
344
+ mediaStartNodeKeys: refIds(b.mediaStartNodeIds),
345
+ hasMediaRootAccess: b.hasMediaRootAccess === true,
346
+ elementStartNodeKeys: refIds(b.elementStartNodeIds),
347
+ hasElementRootAccess: b.hasElementRootAccess === true,
348
+ }
349
+ return status(await users.update(me(ctx), ctx.params.id as string, input))
350
+ })
351
+
352
+ router.handle('DeleteUserById', async (ctx) =>
353
+ status(await users.delete(me(ctx), [ctx.params.id as string])),
354
+ )
355
+ router.handle('DeleteUser', async (ctx) =>
356
+ status(await users.delete(me(ctx), refIds((await body(ctx)).userIds))),
357
+ )
358
+ router.handle('PostUserDisable', async (ctx) =>
359
+ status(await users.setApproved(me(ctx), refIds((await body(ctx)).userIds), false)),
360
+ )
361
+ router.handle('PostUserEnable', async (ctx) =>
362
+ status(await users.setApproved(me(ctx), refIds((await body(ctx)).userIds), true)),
363
+ )
364
+ router.handle('PostUserUnlock', async (ctx) =>
365
+ status(await users.unlock(me(ctx), refIds((await body(ctx)).userIds))),
366
+ )
367
+ router.handle('PostUserSetUserGroups', async (ctx) => {
368
+ const b = await body(ctx)
369
+ return status(await users.setGroups(me(ctx), refIds(b.userIds), refIds(b.userGroupIds)))
370
+ })
371
+
372
+ router.handle('PostUserByIdChangePassword', async (ctx) =>
373
+ status(
374
+ await users.changePassword(
375
+ me(ctx),
376
+ ctx.params.id as string,
377
+ str((await body(ctx)).newPassword),
378
+ null,
379
+ ),
380
+ ),
381
+ )
382
+ router.handle('PostUserCurrentChangePassword', async (ctx) => {
383
+ const b = await body(ctx)
384
+ return status(
385
+ await users.changePassword(
386
+ me(ctx),
387
+ me(ctx).id,
388
+ str(b.newPassword),
389
+ typeof b.oldPassword === 'string' ? b.oldPassword : null,
390
+ ),
391
+ )
392
+ })
393
+
394
+ router.handle('PostUserByIdResetPassword', async (ctx) => {
395
+ const result = await users.resetPassword(me(ctx), ctx.params.id as string)
396
+ return result.status === 'Success'
397
+ ? Response.json({ resetPassword: result.value })
398
+ : status(result.status)
399
+ })
400
+
401
+ router.handle('PostUserAvatarById', async (ctx) =>
402
+ status(
403
+ await users.setAvatar(me(ctx), ctx.params.id as string, refId((await body(ctx)).file) ?? ''),
404
+ ),
405
+ )
406
+ router.handle('DeleteUserAvatarById', async (ctx) =>
407
+ status(await users.removeAvatar(me(ctx), ctx.params.id as string)),
408
+ )
409
+ router.handle('PostUserCurrentAvatar', async (ctx) =>
410
+ status(await users.setAvatar(me(ctx), me(ctx).id, refId((await body(ctx)).file) ?? '')),
411
+ )
412
+ router.handle('DeleteUserCurrentAvatar', async (ctx) =>
413
+ status(await users.removeAvatar(me(ctx), me(ctx).id)),
414
+ )
415
+
416
+ router.handle('GetUserByIdCalculateStartNodes', async (ctx) => {
417
+ const result = await users.calculateStartNodes(me(ctx), ctx.params.id as string)
418
+ return result.status === 'Success' ? Response.json(result.value) : status(result.status)
419
+ })
420
+
421
+ router.handle('GetUserByIdClientCredentials', async (ctx) => {
422
+ const result = await users.clientCredentials(me(ctx), ctx.params.id as string)
423
+ return result.status === 'Success' ? Response.json(result.value) : status(result.status)
424
+ })
425
+ router.handle('PostUserByIdClientCredentials', async (ctx) => {
426
+ const b = await body(ctx)
427
+ return status(
428
+ await users.addClientCredential(
429
+ me(ctx),
430
+ ctx.params.id as string,
431
+ str(b.clientId),
432
+ str(b.clientSecret),
433
+ ),
434
+ )
435
+ })
436
+ router.handle('DeleteUserByIdClientCredentialsByClientId', async (ctx) =>
437
+ status(
438
+ await users.removeClientCredential(
439
+ me(ctx),
440
+ ctx.params.id as string,
441
+ ctx.params.clientId as string,
442
+ ),
443
+ ),
444
+ )
445
+
446
+ router.handle('PutUserCurrentProfile', async (ctx) =>
447
+ status(await users.setLanguage(me(ctx), str((await body(ctx)).languageIsoCode))),
448
+ )
449
+
450
+ for (const [operation, tree] of [
451
+ ['GetUserCurrentPermissions', 'document'],
452
+ ['GetUserCurrentPermissionsDocument', 'document'],
453
+ ['GetUserCurrentPermissionsMedia', 'media'],
454
+ ['GetUserCurrentPermissionsElement', 'element'],
455
+ ] as const)
456
+ router.handle(operation, async (ctx) =>
457
+ Response.json({
458
+ permissions: await users.permissions(me(ctx), tree, ctx.url.searchParams.getAll('id')),
459
+ }),
460
+ )
461
+
462
+ // No two-factor providers are installed: every provider lookup finds nothing.
463
+ const noProvider = () =>
464
+ bad(
465
+ 'Provider not found',
466
+ 'No two-factor provider with that name is installed.',
467
+ 'ProviderNameNotFound',
468
+ 404,
469
+ )
470
+ router.handle('GetUserCurrent2fa', () => Response.json([]))
471
+ router.handle('GetUserCurrent2faByProviderName', noProvider)
472
+ router.handle('PostUserCurrent2faByProviderName', noProvider)
473
+ router.handle('DeleteUserCurrent2faByProviderName', noProvider)
474
+ router.handle('GetUserById2fa', async (ctx) => {
475
+ const found = await users.get(me(ctx), ctx.params.id as string)
476
+ return found.status === 'Success' ? Response.json([]) : status(found.status)
477
+ })
478
+ router.handle('DeleteUserById2faByProviderName', noProvider)
479
+ // External backoffice logins arrive with WP-6.8's providers.
480
+ router.handle('GetUserCurrentLoginProviders', () => Response.json([]))
481
+
482
+ router.handle('PostSecurityForgotPassword', async (ctx) => {
483
+ await users.forgotPassword(str((await body(ctx)).email).trim())
484
+ return ok()
485
+ })
486
+ router.handle('PostSecurityForgotPasswordVerify', async (ctx) => {
487
+ const b = await body(ctx)
488
+ const result = await users.verifyResetCode(refId(b.user) ?? '', str(b.resetCode))
489
+ return userStatusResponse(result) ?? passwordConfiguration()
490
+ })
491
+ router.handle('PostSecurityForgotPasswordReset', async (ctx) => {
492
+ const b = await body(ctx)
493
+ const result = await users.resetWithCode(refId(b.user) ?? '', str(b.resetCode), str(b.password))
494
+ return userStatusResponse(result) ?? new Response(null, { status: 204 })
495
+ })
496
+ }
497
+
498
+ function registerGroups(router: ManagementApiRouter, groups: UserGroupPort): void {
499
+ const status = (s: UserGroupStatus) => groupStatusResponse(s) ?? ok()
500
+
501
+ router.handle('GetUserGroup', async (ctx) => {
502
+ const paging = skipTake(ctx)
503
+ if (paging instanceof Response) return paging
504
+ return Response.json(await groups.list(paging.skip, paging.take))
505
+ })
506
+ router.handle('GetFilterUserGroup', async (ctx) => {
507
+ const paging = skipTake(ctx)
508
+ if (paging instanceof Response) return paging
509
+ return Response.json(
510
+ await groups.filter(
511
+ me(ctx),
512
+ ctx.url.searchParams.get('filter') ?? '',
513
+ paging.skip,
514
+ paging.take,
515
+ ),
516
+ )
517
+ })
518
+ router.handle('GetItemUserGroup', async (ctx) =>
519
+ Response.json(await groups.items(ctx.url.searchParams.getAll('id'))),
520
+ )
521
+ router.handle('GetUserGroupById', async (ctx) => {
522
+ const found = await groups.get(me(ctx), ctx.params.id as string)
523
+ if (found === 'NotFound') return status('NotFound')
524
+ if (found === 'Forbidden') return status('Forbidden')
525
+ return Response.json(found)
526
+ })
527
+ router.handle('PostUserGroup', async (ctx) => {
528
+ const result = await groups.create(me(ctx), readGroup(await body(ctx)))
529
+ if (result.status !== 'Success' || !result.key) return status(result.status)
530
+ return created(`${ctx.url.origin}${V1}/user-group/${result.key}`, result.key)
531
+ })
532
+ router.handle('PutUserGroupById', async (ctx) =>
533
+ status(await groups.update(me(ctx), ctx.params.id as string, readGroup(await body(ctx)))),
534
+ )
535
+ router.handle('DeleteUserGroupById', async (ctx) =>
536
+ status(await groups.delete(me(ctx), [ctx.params.id as string])),
537
+ )
538
+ router.handle('DeleteUserGroup', async (ctx) =>
539
+ status(await groups.delete(me(ctx), refIds((await body(ctx)).userGroupIds))),
540
+ )
541
+ // The body is a bare array of references
542
+ router.handle('PostUserGroupByIdUsers', async (ctx) =>
543
+ status(
544
+ await groups.addUsers(me(ctx), ctx.params.id as string, refIds(await ctx.request.json())),
545
+ ),
546
+ )
547
+ router.handle('DeleteUserGroupByIdUsers', async (ctx) =>
548
+ status(
549
+ await groups.removeUsers(me(ctx), ctx.params.id as string, refIds(await ctx.request.json())),
550
+ ),
551
+ )
552
+ }
553
+
554
+ function registerUserData(router: ManagementApiRouter, data: UserDataPort): void {
555
+ const operationStatus = (s: string, code: number) =>
556
+ new Response(JSON.stringify(s), {
557
+ status: code,
558
+ headers: { 'content-type': 'application/json' },
559
+ })
560
+
561
+ router.handle('GetUserData', async (ctx) => {
562
+ const paging = skipTake(ctx)
563
+ if (paging instanceof Response) return paging
564
+ const q = ctx.url.searchParams
565
+ const page = await data.list(
566
+ me(ctx),
567
+ { groups: q.getAll('groups'), identifiers: q.getAll('identifiers') },
568
+ paging.skip,
569
+ paging.take,
570
+ )
571
+ return Response.json(paged(page.items, page.total))
572
+ })
573
+ router.handle('GetUserDataById', async (ctx) => {
574
+ const found = await data.get(me(ctx), ctx.params.id as string)
575
+ if (!found) return problemResponse(notFound('User data not found'))
576
+ return Response.json({ group: found.group, identifier: found.identifier, value: found.value })
577
+ })
578
+ router.handle('PostUserData', async (ctx) => {
579
+ const b = await body(ctx)
580
+ const key = typeof b.key === 'string' ? b.key : crypto.randomUUID()
581
+ const result = await data.create(me(ctx), {
582
+ key,
583
+ group: str(b.group),
584
+ identifier: str(b.identifier),
585
+ value: str(b.value),
586
+ })
587
+ if (result !== 'Success') return operationStatus(result, 400)
588
+ return created(`${ctx.url.origin}${V1}/user-data/${key}`, key)
589
+ })
590
+ router.handle('PutUserData', async (ctx) => {
591
+ const b = await body(ctx)
592
+ const result = await data.update(me(ctx), {
593
+ key: str(b.key),
594
+ group: str(b.group),
595
+ identifier: str(b.identifier),
596
+ value: str(b.value),
597
+ })
598
+ return result === 'Success' ? ok() : operationStatus(result, 404)
599
+ })
600
+ router.handle('DeleteUserDataById', async (ctx) => {
601
+ const result = await data.delete(me(ctx), ctx.params.id as string)
602
+ return result === 'Success' ? ok() : operationStatus(result, 404)
603
+ })
604
+ }
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ export * from './authorization.ts'
2
+ export * from './handlers/content-type.ts'
3
+ export * from './handlers/package.ts'
4
+ export * from './handlers/settings-stubs.ts'
5
+ export * from './handlers/template-query.ts'
6
+ export * from './handlers/user.ts'
7
+ export * from './ports.ts'
8
+ export * from './ports-content.ts'
9
+ export * from './ports-files.ts'
10
+ export * from './ports-logs.ts'
11
+ export * from './ports-members.ts'
12
+ export * from './ports-models-builder.ts'
13
+ export * from './ports-packages.ts'
14
+ export * from './ports-users.ts'
15
+ export * from './router.ts'
16
+ export * from './routes.ts'
17
+ export * from './server-events.ts'