@teamblind/axeda-better-auth 0.0.0-stage → 0.1.0-alpha.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.
@@ -0,0 +1,247 @@
1
+ import { createAuthEndpoint, sessionMiddleware } from 'better-auth/api';
2
+ import { z } from 'zod';
3
+ import { toBusinessDto, toCompanyDto, toInvitationDto, toAssignmentDto } from './dto.js';
4
+ import { access, cursor, id, limit, pageOf, run, scopeBody, scopeFromQuery, scopeQuery, } from './endpoints.js';
5
+ import { toApiError } from './http-errors.js';
6
+ import { AxedaError } from '@teamblind/axeda-core';
7
+ const deleted = z.enum(['exclude', 'include', 'only']);
8
+ const version = z.number().int().min(1);
9
+ const status = z.enum(['active', 'inactive']);
10
+ const url = z.string().max(2048);
11
+ const slug = z.string().min(1).max(63);
12
+ const name = z.string().min(1).max(200);
13
+ const metadata = z.record(z.string().max(100), z.unknown());
14
+ /** Compañías y negocios. El permiso lo impone el caso de uso (ver `AxedaError` → 403 / 404). */
15
+ export function createTenantEndpoints(core) {
16
+ return {
17
+ axedaCompanyList: createAuthEndpoint('/axeda/company/list', {
18
+ method: 'GET',
19
+ use: [sessionMiddleware],
20
+ query: z.strictObject({
21
+ deleted: deleted.optional(),
22
+ cursor: cursor.optional(),
23
+ limit: limit.optional(),
24
+ }),
25
+ metadata: access('(membership)', 'per-target'),
26
+ }, (ctx) => run(ctx, core, async (axeda, actor) => {
27
+ const page = await axeda.companies.list(actor, { deleted: ctx.query.deleted }, pageOf(ctx.query));
28
+ return ctx.json({ items: page.items.map(toCompanyDto), nextCursor: page.nextCursor });
29
+ })),
30
+ axedaCompanyGet: createAuthEndpoint('/axeda/company/get', {
31
+ method: 'GET',
32
+ use: [sessionMiddleware],
33
+ query: z.strictObject({ id }),
34
+ metadata: access('(membership)', 'per-target'),
35
+ }, (ctx) => run(ctx, core, async (axeda, actor) => ctx.json(toCompanyDto(await axeda.companies.get(actor, ctx.query.id))))),
36
+ axedaCompanyCreate: createAuthEndpoint('/axeda/company/create', {
37
+ method: 'POST',
38
+ use: [sessionMiddleware],
39
+ body: z.strictObject({
40
+ slug,
41
+ name,
42
+ legalName: name.nullish(),
43
+ logoUrl: url.nullish(),
44
+ faviconUrl: url.nullish(),
45
+ metadata: metadata.optional(),
46
+ }),
47
+ metadata: access('axeda:company:manage', 'global'),
48
+ }, (ctx) => run(ctx, core, async (axeda, actor) => ctx.json(toCompanyDto(await axeda.companies.create(actor, ctx.body))))),
49
+ axedaCompanyUpdate: createAuthEndpoint('/axeda/company/update', {
50
+ method: 'POST',
51
+ use: [sessionMiddleware],
52
+ body: z.strictObject({
53
+ id,
54
+ version,
55
+ name: name.optional(),
56
+ legalName: name.nullish(),
57
+ logoUrl: url.nullish(),
58
+ faviconUrl: url.nullish(),
59
+ metadata: metadata.optional(),
60
+ }),
61
+ metadata: access('axeda:company:manage', 'per-target'),
62
+ }, (ctx) => run(ctx, core, async (axeda, actor) => {
63
+ const { id: companyId, version: expected, ...changes } = ctx.body;
64
+ return ctx.json(toCompanyDto(await axeda.companies.update(actor, companyId, expected, changes)));
65
+ })),
66
+ axedaCompanySetStatus: createAuthEndpoint('/axeda/company/set-status', {
67
+ method: 'POST',
68
+ use: [sessionMiddleware],
69
+ body: z.strictObject({ id, version, status }),
70
+ metadata: access('axeda:company:manage', 'global'),
71
+ }, (ctx) => run(ctx, core, async (axeda, actor) => ctx.json(toCompanyDto(await axeda.companies.setStatus(actor, ctx.body.id, ctx.body.version, ctx.body.status))))),
72
+ axedaCompanyDelete: createAuthEndpoint('/axeda/company/delete', {
73
+ method: 'POST',
74
+ use: [sessionMiddleware],
75
+ body: z.strictObject({ id, version, immediate: z.boolean().optional() }),
76
+ metadata: access('axeda:company:manage', 'global'),
77
+ }, (ctx) => run(ctx, core, async (axeda, actor) => {
78
+ await axeda.companies.delete(actor, ctx.body.id, ctx.body.version, {
79
+ immediate: ctx.body.immediate,
80
+ });
81
+ return ctx.json({ deleted: true });
82
+ })),
83
+ axedaCompanyRestore: createAuthEndpoint('/axeda/company/restore', {
84
+ method: 'POST',
85
+ use: [sessionMiddleware],
86
+ body: z.strictObject({ id, version }),
87
+ metadata: access('axeda:company:manage', 'global'),
88
+ }, (ctx) => run(ctx, core, async (axeda, actor) => ctx.json(toCompanyDto(await axeda.companies.restore(actor, ctx.body.id, ctx.body.version))))),
89
+ axedaBusinessList: createAuthEndpoint('/axeda/business/list', {
90
+ method: 'GET',
91
+ use: [sessionMiddleware],
92
+ query: z.strictObject({
93
+ companyId: id,
94
+ deleted: deleted.optional(),
95
+ cursor: cursor.optional(),
96
+ limit: limit.optional(),
97
+ }),
98
+ metadata: access('(membership)', 'per-target'),
99
+ }, (ctx) => run(ctx, core, async (axeda, actor) => {
100
+ const page = await axeda.businesses.list(actor, ctx.query.companyId, { deleted: ctx.query.deleted }, pageOf(ctx.query));
101
+ return ctx.json({ items: page.items.map(toBusinessDto), nextCursor: page.nextCursor });
102
+ })),
103
+ axedaBusinessGet: createAuthEndpoint('/axeda/business/get', {
104
+ method: 'GET',
105
+ use: [sessionMiddleware],
106
+ query: z.strictObject({ id }),
107
+ metadata: access('(membership)', 'per-target'),
108
+ }, (ctx) => run(ctx, core, async (axeda, actor) => ctx.json(toBusinessDto(await axeda.businesses.get(actor, ctx.query.id))))),
109
+ axedaBusinessCreate: createAuthEndpoint('/axeda/business/create', {
110
+ method: 'POST',
111
+ use: [sessionMiddleware],
112
+ body: z.strictObject({
113
+ companyId: id,
114
+ slug,
115
+ name,
116
+ logoUrl: url.nullish(),
117
+ faviconUrl: url.nullish(),
118
+ metadata: metadata.optional(),
119
+ }),
120
+ metadata: access('axeda:business:manage', 'per-target'),
121
+ }, (ctx) => run(ctx, core, async (axeda, actor) => {
122
+ const { companyId, ...input } = ctx.body;
123
+ return ctx.json(toBusinessDto(await axeda.businesses.create(actor, companyId, input)));
124
+ })),
125
+ axedaBusinessUpdate: createAuthEndpoint('/axeda/business/update', {
126
+ method: 'POST',
127
+ use: [sessionMiddleware],
128
+ body: z.strictObject({
129
+ id,
130
+ version,
131
+ name: name.optional(),
132
+ logoUrl: url.nullish(),
133
+ faviconUrl: url.nullish(),
134
+ metadata: metadata.optional(),
135
+ }),
136
+ metadata: access('axeda:business:manage', 'per-target'),
137
+ }, (ctx) => run(ctx, core, async (axeda, actor) => {
138
+ const { id: businessId, version: expected, ...changes } = ctx.body;
139
+ return ctx.json(toBusinessDto(await axeda.businesses.update(actor, businessId, expected, changes)));
140
+ })),
141
+ axedaBusinessSetStatus: createAuthEndpoint('/axeda/business/set-status', {
142
+ method: 'POST',
143
+ use: [sessionMiddleware],
144
+ body: z.strictObject({ id, version, status }),
145
+ metadata: access('axeda:business:manage', 'per-target'),
146
+ }, (ctx) => run(ctx, core, async (axeda, actor) => ctx.json(toBusinessDto(await axeda.businesses.setStatus(actor, ctx.body.id, ctx.body.version, ctx.body.status))))),
147
+ axedaBusinessDelete: createAuthEndpoint('/axeda/business/delete', {
148
+ method: 'POST',
149
+ use: [sessionMiddleware],
150
+ body: z.strictObject({ id, version, immediate: z.boolean().optional() }),
151
+ metadata: access('axeda:business:manage', 'per-target'),
152
+ }, (ctx) => run(ctx, core, async (axeda, actor) => {
153
+ await axeda.businesses.delete(actor, ctx.body.id, ctx.body.version, {
154
+ immediate: ctx.body.immediate,
155
+ });
156
+ return ctx.json({ deleted: true });
157
+ })),
158
+ axedaBusinessRestore: createAuthEndpoint('/axeda/business/restore', {
159
+ method: 'POST',
160
+ use: [sessionMiddleware],
161
+ body: z.strictObject({ id, version }),
162
+ metadata: access('axeda:business:manage', 'per-target'),
163
+ }, (ctx) => run(ctx, core, async (axeda, actor) => ctx.json(toBusinessDto(await axeda.businesses.restore(actor, ctx.body.id, ctx.body.version))))),
164
+ };
165
+ }
166
+ /** Invitaciones. Aceptar solo exige sesión: lo que lo autoriza es el secreto y el correo verificado. */
167
+ export function createInvitationEndpoints(core) {
168
+ return {
169
+ axedaInvitationList: createAuthEndpoint('/axeda/invitation/list', {
170
+ method: 'GET',
171
+ use: [sessionMiddleware],
172
+ query: z.strictObject({
173
+ ...scopeQuery,
174
+ pendingOnly: z.enum(['true', 'false']).optional(),
175
+ cursor: cursor.optional(),
176
+ limit: limit.optional(),
177
+ }),
178
+ metadata: access('axeda:invitation:manage', 'scope-parameter'),
179
+ }, (ctx) => run(ctx, core, async (axeda, actor) => {
180
+ const scope = scopeFromQuery(ctx.query);
181
+ if (scope === undefined) {
182
+ throw new AxedaError('AXEDA_INVALID_INPUT', 'Pass scopeType (and scopeId).');
183
+ }
184
+ const page = await axeda.invitations.list(actor, scope, { pendingOnly: ctx.query.pendingOnly === 'true' }, pageOf(ctx.query));
185
+ return ctx.json({ items: page.items.map(toInvitationDto), nextCursor: page.nextCursor });
186
+ })),
187
+ axedaInvitationCreate: createAuthEndpoint('/axeda/invitation/create', {
188
+ method: 'POST',
189
+ use: [sessionMiddleware],
190
+ body: z.strictObject({
191
+ email: z.string().min(3).max(254),
192
+ scope: scopeBody,
193
+ roleIds: z.array(id).min(1).max(50),
194
+ ttlHours: z
195
+ .number()
196
+ .int()
197
+ .min(1)
198
+ .max(24 * 30)
199
+ .optional(),
200
+ }),
201
+ metadata: access('axeda:invitation:manage', 'scope-parameter'),
202
+ }, (ctx) => run(ctx, core, async (axeda, actor) => ctx.json(toInvitationDto(await axeda.invitations.create(actor, ctx.body))))),
203
+ axedaInvitationAccept: createAuthEndpoint('/axeda/invitation/accept', {
204
+ method: 'POST',
205
+ use: [sessionMiddleware],
206
+ body: z.strictObject({ token: z.string().min(1).max(512) }),
207
+ metadata: access('(authenticated; the secret and the verified email authorize)', 'own'),
208
+ }, (ctx) => run(ctx, core, async (axeda, actor) => {
209
+ const created = await axeda.invitations.accept(actor, ctx.body.token);
210
+ return ctx.json({ assignments: created.map(toAssignmentDto) });
211
+ })),
212
+ axedaInvitationRevoke: createAuthEndpoint('/axeda/invitation/revoke', {
213
+ method: 'POST',
214
+ use: [sessionMiddleware],
215
+ body: z.strictObject({ id }),
216
+ metadata: access('axeda:invitation:manage', 'per-target'),
217
+ }, (ctx) => run(ctx, core, async (axeda, actor) => {
218
+ await axeda.invitations.revoke(actor, ctx.body.id);
219
+ return ctx.json({ revoked: true });
220
+ })),
221
+ };
222
+ }
223
+ /**
224
+ * Identidad pública: nombre y logos de una compañía o negocio activo. **No** pide sesión, por eso
225
+ * está aparte y solo se registra si la aplicación lo activa (`publicIdentity: true`). Una
226
+ * compañía inexistente, inactiva o borrada responde igual: 404.
227
+ */
228
+ export function createPublicEndpoints(core, enabled) {
229
+ return {
230
+ axedaPublicIdentity: createAuthEndpoint('/axeda/public/identity', {
231
+ method: 'GET',
232
+ query: z.strictObject({ company: slug, business: slug.optional() }),
233
+ metadata: { axeda: { public: true, requires: '(public)', scope: 'own' } },
234
+ }, async (ctx) => {
235
+ try {
236
+ const identity = enabled() ? await core().publicIdentity(ctx.query) : null;
237
+ if (identity === null) {
238
+ throw new AxedaError('AXEDA_COMPANY_NOT_FOUND', 'Not found.');
239
+ }
240
+ return ctx.json(identity);
241
+ }
242
+ catch (error) {
243
+ throw toApiError(error);
244
+ }
245
+ }),
246
+ };
247
+ }
@@ -0,0 +1,242 @@
1
+ import { z } from 'zod';
2
+ import { type CoreProvider } from './endpoints.js';
3
+ /** El primer idioma de `Accept-Language` (`es-MX,es;q=0.9,en;q=0.8` → `es-MX`), si es válido. */
4
+ export declare function localeFromHeader(header: string | null | undefined): string | undefined;
5
+ /**
6
+ * Administración de usuarios (solo global, `axeda:user:manage`) y el menú de cada persona.
7
+ * Axeda no maneja contraseñas: `user/create` crea la cuenta sin credencial.
8
+ */
9
+ export declare function createUserEndpoints(core: CoreProvider): {
10
+ axedaUserList: import("better-call").StrictEndpoint<"/axeda/user/list", {
11
+ method: "GET";
12
+ use: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<{
13
+ session: {
14
+ session: Record<string, any> & {
15
+ id: string;
16
+ createdAt: Date;
17
+ updatedAt: Date;
18
+ userId: string;
19
+ expiresAt: Date;
20
+ token: string;
21
+ ipAddress?: string | null | undefined;
22
+ userAgent?: string | null | undefined;
23
+ };
24
+ user: Record<string, any> & {
25
+ id: string;
26
+ createdAt: Date;
27
+ updatedAt: Date;
28
+ email: string;
29
+ emailVerified: boolean;
30
+ name: string;
31
+ image?: string | null | undefined;
32
+ };
33
+ };
34
+ }>>[];
35
+ query: z.ZodObject<{
36
+ emailPrefix: z.ZodOptional<z.ZodString>;
37
+ banned: z.ZodOptional<z.ZodEnum<{
38
+ true: "true";
39
+ false: "false";
40
+ }>>;
41
+ cursor: z.ZodOptional<z.ZodString>;
42
+ limit: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
43
+ }, z.core.$strict>;
44
+ metadata: {
45
+ axeda: {
46
+ requires: string;
47
+ scope: "own" | "any" | "global" | "scope-parameter" | "per-target";
48
+ };
49
+ };
50
+ }, {
51
+ items: import("./dto.js").ManagedUserDto[];
52
+ nextCursor: string | null;
53
+ }>;
54
+ axedaUserCreate: import("better-call").StrictEndpoint<"/axeda/user/create", {
55
+ method: "POST";
56
+ use: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<{
57
+ session: {
58
+ session: Record<string, any> & {
59
+ id: string;
60
+ createdAt: Date;
61
+ updatedAt: Date;
62
+ userId: string;
63
+ expiresAt: Date;
64
+ token: string;
65
+ ipAddress?: string | null | undefined;
66
+ userAgent?: string | null | undefined;
67
+ };
68
+ user: Record<string, any> & {
69
+ id: string;
70
+ createdAt: Date;
71
+ updatedAt: Date;
72
+ email: string;
73
+ emailVerified: boolean;
74
+ name: string;
75
+ image?: string | null | undefined;
76
+ };
77
+ };
78
+ }>>[];
79
+ body: z.ZodObject<{
80
+ email: z.ZodString;
81
+ name: z.ZodString;
82
+ }, z.core.$strict>;
83
+ metadata: {
84
+ axeda: {
85
+ requires: string;
86
+ scope: "own" | "any" | "global" | "scope-parameter" | "per-target";
87
+ };
88
+ };
89
+ }, import("./dto.js").ManagedUserDto>;
90
+ axedaUserBan: import("better-call").StrictEndpoint<"/axeda/user/ban", {
91
+ method: "POST";
92
+ use: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<{
93
+ session: {
94
+ session: Record<string, any> & {
95
+ id: string;
96
+ createdAt: Date;
97
+ updatedAt: Date;
98
+ userId: string;
99
+ expiresAt: Date;
100
+ token: string;
101
+ ipAddress?: string | null | undefined;
102
+ userAgent?: string | null | undefined;
103
+ };
104
+ user: Record<string, any> & {
105
+ id: string;
106
+ createdAt: Date;
107
+ updatedAt: Date;
108
+ email: string;
109
+ emailVerified: boolean;
110
+ name: string;
111
+ image?: string | null | undefined;
112
+ };
113
+ };
114
+ }>>[];
115
+ body: z.ZodObject<{
116
+ id: z.ZodString;
117
+ reason: z.ZodOptional<z.ZodNullable<z.ZodString>>;
118
+ expiresAt: z.ZodOptional<z.ZodNullable<z.ZodISODateTime>>;
119
+ }, z.core.$strict>;
120
+ metadata: {
121
+ axeda: {
122
+ requires: string;
123
+ scope: "own" | "any" | "global" | "scope-parameter" | "per-target";
124
+ };
125
+ };
126
+ }, import("./dto.js").ManagedUserDto>;
127
+ axedaUserUnban: import("better-call").StrictEndpoint<"/axeda/user/unban", {
128
+ method: "POST";
129
+ use: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<{
130
+ session: {
131
+ session: Record<string, any> & {
132
+ id: string;
133
+ createdAt: Date;
134
+ updatedAt: Date;
135
+ userId: string;
136
+ expiresAt: Date;
137
+ token: string;
138
+ ipAddress?: string | null | undefined;
139
+ userAgent?: string | null | undefined;
140
+ };
141
+ user: Record<string, any> & {
142
+ id: string;
143
+ createdAt: Date;
144
+ updatedAt: Date;
145
+ email: string;
146
+ emailVerified: boolean;
147
+ name: string;
148
+ image?: string | null | undefined;
149
+ };
150
+ };
151
+ }>>[];
152
+ body: z.ZodObject<{
153
+ id: z.ZodString;
154
+ }, z.core.$strict>;
155
+ metadata: {
156
+ axeda: {
157
+ requires: string;
158
+ scope: "own" | "any" | "global" | "scope-parameter" | "per-target";
159
+ };
160
+ };
161
+ }, import("./dto.js").ManagedUserDto>;
162
+ axedaUserRevokeSessions: import("better-call").StrictEndpoint<"/axeda/user/revoke-sessions", {
163
+ method: "POST";
164
+ use: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<{
165
+ session: {
166
+ session: Record<string, any> & {
167
+ id: string;
168
+ createdAt: Date;
169
+ updatedAt: Date;
170
+ userId: string;
171
+ expiresAt: Date;
172
+ token: string;
173
+ ipAddress?: string | null | undefined;
174
+ userAgent?: string | null | undefined;
175
+ };
176
+ user: Record<string, any> & {
177
+ id: string;
178
+ createdAt: Date;
179
+ updatedAt: Date;
180
+ email: string;
181
+ emailVerified: boolean;
182
+ name: string;
183
+ image?: string | null | undefined;
184
+ };
185
+ };
186
+ }>>[];
187
+ body: z.ZodObject<{
188
+ id: z.ZodString;
189
+ }, z.core.$strict>;
190
+ metadata: {
191
+ axeda: {
192
+ requires: string;
193
+ scope: "own" | "any" | "global" | "scope-parameter" | "per-target";
194
+ };
195
+ };
196
+ }, {
197
+ revoked: number;
198
+ }>;
199
+ axedaMenu: import("better-call").StrictEndpoint<"/axeda/menu", {
200
+ method: "GET";
201
+ use: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<{
202
+ session: {
203
+ session: Record<string, any> & {
204
+ id: string;
205
+ createdAt: Date;
206
+ updatedAt: Date;
207
+ userId: string;
208
+ expiresAt: Date;
209
+ token: string;
210
+ ipAddress?: string | null | undefined;
211
+ userAgent?: string | null | undefined;
212
+ };
213
+ user: Record<string, any> & {
214
+ id: string;
215
+ createdAt: Date;
216
+ updatedAt: Date;
217
+ email: string;
218
+ emailVerified: boolean;
219
+ name: string;
220
+ image?: string | null | undefined;
221
+ };
222
+ };
223
+ }>>[];
224
+ query: z.ZodObject<{
225
+ locale: z.ZodOptional<z.ZodString>;
226
+ scopeType: z.ZodOptional<z.ZodEnum<{
227
+ company: "company";
228
+ business: "business";
229
+ global: "global";
230
+ }>>;
231
+ scopeId: z.ZodOptional<z.ZodString>;
232
+ }, z.core.$strict>;
233
+ metadata: {
234
+ axeda: {
235
+ requires: string;
236
+ scope: "own" | "any" | "global" | "scope-parameter" | "per-target";
237
+ };
238
+ };
239
+ }, {
240
+ items: import("@teamblind/axeda-core").MenuNode[];
241
+ }>;
242
+ };
@@ -0,0 +1,85 @@
1
+ import { createAuthEndpoint, sessionMiddleware } from 'better-auth/api';
2
+ import { z } from 'zod';
3
+ import { toManagedUserDto } from './dto.js';
4
+ import { access, cursor, id, limit, pageOf, run, scopeFromQuery, scopeQuery, } from './endpoints.js';
5
+ const LOCALE = /^[A-Za-z]{2}(-[A-Za-z0-9]{2,8})?$/;
6
+ /** El primer idioma de `Accept-Language` (`es-MX,es;q=0.9,en;q=0.8` → `es-MX`), si es válido. */
7
+ export function localeFromHeader(header) {
8
+ const first = header?.split(',')[0]?.split(';')[0]?.trim();
9
+ return first !== undefined && LOCALE.test(first) ? first : undefined;
10
+ }
11
+ /**
12
+ * Administración de usuarios (solo global, `axeda:user:manage`) y el menú de cada persona.
13
+ * Axeda no maneja contraseñas: `user/create` crea la cuenta sin credencial.
14
+ */
15
+ export function createUserEndpoints(core) {
16
+ return {
17
+ axedaUserList: createAuthEndpoint('/axeda/user/list', {
18
+ method: 'GET',
19
+ use: [sessionMiddleware],
20
+ query: z.strictObject({
21
+ emailPrefix: z.string().min(1).max(254).optional(),
22
+ banned: z.enum(['true', 'false']).optional(),
23
+ cursor: cursor.optional(),
24
+ limit: limit.optional(),
25
+ }),
26
+ metadata: access('axeda:user:manage', 'global'),
27
+ }, (ctx) => run(ctx, core, async (axeda, actor) => {
28
+ const { emailPrefix, banned } = ctx.query;
29
+ const page = await axeda.users.list(actor, { emailPrefix, banned: banned === undefined ? undefined : banned === 'true' }, pageOf(ctx.query));
30
+ return ctx.json({ items: page.items.map(toManagedUserDto), nextCursor: page.nextCursor });
31
+ })),
32
+ axedaUserCreate: createAuthEndpoint('/axeda/user/create', {
33
+ method: 'POST',
34
+ use: [sessionMiddleware],
35
+ body: z.strictObject({
36
+ email: z.string().min(3).max(254),
37
+ name: z.string().min(1).max(200),
38
+ }),
39
+ metadata: access('axeda:user:manage', 'global'),
40
+ }, (ctx) => run(ctx, core, async (axeda, actor) => ctx.json(toManagedUserDto(await axeda.users.create(actor, ctx.body))))),
41
+ axedaUserBan: createAuthEndpoint('/axeda/user/ban', {
42
+ method: 'POST',
43
+ use: [sessionMiddleware],
44
+ body: z.strictObject({
45
+ id,
46
+ reason: z.string().min(1).max(500).nullish(),
47
+ expiresAt: z.iso.datetime().nullish(),
48
+ }),
49
+ metadata: access('axeda:user:manage', 'global'),
50
+ }, (ctx) => run(ctx, core, async (axeda, actor) => {
51
+ const { expiresAt, reason } = ctx.body;
52
+ const banned = await axeda.users.ban(actor, ctx.body.id, {
53
+ reason,
54
+ expiresAt: expiresAt === null || expiresAt === undefined ? null : new Date(expiresAt),
55
+ });
56
+ return ctx.json(toManagedUserDto(banned));
57
+ })),
58
+ axedaUserUnban: createAuthEndpoint('/axeda/user/unban', {
59
+ method: 'POST',
60
+ use: [sessionMiddleware],
61
+ body: z.strictObject({ id }),
62
+ metadata: access('axeda:user:manage', 'global'),
63
+ }, (ctx) => run(ctx, core, async (axeda, actor) => ctx.json(toManagedUserDto(await axeda.users.unban(actor, ctx.body.id))))),
64
+ axedaUserRevokeSessions: createAuthEndpoint('/axeda/user/revoke-sessions', {
65
+ method: 'POST',
66
+ use: [sessionMiddleware],
67
+ body: z.strictObject({ id }),
68
+ metadata: access('axeda:user:manage', 'global'),
69
+ }, (ctx) => run(ctx, core, async (axeda, actor) => ctx.json({ revoked: await axeda.users.revokeSessions(actor, ctx.body.id) }))),
70
+ axedaMenu: createAuthEndpoint('/axeda/menu', {
71
+ method: 'GET',
72
+ use: [sessionMiddleware],
73
+ query: z.strictObject({
74
+ ...scopeQuery,
75
+ locale: z.string().regex(LOCALE).optional(),
76
+ }),
77
+ metadata: access('(authenticated; the menu only lists what you may use)', 'own'),
78
+ }, (ctx) => run(ctx, core, async (axeda, actor) => {
79
+ const locale = ctx.query.locale ??
80
+ localeFromHeader(ctx.headers?.get('accept-language') ?? ctx.request?.headers.get('accept-language'));
81
+ const menu = await axeda.menu(actor, { scope: scopeFromQuery(ctx.query), locale });
82
+ return ctx.json({ items: menu });
83
+ })),
84
+ };
85
+ }