@manablox/api-rpc 0.2.0 → 0.3.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.
- package/dist/index.d.ts +5971 -0
- package/dist/index.js +994 -0
- package/package.json +18 -11
- package/src/base.ts +0 -59
- package/src/context.ts +0 -70
- package/src/index.ts +0 -28
- package/src/routers/asset.ts +0 -96
- package/src/routers/content-type.ts +0 -117
- package/src/routers/content.ts +0 -266
- package/src/routers/menu.ts +0 -77
- package/src/routers/role.ts +0 -51
- package/src/routers/space.ts +0 -149
- package/src/routers/user.ts +0 -175
- package/src/routers/workflow.ts +0 -253
- package/src/schemas.ts +0 -40
- package/test/asset.test.ts +0 -144
- package/test/content.test.ts +0 -256
- package/test/helpers.ts +0 -163
- package/test/menu.test.ts +0 -192
- package/test/role.test.ts +0 -205
- package/test/space.test.ts +0 -170
- package/test/user.test.ts +0 -212
- package/test/workflow.test.ts +0 -259
- package/tsconfig.json +0 -1
- package/vitest.config.ts +0 -8
package/src/routers/menu.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { scoped } from '../base.js';
|
|
3
|
-
import { locale, machineName, uuid } from '../schemas.js';
|
|
4
|
-
|
|
5
|
-
const menuSchema = z.object({
|
|
6
|
-
name: z.string().min(1).max(200),
|
|
7
|
-
machineName,
|
|
8
|
-
description: z.string().max(2000).nullable().optional(),
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
/** One entry as the editor hands it over: a document by localization id, or a link. */
|
|
12
|
-
export interface MenuItemInputShape {
|
|
13
|
-
id?: string | undefined;
|
|
14
|
-
localizationId?: string | null | undefined;
|
|
15
|
-
label?: string | null | undefined;
|
|
16
|
-
url?: string | null | undefined;
|
|
17
|
-
children?: MenuItemInputShape[] | undefined;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const menuItemSchema: z.ZodType<MenuItemInputShape> = z.lazy(() =>
|
|
21
|
-
z.object({
|
|
22
|
-
id: uuid.optional(),
|
|
23
|
-
localizationId: uuid.nullable().optional(),
|
|
24
|
-
label: z.string().max(200).nullable().optional(),
|
|
25
|
-
url: z.string().max(2000).nullable().optional(),
|
|
26
|
-
children: z.array(menuItemSchema).max(500).optional(),
|
|
27
|
-
}),
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Menus. Each rule — the unique machine name, what an entry may point at — lives in
|
|
32
|
-
* `MenuService`; a procedure here is an input schema, a permission and one call.
|
|
33
|
-
*/
|
|
34
|
-
export const menuRouter = {
|
|
35
|
-
list: scoped('menu:read')
|
|
36
|
-
.input(z.object({ spaceId: uuid }))
|
|
37
|
-
.handler(async ({ input, context }) => context.menus.list(input.spaceId)),
|
|
38
|
-
|
|
39
|
-
/** The menu with its entries, each content entry resolved to the document in `locale`. */
|
|
40
|
-
get: scoped('menu:read')
|
|
41
|
-
.input(z.object({ spaceId: uuid, id: uuid, locale }))
|
|
42
|
-
.handler(async ({ input, context }) =>
|
|
43
|
-
context.menus.get(input.spaceId, input.id, input.locale),
|
|
44
|
-
),
|
|
45
|
-
|
|
46
|
-
create: scoped('menu:write')
|
|
47
|
-
.input(menuSchema.extend({ spaceId: uuid }))
|
|
48
|
-
.handler(async ({ input, context }) => context.menus.create(input)),
|
|
49
|
-
|
|
50
|
-
update: scoped('menu:write')
|
|
51
|
-
.input(menuSchema.partial().extend({ spaceId: uuid, id: uuid }))
|
|
52
|
-
.handler(async ({ input, context }) => {
|
|
53
|
-
const { spaceId, id, ...data } = input;
|
|
54
|
-
return context.menus.update(spaceId, id, data);
|
|
55
|
-
}),
|
|
56
|
-
|
|
57
|
-
delete: scoped('menu:write')
|
|
58
|
-
.input(z.object({ spaceId: uuid, id: uuid }))
|
|
59
|
-
.handler(async ({ input, context }) => {
|
|
60
|
-
await context.menus.delete(input.spaceId, input.id);
|
|
61
|
-
return { ok: true };
|
|
62
|
-
}),
|
|
63
|
-
|
|
64
|
-
/** Replaces the whole entry tree; the editor saves a menu as one document. */
|
|
65
|
-
setItems: scoped('menu:write')
|
|
66
|
-
.input(z.object({ spaceId: uuid, id: uuid, items: z.array(menuItemSchema).max(500) }))
|
|
67
|
-
.handler(async ({ input, context }) =>
|
|
68
|
-
context.menus.setItems(input.spaceId, input.id, input.items),
|
|
69
|
-
),
|
|
70
|
-
|
|
71
|
-
/** Menus a document is linked from, for the editor's hint. */
|
|
72
|
-
usedIn: scoped('menu:read')
|
|
73
|
-
.input(z.object({ spaceId: uuid, localizationId: uuid }))
|
|
74
|
-
.handler(async ({ input, context }) =>
|
|
75
|
-
context.menus.usedIn(input.spaceId, input.localizationId),
|
|
76
|
-
),
|
|
77
|
-
};
|
package/src/routers/role.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { authed, scoped } from '../base.js';
|
|
3
|
-
import { machineName, uuid } from '../schemas.js';
|
|
4
|
-
|
|
5
|
-
const roleSchema = z.object({
|
|
6
|
-
spaceId: uuid,
|
|
7
|
-
name: z.string().trim().min(1).max(100),
|
|
8
|
-
machineName,
|
|
9
|
-
description: z.string().max(500).nullable().optional(),
|
|
10
|
-
/** `space:write`, `content:read` for every type, `content:read:<typeId>` for one. */
|
|
11
|
-
permissions: z.array(z.string().max(120)).max(500),
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* The roles of a space. The rules — reserved names, grants that exist, a role nobody
|
|
16
|
-
* holds before it goes — live in `RoleService`; a procedure here is an input schema, a
|
|
17
|
-
* permission and one call.
|
|
18
|
-
*/
|
|
19
|
-
export const roleRouter = {
|
|
20
|
-
/** The permission catalogue, grouped the way the role editor lays it out. */
|
|
21
|
-
catalog: authed.handler(async ({ context }) => context.roles.catalog()),
|
|
22
|
-
|
|
23
|
-
list: scoped('role:read')
|
|
24
|
-
.input(z.object({ spaceId: uuid }))
|
|
25
|
-
.handler(async ({ input, context }) => context.roles.list(input.spaceId)),
|
|
26
|
-
|
|
27
|
-
get: scoped('role:read')
|
|
28
|
-
.input(z.object({ spaceId: uuid, id: uuid }))
|
|
29
|
-
.handler(async ({ input, context }) => context.roles.get(input.spaceId, input.id)),
|
|
30
|
-
|
|
31
|
-
create: scoped('role:write')
|
|
32
|
-
.input(roleSchema)
|
|
33
|
-
.handler(async ({ input, context }) => {
|
|
34
|
-
const { spaceId, ...data } = input;
|
|
35
|
-
return context.roles.create(spaceId, data);
|
|
36
|
-
}),
|
|
37
|
-
|
|
38
|
-
update: scoped('role:write')
|
|
39
|
-
.input(roleSchema.extend({ id: uuid }))
|
|
40
|
-
.handler(async ({ input, context }) => {
|
|
41
|
-
const { spaceId, id, ...data } = input;
|
|
42
|
-
return context.roles.update(spaceId, id, data);
|
|
43
|
-
}),
|
|
44
|
-
|
|
45
|
-
delete: scoped('role:write')
|
|
46
|
-
.input(z.object({ spaceId: uuid, id: uuid }))
|
|
47
|
-
.handler(async ({ input, context }) => {
|
|
48
|
-
await context.roles.delete(input.spaceId, input.id);
|
|
49
|
-
return { ok: true };
|
|
50
|
-
}),
|
|
51
|
-
};
|
package/src/routers/space.ts
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { authed, base, scoped, superadmin } from '../base.js';
|
|
3
|
-
import {
|
|
4
|
-
locale,
|
|
5
|
-
localeList,
|
|
6
|
-
machineName,
|
|
7
|
-
mimeTypePattern,
|
|
8
|
-
searchTerm,
|
|
9
|
-
spaceRole,
|
|
10
|
-
uuid,
|
|
11
|
-
} from '../schemas.js';
|
|
12
|
-
|
|
13
|
-
const spaceSchema = z.object({
|
|
14
|
-
name: z.string().min(1).max(200),
|
|
15
|
-
machineName,
|
|
16
|
-
description: z.string().nullable().optional(),
|
|
17
|
-
url: z.string().url(),
|
|
18
|
-
defaultLocale: locale.default('en'),
|
|
19
|
-
locales: localeList.default(['en']),
|
|
20
|
-
settings: z.record(z.string(), z.unknown()).optional(),
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Spaces and membership. Every rule — the locale invariant, the last-owner guard, the
|
|
25
|
-
* creator-owns-it grant — lives in `SpaceService`; a procedure here is an input schema,
|
|
26
|
-
* a permission and one call.
|
|
27
|
-
*/
|
|
28
|
-
export const spaceRouter = {
|
|
29
|
-
/**
|
|
30
|
-
* Only the spaces the caller is a member of — a superadmin sees all. A member's query
|
|
31
|
-
* is bounded by their memberships rather than by the instance, so a large multi-tenant
|
|
32
|
-
* install does not load every space to show someone their two.
|
|
33
|
-
*/
|
|
34
|
-
list: authed.handler(async ({ context }) => {
|
|
35
|
-
const { principal } = context;
|
|
36
|
-
const all =
|
|
37
|
-
principal.role === 'superadmin'
|
|
38
|
-
? await context.repos.spaces.all()
|
|
39
|
-
: await context.repos.spaces.findManyByIds(Object.keys(principal.spaces));
|
|
40
|
-
const allowed = principal.allowedSpaceIds;
|
|
41
|
-
return allowed ? all.filter((space) => allowed.includes(space.id)) : all;
|
|
42
|
-
}),
|
|
43
|
-
|
|
44
|
-
get: scoped('space:read')
|
|
45
|
-
.input(z.object({ spaceId: uuid }))
|
|
46
|
-
.handler(async ({ input, context }) => context.repos.spaces.findById(input.spaceId)),
|
|
47
|
-
|
|
48
|
-
create: superadmin
|
|
49
|
-
.input(spaceSchema)
|
|
50
|
-
.handler(async ({ input, context }) =>
|
|
51
|
-
context.spaces.create(input, context.principal?.userId ?? null),
|
|
52
|
-
),
|
|
53
|
-
|
|
54
|
-
update: scoped('space:write')
|
|
55
|
-
.input(spaceSchema.partial().extend({ spaceId: uuid }))
|
|
56
|
-
.handler(async ({ input, context }) => {
|
|
57
|
-
const { spaceId, ...data } = input;
|
|
58
|
-
return context.spaces.update(spaceId, data);
|
|
59
|
-
}),
|
|
60
|
-
|
|
61
|
-
delete: scoped('space:delete')
|
|
62
|
-
.input(z.object({ spaceId: uuid }))
|
|
63
|
-
.handler(async ({ input, context }) => {
|
|
64
|
-
await context.spaces.delete(input.spaceId);
|
|
65
|
-
return { ok: true };
|
|
66
|
-
}),
|
|
67
|
-
|
|
68
|
-
/** Nominates one document as the space's root; `null` clears it. */
|
|
69
|
-
setHome: scoped('space:write')
|
|
70
|
-
.input(z.object({ spaceId: uuid, contentId: uuid.nullable() }))
|
|
71
|
-
.handler(async ({ input, context }) => context.spaces.setHome(input.spaceId, input.contentId)),
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* The space's upload limits, each narrowing the instance's. `allowedMimeTypes` absent
|
|
75
|
-
* means the instance's list; empty means the same thing rather than "nothing".
|
|
76
|
-
*/
|
|
77
|
-
setAssetSettings: scoped('space:write')
|
|
78
|
-
.input(
|
|
79
|
-
z.object({
|
|
80
|
-
spaceId: uuid,
|
|
81
|
-
allowedMimeTypes: z.array(mimeTypePattern).max(50).optional(),
|
|
82
|
-
maxFileSize: z.number().int().positive().nullable().optional(),
|
|
83
|
-
}),
|
|
84
|
-
)
|
|
85
|
-
.handler(async ({ input, context }) =>
|
|
86
|
-
context.spaces.setAssetSettings(input.spaceId, {
|
|
87
|
-
...(input.allowedMimeTypes?.length ? { allowedMimeTypes: input.allowedMimeTypes } : {}),
|
|
88
|
-
...(input.maxFileSize ? { maxFileSize: input.maxFileSize } : {}),
|
|
89
|
-
}),
|
|
90
|
-
),
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* The whole space as one JSON document. `space:write` rather than `space:read` because
|
|
94
|
-
* an export is every field of every document in one file, regardless of who may read
|
|
95
|
-
* what.
|
|
96
|
-
*/
|
|
97
|
-
export: scoped('space:write')
|
|
98
|
-
.input(z.object({ spaceId: uuid }))
|
|
99
|
-
.handler(async ({ input, context }) => context.spaces.export(input.spaceId)),
|
|
100
|
-
|
|
101
|
-
/** Restores such a document into an instance that does not hold the space yet. Superadmin, because it creates a space. */
|
|
102
|
-
import: superadmin
|
|
103
|
-
.input(z.object({ payload: z.unknown() }))
|
|
104
|
-
.handler(async ({ input, context }) =>
|
|
105
|
-
context.spaces.import(input.payload, context.principal.userId),
|
|
106
|
-
),
|
|
107
|
-
|
|
108
|
-
members: scoped('user:read')
|
|
109
|
-
.input(z.object({ spaceId: uuid }))
|
|
110
|
-
.handler(async ({ input, context }) => context.spaces.members(input.spaceId)),
|
|
111
|
-
|
|
112
|
-
grant: scoped('user:write')
|
|
113
|
-
.input(z.object({ spaceId: uuid, userId: uuid, role: spaceRole }))
|
|
114
|
-
.handler(async ({ input, context }) => {
|
|
115
|
-
await context.spaces.grant(input.spaceId, input.userId, input.role);
|
|
116
|
-
return { ok: true };
|
|
117
|
-
}),
|
|
118
|
-
|
|
119
|
-
/** Users who are not yet members, for the add-member picker. */
|
|
120
|
-
candidates: scoped('user:write')
|
|
121
|
-
.input(z.object({ spaceId: uuid, search: searchTerm.optional() }))
|
|
122
|
-
.handler(async ({ input, context }) => context.spaces.candidates(input.spaceId, input.search)),
|
|
123
|
-
|
|
124
|
-
/** Grants the same role to several users at once, as the picker hands them over. */
|
|
125
|
-
addMembers: scoped('user:write')
|
|
126
|
-
.input(
|
|
127
|
-
z.object({
|
|
128
|
-
spaceId: uuid,
|
|
129
|
-
userIds: z.array(uuid).min(1).max(100),
|
|
130
|
-
role: spaceRole.default('editor'),
|
|
131
|
-
}),
|
|
132
|
-
)
|
|
133
|
-
.handler(async ({ input, context }) => ({
|
|
134
|
-
ok: true,
|
|
135
|
-
added: await context.spaces.addMembers(input.spaceId, input.userIds, input.role),
|
|
136
|
-
})),
|
|
137
|
-
|
|
138
|
-
revoke: scoped('user:write')
|
|
139
|
-
.input(z.object({ spaceId: uuid, userId: uuid }))
|
|
140
|
-
.handler(async ({ input, context }) => {
|
|
141
|
-
await context.spaces.revoke(input.spaceId, input.userId);
|
|
142
|
-
return { ok: true };
|
|
143
|
-
}),
|
|
144
|
-
|
|
145
|
-
/** Locales available for a space, for the editor's language switcher. */
|
|
146
|
-
locales: base
|
|
147
|
-
.input(z.object({ spaceId: uuid }))
|
|
148
|
-
.handler(async ({ input, context }) => context.spaces.locales(input.spaceId)),
|
|
149
|
-
};
|
package/src/routers/user.ts
DELETED
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
import { assertCan, effectiveGrants, MIN_PASSWORD_LENGTH, normaliseGrants } from '@manablox/auth';
|
|
2
|
-
import { ManabloxError } from '@manablox/core';
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
import { authed, base, superadmin } from '../base.js';
|
|
5
|
-
import { pagination, searchTerm, uuid } from '../schemas.js';
|
|
6
|
-
|
|
7
|
-
const instanceRole = z.enum(['superadmin', 'editor']);
|
|
8
|
-
const password = z.string().min(MIN_PASSWORD_LENGTH).max(200);
|
|
9
|
-
const email = z.string().email().max(320);
|
|
10
|
-
const displayName = z.string().trim().min(1).max(200);
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* The caller's own account and keys, and — for a superadmin — every account on the
|
|
14
|
-
* instance. The rules (no locking yourself out, one superadmin always remains) live in
|
|
15
|
-
* `UserService`; a procedure here is an input schema, a permission and one call.
|
|
16
|
-
*/
|
|
17
|
-
export const userRouter = {
|
|
18
|
-
me: authed.handler(async ({ context }) => {
|
|
19
|
-
const user = await context.repos.users.findById(context.principal.userId);
|
|
20
|
-
return user
|
|
21
|
-
? {
|
|
22
|
-
id: user.id,
|
|
23
|
-
email: user.email,
|
|
24
|
-
name: user.name,
|
|
25
|
-
image: user.image,
|
|
26
|
-
role: user.role,
|
|
27
|
-
spaces: context.principal.spaces,
|
|
28
|
-
// Space id → every grant held there, built-in roles resolved too, so the admin
|
|
29
|
-
// shows and hides controls from the same table the server enforces.
|
|
30
|
-
permissions: Object.fromEntries(
|
|
31
|
-
Object.keys(context.principal.spaces).map((spaceId) => [
|
|
32
|
-
spaceId,
|
|
33
|
-
effectiveGrants(context.principal, spaceId),
|
|
34
|
-
]),
|
|
35
|
-
),
|
|
36
|
-
}
|
|
37
|
-
: null;
|
|
38
|
-
}),
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Whether the instance still has no account at all. Public, because the login page
|
|
42
|
-
* needs it before anyone is signed in: it decides whether to offer "create the first
|
|
43
|
-
* account". Once one exists, sign-up is closed and every account is created here.
|
|
44
|
-
*/
|
|
45
|
-
setupNeeded: base.handler(async ({ context }) => ({
|
|
46
|
-
setupNeeded: (await context.repos.users.count()) === 0,
|
|
47
|
-
})),
|
|
48
|
-
|
|
49
|
-
list: superadmin
|
|
50
|
-
.input(pagination({ limit: 25, max: 100 }).extend({ search: searchTerm.optional() }))
|
|
51
|
-
.handler(async ({ input, context }) =>
|
|
52
|
-
context.users.list({ limit: input.limit, offset: input.offset }, input.search),
|
|
53
|
-
),
|
|
54
|
-
|
|
55
|
-
get: superadmin
|
|
56
|
-
.input(z.object({ userId: uuid }))
|
|
57
|
-
.handler(async ({ input, context }) => context.users.get(input.userId)),
|
|
58
|
-
|
|
59
|
-
create: superadmin
|
|
60
|
-
.input(
|
|
61
|
-
z.object({
|
|
62
|
-
name: displayName,
|
|
63
|
-
email,
|
|
64
|
-
password,
|
|
65
|
-
role: instanceRole.default('editor'),
|
|
66
|
-
}),
|
|
67
|
-
)
|
|
68
|
-
.handler(async ({ input, context }) => context.users.create(input)),
|
|
69
|
-
|
|
70
|
-
update: superadmin
|
|
71
|
-
.input(z.object({ userId: uuid, name: displayName.optional(), email: email.optional() }))
|
|
72
|
-
.handler(async ({ input, context }) => {
|
|
73
|
-
const { userId, ...data } = input;
|
|
74
|
-
return context.users.update(userId, data);
|
|
75
|
-
}),
|
|
76
|
-
|
|
77
|
-
setRole: superadmin
|
|
78
|
-
.input(z.object({ userId: uuid, role: instanceRole }))
|
|
79
|
-
.handler(async ({ input, context }) => context.users.setRole(input.userId, input.role)),
|
|
80
|
-
|
|
81
|
-
/** Resets a password and signs the account out everywhere. */
|
|
82
|
-
setPassword: superadmin
|
|
83
|
-
.input(z.object({ userId: uuid, password }))
|
|
84
|
-
.handler(async ({ input, context }) => {
|
|
85
|
-
await context.users.setPassword(input.userId, input.password);
|
|
86
|
-
return { ok: true };
|
|
87
|
-
}),
|
|
88
|
-
|
|
89
|
-
ban: superadmin
|
|
90
|
-
.input(z.object({ userId: uuid, reason: z.string().trim().max(500).optional() }))
|
|
91
|
-
.handler(async ({ input, context }) =>
|
|
92
|
-
context.users.ban(context.principal.userId, input.userId, input.reason || null),
|
|
93
|
-
),
|
|
94
|
-
|
|
95
|
-
unban: superadmin
|
|
96
|
-
.input(z.object({ userId: uuid }))
|
|
97
|
-
.handler(async ({ input, context }) => context.users.unban(input.userId)),
|
|
98
|
-
|
|
99
|
-
revokeSessions: superadmin
|
|
100
|
-
.input(z.object({ userId: uuid }))
|
|
101
|
-
.handler(async ({ input, context }) => {
|
|
102
|
-
await context.users.revokeSessions(input.userId);
|
|
103
|
-
return { ok: true };
|
|
104
|
-
}),
|
|
105
|
-
|
|
106
|
-
delete: superadmin.input(z.object({ userId: uuid })).handler(async ({ input, context }) => {
|
|
107
|
-
await context.users.delete(context.principal.userId, input.userId);
|
|
108
|
-
return { ok: true };
|
|
109
|
-
}),
|
|
110
|
-
|
|
111
|
-
apiKeys: authed.handler(async ({ context }) => context.apiKeys.list(context.principal.userId)),
|
|
112
|
-
|
|
113
|
-
issueApiKey: authed
|
|
114
|
-
.input(
|
|
115
|
-
z.object({
|
|
116
|
-
name: z.string().min(1).max(100),
|
|
117
|
-
expiresAt: z.coerce.date().optional(),
|
|
118
|
-
/** Empty or omitted issues an unrestricted key. */
|
|
119
|
-
spaceIds: z.array(uuid).optional(),
|
|
120
|
-
/**
|
|
121
|
-
* Grants the key is confined to, in the roles' vocabulary; omitted leaves the
|
|
122
|
-
* owner's role as the limit. At use the two are intersected, so a grant here
|
|
123
|
-
* never widens the key beyond its owner.
|
|
124
|
-
*/
|
|
125
|
-
permissions: z.array(z.string().max(120)).max(500).optional(),
|
|
126
|
-
}),
|
|
127
|
-
)
|
|
128
|
-
.handler(async ({ input, context }) => {
|
|
129
|
-
// A key may only be confined to spaces the issuer can already reach, so a
|
|
130
|
-
// restriction cannot be used to name a space the caller has no business knowing.
|
|
131
|
-
for (const spaceId of input.spaceIds ?? []) {
|
|
132
|
-
assertCan(context.principal, spaceId, 'space:read');
|
|
133
|
-
}
|
|
134
|
-
// A typed grant may name a type of any space the key will reach.
|
|
135
|
-
const reachable = input.spaceIds?.length
|
|
136
|
-
? input.spaceIds
|
|
137
|
-
: context.principal.role === 'superadmin'
|
|
138
|
-
? null
|
|
139
|
-
: Object.keys(context.principal.spaces);
|
|
140
|
-
const typeIds = new Set(
|
|
141
|
-
(reachable
|
|
142
|
-
? reachable.flatMap((spaceId) => context.manablox.contentTypes.forSpace(spaceId))
|
|
143
|
-
: context.manablox.contentTypes.all
|
|
144
|
-
).map((type) => type.id),
|
|
145
|
-
);
|
|
146
|
-
let permissions: string[] | null = null;
|
|
147
|
-
if (input.permissions) {
|
|
148
|
-
const checked = normaliseGrants(input.permissions, typeIds);
|
|
149
|
-
if (checked.unknown.length) {
|
|
150
|
-
throw ManabloxError.validation(
|
|
151
|
-
checked.unknown.map(({ index, grant }) => ({
|
|
152
|
-
key: 'role.permission.unknown' as const,
|
|
153
|
-
path: ['permissions', index],
|
|
154
|
-
params: { permission: grant },
|
|
155
|
-
})),
|
|
156
|
-
'apiKey.validation.failed',
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
permissions = checked.permissions;
|
|
160
|
-
}
|
|
161
|
-
return context.apiKeys.issue(context.principal.userId, input.name, {
|
|
162
|
-
expiresAt: input.expiresAt,
|
|
163
|
-
spaceIds: input.spaceIds,
|
|
164
|
-
permissions,
|
|
165
|
-
});
|
|
166
|
-
}),
|
|
167
|
-
|
|
168
|
-
revokeApiKey: authed.input(z.object({ id: uuid })).handler(async ({ input, context }) => {
|
|
169
|
-
// Scope the revoke to the caller's own keys so an id from elsewhere is inert.
|
|
170
|
-
const own = await context.apiKeys.list(context.principal.userId);
|
|
171
|
-
if (!own.some((key) => key.id === input.id)) return { ok: false };
|
|
172
|
-
await context.apiKeys.revoke(input.id);
|
|
173
|
-
return { ok: true };
|
|
174
|
-
}),
|
|
175
|
-
};
|