@crombolo/api-management 0.13.2 → 0.14.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.
- package/package.json +3 -3
- package/src/handlers/member.ts +1 -1
- package/src/handlers/user.ts +50 -9
- package/src/ports-users.ts +21 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crombolo/api-management",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "Umbraco-compatible Management API handlers for crombolo.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Crombolo",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"access": "public"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@crombolo/core": "0.
|
|
32
|
-
"@crombolo/contracts": "0.
|
|
31
|
+
"@crombolo/core": "0.14.1",
|
|
32
|
+
"@crombolo/contracts": "0.14.1"
|
|
33
33
|
}
|
|
34
34
|
}
|
package/src/handlers/member.ts
CHANGED
|
@@ -66,7 +66,7 @@ export function toSaveMember(key: string, body: Record<string, unknown>): SaveMe
|
|
|
66
66
|
oldPassword: typeof body.oldPassword === 'string' ? body.oldPassword : null,
|
|
67
67
|
isApproved: body.isApproved !== false,
|
|
68
68
|
isLockedOut: body.isLockedOut === true,
|
|
69
|
-
isTwoFactorEnabled: body.isTwoFactorEnabled
|
|
69
|
+
isTwoFactorEnabled: body.isTwoFactorEnabled !== false,
|
|
70
70
|
groupKeys: strings(body.groups),
|
|
71
71
|
values: readValues(body),
|
|
72
72
|
variants: readVariants(body),
|
package/src/handlers/user.ts
CHANGED
|
@@ -459,7 +459,8 @@ function registerUsers(router: ManagementApiRouter, users: UserPort): void {
|
|
|
459
459
|
}),
|
|
460
460
|
)
|
|
461
461
|
|
|
462
|
-
//
|
|
462
|
+
// Story 032: the authenticator provider, when the server offers one; any other
|
|
463
|
+
// provider name finds nothing, as an uninstalled provider does in Umbraco.
|
|
463
464
|
const noProvider = () =>
|
|
464
465
|
bad(
|
|
465
466
|
'Provider not found',
|
|
@@ -467,17 +468,57 @@ function registerUsers(router: ManagementApiRouter, users: UserPort): void {
|
|
|
467
468
|
'ProviderNameNotFound',
|
|
468
469
|
404,
|
|
469
470
|
)
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
471
|
+
const twoFactor = users.twoFactor
|
|
472
|
+
const provided = (ctx: RequestContext) =>
|
|
473
|
+
twoFactor && ctx.params.providerName === twoFactor.providerName ? twoFactor : undefined
|
|
474
|
+
const refused = (message: string) => bad('Invalid code', message, 'InvalidCode', 400)
|
|
475
|
+
router.handle('GetUserCurrent2fa', async (ctx) =>
|
|
476
|
+
Response.json(twoFactor ? await twoFactor.list(me(ctx).id) : []),
|
|
477
|
+
)
|
|
478
|
+
router.handle('GetUserCurrent2faByProviderName', async (ctx) => {
|
|
479
|
+
const provider = provided(ctx)
|
|
480
|
+
if (!provider) return noProvider()
|
|
481
|
+
const setup = provider.setup(me(ctx).email || me(ctx).userName)
|
|
482
|
+
return setup.ok
|
|
483
|
+
? Response.json({
|
|
484
|
+
$type: 'TwoFactorAuthInfo',
|
|
485
|
+
secret: setup.secret,
|
|
486
|
+
qrCodeSetupImageUrl: setup.qrCodeSetupImageUrl,
|
|
487
|
+
})
|
|
488
|
+
: bad('Two-factor sign-in is not available', setup.message, 'NotAvailable', 400)
|
|
489
|
+
})
|
|
490
|
+
router.handle('PostUserCurrent2faByProviderName', async (ctx) => {
|
|
491
|
+
const provider = provided(ctx)
|
|
492
|
+
if (!provider) return noProvider()
|
|
493
|
+
const b = await body(ctx)
|
|
494
|
+
const enabled = await provider.enable(me(ctx).id, str(b.code), str(b.secret))
|
|
495
|
+
return enabled.ok
|
|
496
|
+
? Response.json({ $type: 'TwoFactorRecoveryCodes', recoveryCodes: enabled.recoveryCodes })
|
|
497
|
+
: refused(enabled.message)
|
|
498
|
+
})
|
|
499
|
+
router.handle('DeleteUserCurrent2faByProviderName', async (ctx) => {
|
|
500
|
+
const provider = provided(ctx)
|
|
501
|
+
if (!provider) return noProvider()
|
|
502
|
+
const disabled = await provider.disable(me(ctx).id, ctx.url.searchParams.get('code') ?? '')
|
|
503
|
+
return disabled.ok ? ok() : refused(disabled.message)
|
|
504
|
+
})
|
|
474
505
|
router.handle('GetUserById2fa', async (ctx) => {
|
|
475
506
|
const found = await users.get(me(ctx), ctx.params.id as string)
|
|
476
|
-
|
|
507
|
+
if (found.status !== 'Success') return status(found.status)
|
|
508
|
+
return Response.json(twoFactor ? await twoFactor.list(ctx.params.id as string) : [])
|
|
477
509
|
})
|
|
478
|
-
router.handle('DeleteUserById2faByProviderName',
|
|
479
|
-
|
|
480
|
-
|
|
510
|
+
router.handle('DeleteUserById2faByProviderName', async (ctx) => {
|
|
511
|
+
const provider = provided(ctx)
|
|
512
|
+
if (!provider) return noProvider()
|
|
513
|
+
// Who may manage this user, by the same rule every user operation follows.
|
|
514
|
+
const found = await users.get(me(ctx), ctx.params.id as string)
|
|
515
|
+
if (found.status !== 'Success') return status(found.status)
|
|
516
|
+
await provider.reset(ctx.params.id as string)
|
|
517
|
+
return ok()
|
|
518
|
+
})
|
|
519
|
+
router.handle('GetUserCurrentLoginProviders', async (ctx) =>
|
|
520
|
+
Response.json((await users.loginProviders?.(me(ctx))) ?? []),
|
|
521
|
+
)
|
|
481
522
|
|
|
482
523
|
router.handle('PostSecurityForgotPassword', async (ctx) => {
|
|
483
524
|
await users.forgotPassword(str((await body(ctx)).email).trim())
|
package/src/ports-users.ts
CHANGED
|
@@ -86,7 +86,26 @@ export interface UserUpdateInput {
|
|
|
86
86
|
|
|
87
87
|
export type Result<T> = { status: 'Success'; value: T } | { status: Exclude<UserStatus, 'Success'> }
|
|
88
88
|
|
|
89
|
+
/** Two-factor sign-in for users (story 032): the one authenticator provider. */
|
|
90
|
+
export interface UserTwoFactorPort {
|
|
91
|
+
providerName: string
|
|
92
|
+
list(userKey: string): Promise<Array<{ providerName: string; isEnabledOnUser: boolean }>>
|
|
93
|
+
setup(
|
|
94
|
+
account: string,
|
|
95
|
+
): { ok: true; secret: string; qrCodeSetupImageUrl: string } | { ok: false; message: string }
|
|
96
|
+
enable(
|
|
97
|
+
userKey: string,
|
|
98
|
+
code: string,
|
|
99
|
+
secret: string,
|
|
100
|
+
): Promise<{ ok: true; recoveryCodes: string[] } | { ok: false; message: string }>
|
|
101
|
+
disable(userKey: string, code: string): Promise<{ ok: true } | { ok: false; message: string }>
|
|
102
|
+
/** An administrator turning it off for someone who lost their phone. */
|
|
103
|
+
reset(userKey: string): Promise<void>
|
|
104
|
+
}
|
|
105
|
+
|
|
89
106
|
export interface UserPort {
|
|
107
|
+
/** Two-factor sign-in, when the server offers it. */
|
|
108
|
+
twoFactor?: UserTwoFactorPort
|
|
90
109
|
list(principal: Principal, skip: number, take: number): Promise<Page<UserResponse>>
|
|
91
110
|
filter(principal: Principal, query: UserFilter): Promise<Page<UserResponse>>
|
|
92
111
|
get(principal: Principal, key: string): Promise<Result<UserResponse>>
|
|
@@ -138,6 +157,8 @@ export interface UserPort {
|
|
|
138
157
|
): Promise<Array<{ nodeKey: string; permissions: string[] }>>
|
|
139
158
|
configuration(): ResponseOf<'GetUserConfiguration'>
|
|
140
159
|
currentConfiguration(): ResponseOf<'GetUserCurrentConfiguration'>
|
|
160
|
+
/** The backoffice identity providers, and which the caller has signed in with (story 031). */
|
|
161
|
+
loginProviders?(principal: Principal): Promise<ResponseOf<'GetUserCurrentLoginProviders'>>
|
|
141
162
|
passwordConfiguration(): ResponseOf<'GetSecurityConfiguration'>['passwordConfiguration']
|
|
142
163
|
/** Anonymous: sends a reset link when the address belongs to a user; says nothing either way. */
|
|
143
164
|
forgotPassword(email: string): Promise<void>
|