create-fullstack-scaffold 0.5.7 → 0.6.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/cli/index.js +65 -2
- package/dist/cli/index.js.map +1 -1
- package/package.json +12 -12
- package/template/package.json +10 -10
- package/template/src/cli/modules/tenant/index.ts +89 -0
- package/template/src/server/db/init.ts +0 -52
- package/template/src/server/db/schema/index.ts +3 -0
- package/template/src/server/db/schema/tenant-invitations.ts +33 -0
- package/template/src/server/db/schema/tenant-members.ts +38 -0
- package/template/src/server/db/schema/tenant-roles.ts +32 -0
- package/template/src/server/db/schema/todos.ts +4 -0
- package/template/src/server/db/test-setup.ts +128 -0
- package/template/src/server/entries/node.ts +13 -0
- package/template/src/server/index.ts +12 -0
- package/template/src/server/middleware/__tests__/tenant-isolation.test.ts +55 -9
- package/template/src/server/middleware/auth.ts +17 -19
- package/template/src/server/middleware/tenant-isolation.ts +4 -2
- package/template/src/server/module-auth/module.ts +2 -1
- package/template/src/server/module-auth/services/auth-service.ts +39 -0
- package/template/src/server/module-tenant/__tests__/tenant-routes.test.ts +97 -0
- package/template/src/server/module-tenant/__tests__/tenant-service.test.ts +201 -22
- package/template/src/server/module-tenant/module.ts +1 -1
- package/template/src/server/module-tenant/routes/tenant-routes.ts +339 -2
- package/template/src/server/module-tenant/services/tenant-service.ts +581 -17
- package/template/src/server/module-todos/routes/todos-routes.ts +10 -2
- package/template/src/server/module-todos/services/todo-service.ts +12 -2
- package/template/src/server/utils/__tests__/captcha.test.ts +6 -4
- package/template/src/server/utils/id-helpers.ts +13 -0
- package/template/src/shared/modules/index.ts +29 -6
- package/template/src/shared/modules/tenant/index.ts +30 -0
- package/template/src/shared/modules/tenant/permissions.ts +54 -0
- package/template/src/shared/modules/tenant/role-templates.ts +66 -0
- package/template/src/shared/modules/tenant/schemas.ts +113 -1
- package/template/src/shared/modules/todos/schemas.ts +1 -0
- package/template/src/shared/schemas/index.ts +28 -0
- package/template/src/tenant/App.tsx +30 -23
- package/template/src/tenant/components/TenantGuard.tsx +12 -2
- package/template/src/tenant/layouts/Header.tsx +6 -2
- package/template/src/tenant/pages/InviteAcceptPage.tsx +97 -0
- package/template/src/tenant/pages/LoginPage.tsx +82 -0
- package/template/src/tenant/pages/SubscriptionPage.tsx +32 -55
- package/template/src/tenant/pages/UsersPage.tsx +134 -88
- package/template/src/tenant/services/tenantApi.ts +61 -0
- package/template/src/tenant/stores/tenantStore.ts +172 -49
- package/template/src/test/setup-db-path.ts +20 -0
- package/template/vitest.config.ts +3 -1
|
@@ -9,9 +9,39 @@ import {
|
|
|
9
9
|
TenantQuerySchema,
|
|
10
10
|
TenantIdResponseSchema,
|
|
11
11
|
} from '@shared/schemas'
|
|
12
|
+
import {
|
|
13
|
+
TenantRoleSchema,
|
|
14
|
+
TenantMemberSchema,
|
|
15
|
+
TenantInvitationSchema,
|
|
16
|
+
PublicInvitationSchema,
|
|
17
|
+
StringIdResponseSchema,
|
|
18
|
+
TenantArrayResponseSchema,
|
|
19
|
+
TenantRoleArrayResponseSchema,
|
|
20
|
+
TenantMemberArrayResponseSchema,
|
|
21
|
+
CreateTenantRoleSchema,
|
|
22
|
+
UpdateTenantRoleSchema,
|
|
23
|
+
InviteMemberSchema,
|
|
24
|
+
UpdateMemberRoleSchema,
|
|
25
|
+
TenantPermission,
|
|
26
|
+
type PublicInvitation,
|
|
27
|
+
} from '@shared/schemas'
|
|
12
28
|
import { successResponse, errorResponse, success } from '@server/utils/route-helpers'
|
|
13
|
-
import { NotFoundError } from '@server/utils/app-error'
|
|
29
|
+
import { NotFoundError, AuthorizationError } from '@server/utils/app-error'
|
|
14
30
|
import { authMiddleware, requireSuperAdminMiddleware } from '@server/middleware/auth'
|
|
31
|
+
import { Role } from '@shared/modules/permission'
|
|
32
|
+
import type { Context } from 'hono'
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 租户内权限门禁:超管 bypass;成员须持有对应 TenantPermission
|
|
36
|
+
* (非成员直接 403——这就是"租户隔离"的路由层执行点)
|
|
37
|
+
*/
|
|
38
|
+
async function requireTenantPerm(c: Context, tenantId: number, perm: TenantPermission) {
|
|
39
|
+
const user = c.get('authUser')
|
|
40
|
+
if (user.role === Role.SUPER_ADMIN) return
|
|
41
|
+
await tenantService.requireTenantMembership(user.id, tenantId)
|
|
42
|
+
const ok = await tenantService.hasTenantPermission(user.id, tenantId, perm)
|
|
43
|
+
if (!ok) throw AuthorizationError.permissionDenied(perm)
|
|
44
|
+
}
|
|
15
45
|
|
|
16
46
|
const listRoute = createRoute({
|
|
17
47
|
method: 'get',
|
|
@@ -107,6 +137,207 @@ const deleteRoute = createRoute({
|
|
|
107
137
|
},
|
|
108
138
|
})
|
|
109
139
|
|
|
140
|
+
const myTenantsRoute = createRoute({
|
|
141
|
+
method: 'get',
|
|
142
|
+
path: '/tenants/mine',
|
|
143
|
+
tags: ['tenants'],
|
|
144
|
+
security: [{ Bearer: [] }],
|
|
145
|
+
middleware: [authMiddleware()],
|
|
146
|
+
responses: {
|
|
147
|
+
200: successResponse(TenantArrayResponseSchema, 'List tenants the current user belongs to'),
|
|
148
|
+
},
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
const tenantRolesListRoute = createRoute({
|
|
152
|
+
method: 'get',
|
|
153
|
+
path: '/tenants/{tenantId}/roles',
|
|
154
|
+
tags: ['tenants'],
|
|
155
|
+
security: [{ Bearer: [] }],
|
|
156
|
+
middleware: [authMiddleware()],
|
|
157
|
+
request: {
|
|
158
|
+
params: z.object({ tenantId: z.coerce.number().int().positive() }),
|
|
159
|
+
},
|
|
160
|
+
responses: {
|
|
161
|
+
200: successResponse(TenantRoleArrayResponseSchema, 'List tenant roles'),
|
|
162
|
+
403: errorResponse('Not a member of this tenant'),
|
|
163
|
+
},
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
const tenantRoleCreateRoute = createRoute({
|
|
167
|
+
method: 'post',
|
|
168
|
+
path: '/tenants/{tenantId}/roles',
|
|
169
|
+
tags: ['tenants'],
|
|
170
|
+
security: [{ Bearer: [] }],
|
|
171
|
+
middleware: [authMiddleware()],
|
|
172
|
+
request: {
|
|
173
|
+
params: z.object({ tenantId: z.coerce.number().int().positive() }),
|
|
174
|
+
body: { content: { 'application/json': { schema: CreateTenantRoleSchema } } },
|
|
175
|
+
},
|
|
176
|
+
responses: {
|
|
177
|
+
201: successResponse(TenantRoleSchema, 'Create a custom tenant role'),
|
|
178
|
+
400: errorResponse('Invalid input or plan role limit reached'),
|
|
179
|
+
403: errorResponse('No permission'),
|
|
180
|
+
},
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
const tenantRoleUpdateRoute = createRoute({
|
|
184
|
+
method: 'put',
|
|
185
|
+
path: '/tenants/{tenantId}/roles/{roleId}',
|
|
186
|
+
tags: ['tenants'],
|
|
187
|
+
security: [{ Bearer: [] }],
|
|
188
|
+
middleware: [authMiddleware()],
|
|
189
|
+
request: {
|
|
190
|
+
params: z.object({
|
|
191
|
+
tenantId: z.coerce.number().int().positive(),
|
|
192
|
+
roleId: z.string(),
|
|
193
|
+
}),
|
|
194
|
+
body: { content: { 'application/json': { schema: UpdateTenantRoleSchema } } },
|
|
195
|
+
},
|
|
196
|
+
responses: {
|
|
197
|
+
200: successResponse(TenantRoleSchema, 'Update a tenant role'),
|
|
198
|
+
403: errorResponse('No permission'),
|
|
199
|
+
404: errorResponse('Role not found'),
|
|
200
|
+
},
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
const tenantRoleDeleteRoute = createRoute({
|
|
204
|
+
method: 'delete',
|
|
205
|
+
path: '/tenants/{tenantId}/roles/{roleId}',
|
|
206
|
+
tags: ['tenants'],
|
|
207
|
+
security: [{ Bearer: [] }],
|
|
208
|
+
middleware: [authMiddleware()],
|
|
209
|
+
request: {
|
|
210
|
+
params: z.object({
|
|
211
|
+
tenantId: z.coerce.number().int().positive(),
|
|
212
|
+
roleId: z.string(),
|
|
213
|
+
}),
|
|
214
|
+
},
|
|
215
|
+
responses: {
|
|
216
|
+
200: successResponse(StringIdResponseSchema, 'Soft-delete a tenant role'),
|
|
217
|
+
400: errorResponse('System role cannot be deleted'),
|
|
218
|
+
403: errorResponse('No permission'),
|
|
219
|
+
},
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
const tenantMembersListRoute = createRoute({
|
|
223
|
+
method: 'get',
|
|
224
|
+
path: '/tenants/{tenantId}/members',
|
|
225
|
+
tags: ['tenants'],
|
|
226
|
+
security: [{ Bearer: [] }],
|
|
227
|
+
middleware: [authMiddleware()],
|
|
228
|
+
request: {
|
|
229
|
+
params: z.object({ tenantId: z.coerce.number().int().positive() }),
|
|
230
|
+
},
|
|
231
|
+
responses: {
|
|
232
|
+
200: successResponse(TenantMemberArrayResponseSchema, 'List tenant members with roles'),
|
|
233
|
+
403: errorResponse('Not a member of this tenant'),
|
|
234
|
+
},
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
const memberInviteRoute = createRoute({
|
|
238
|
+
method: 'post',
|
|
239
|
+
path: '/tenants/{tenantId}/members/invite',
|
|
240
|
+
tags: ['tenants'],
|
|
241
|
+
security: [{ Bearer: [] }],
|
|
242
|
+
middleware: [authMiddleware()],
|
|
243
|
+
request: {
|
|
244
|
+
params: z.object({ tenantId: z.coerce.number().int().positive() }),
|
|
245
|
+
body: { content: { 'application/json': { schema: InviteMemberSchema } } },
|
|
246
|
+
},
|
|
247
|
+
responses: {
|
|
248
|
+
201: successResponse(TenantInvitationSchema, 'Create an invitation (7-day token)'),
|
|
249
|
+
400: errorResponse('Invalid input or role not in tenant'),
|
|
250
|
+
403: errorResponse('No permission'),
|
|
251
|
+
},
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
const memberUpdateRoute = createRoute({
|
|
255
|
+
method: 'put',
|
|
256
|
+
path: '/tenants/{tenantId}/members/{memberId}',
|
|
257
|
+
tags: ['tenants'],
|
|
258
|
+
security: [{ Bearer: [] }],
|
|
259
|
+
middleware: [authMiddleware()],
|
|
260
|
+
request: {
|
|
261
|
+
params: z.object({
|
|
262
|
+
tenantId: z.coerce.number().int().positive(),
|
|
263
|
+
memberId: z.string(),
|
|
264
|
+
}),
|
|
265
|
+
body: { content: { 'application/json': { schema: UpdateMemberRoleSchema } } },
|
|
266
|
+
},
|
|
267
|
+
responses: {
|
|
268
|
+
200: successResponse(TenantMemberSchema, 'Change member role'),
|
|
269
|
+
403: errorResponse('No permission'),
|
|
270
|
+
404: errorResponse('Member not found'),
|
|
271
|
+
},
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
const memberRemoveRoute = createRoute({
|
|
275
|
+
method: 'delete',
|
|
276
|
+
path: '/tenants/{tenantId}/members/{memberId}',
|
|
277
|
+
tags: ['tenants'],
|
|
278
|
+
security: [{ Bearer: [] }],
|
|
279
|
+
middleware: [authMiddleware()],
|
|
280
|
+
request: {
|
|
281
|
+
params: z.object({
|
|
282
|
+
tenantId: z.coerce.number().int().positive(),
|
|
283
|
+
memberId: z.string(),
|
|
284
|
+
}),
|
|
285
|
+
},
|
|
286
|
+
responses: {
|
|
287
|
+
200: successResponse(StringIdResponseSchema, 'Remove member (soft, status=left)'),
|
|
288
|
+
403: errorResponse('No permission'),
|
|
289
|
+
404: errorResponse('Member not found'),
|
|
290
|
+
},
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
const invitationCancelRoute = createRoute({
|
|
294
|
+
method: 'delete',
|
|
295
|
+
path: '/tenants/{tenantId}/invitations/{invitationId}',
|
|
296
|
+
tags: ['tenants'],
|
|
297
|
+
security: [{ Bearer: [] }],
|
|
298
|
+
middleware: [authMiddleware()],
|
|
299
|
+
request: {
|
|
300
|
+
params: z.object({
|
|
301
|
+
tenantId: z.coerce.number().int().positive(),
|
|
302
|
+
invitationId: z.string(),
|
|
303
|
+
}),
|
|
304
|
+
},
|
|
305
|
+
responses: {
|
|
306
|
+
200: successResponse(StringIdResponseSchema, 'Cancel invitation'),
|
|
307
|
+
403: errorResponse('No permission'),
|
|
308
|
+
404: errorResponse('Invitation not found'),
|
|
309
|
+
},
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
const invitationDetailRoute = createRoute({
|
|
313
|
+
method: 'get',
|
|
314
|
+
path: '/tenants/invitations/{token}',
|
|
315
|
+
tags: ['tenants'],
|
|
316
|
+
summary: 'Get invitation detail (public, for the invite landing page)',
|
|
317
|
+
request: {
|
|
318
|
+
params: z.object({ token: z.string() }),
|
|
319
|
+
},
|
|
320
|
+
responses: {
|
|
321
|
+
200: successResponse(PublicInvitationSchema, 'Invitation detail (sanitized)'),
|
|
322
|
+
404: errorResponse('Invitation not found'),
|
|
323
|
+
},
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
const invitationAcceptRoute = createRoute({
|
|
327
|
+
method: 'post',
|
|
328
|
+
path: '/tenants/invitations/{token}/accept',
|
|
329
|
+
tags: ['tenants'],
|
|
330
|
+
security: [{ Bearer: [] }],
|
|
331
|
+
middleware: [authMiddleware()],
|
|
332
|
+
request: {
|
|
333
|
+
params: z.object({ token: z.string() }),
|
|
334
|
+
},
|
|
335
|
+
responses: {
|
|
336
|
+
200: successResponse(TenantMemberSchema, 'Accept invitation and join tenant'),
|
|
337
|
+
404: errorResponse('Invitation not found or expired'),
|
|
338
|
+
},
|
|
339
|
+
})
|
|
340
|
+
|
|
110
341
|
const getCurrentTenantRoute = createRoute({
|
|
111
342
|
method: 'get',
|
|
112
343
|
path: '/tenant/current',
|
|
@@ -130,6 +361,11 @@ export const apiRoutes = new OpenAPIHono()
|
|
|
130
361
|
|
|
131
362
|
return c.json(success(result), 200)
|
|
132
363
|
})
|
|
364
|
+
.openapi(myTenantsRoute, async c => {
|
|
365
|
+
const user = c.get('authUser')
|
|
366
|
+
const tenantsForUser = await tenantService.getUserTenants(user.id)
|
|
367
|
+
return c.json(success(tenantsForUser), 200)
|
|
368
|
+
})
|
|
133
369
|
.openapi(getRoute, async c => {
|
|
134
370
|
const { id } = c.req.valid('param')
|
|
135
371
|
const tenant = await tenantService.getTenantById(id)
|
|
@@ -144,7 +380,8 @@ export const apiRoutes = new OpenAPIHono()
|
|
|
144
380
|
})
|
|
145
381
|
.openapi(createRouteDef, async c => {
|
|
146
382
|
const data = c.req.valid('json')
|
|
147
|
-
const
|
|
383
|
+
const user = c.get('authUser')
|
|
384
|
+
const tenant = await tenantService.createTenant(data, user.id)
|
|
148
385
|
return c.json(success(tenant), 201)
|
|
149
386
|
})
|
|
150
387
|
.openapi(updateRoute, async c => {
|
|
@@ -166,6 +403,106 @@ export const apiRoutes = new OpenAPIHono()
|
|
|
166
403
|
return c.json(success(tenant), 200)
|
|
167
404
|
})
|
|
168
405
|
// todos 模块已占用 /docs;tenant 的 OpenAPI 文档挂独立路径避免 shadow
|
|
406
|
+
.openapi(tenantRolesListRoute, async c => {
|
|
407
|
+
const { tenantId } = c.req.valid('param')
|
|
408
|
+
await requireTenantPerm(c, tenantId, TenantPermission.ROLE_VIEW)
|
|
409
|
+
const roles = await tenantService.getTenantRoles(tenantId)
|
|
410
|
+
return c.json(success(roles), 200)
|
|
411
|
+
})
|
|
412
|
+
.openapi(tenantRoleCreateRoute, async c => {
|
|
413
|
+
const { tenantId } = c.req.valid('param')
|
|
414
|
+
await requireTenantPerm(c, tenantId, TenantPermission.ROLE_CREATE)
|
|
415
|
+
const data = c.req.valid('json')
|
|
416
|
+
const role = await tenantService.createTenantRole(tenantId, {
|
|
417
|
+
code: data.code,
|
|
418
|
+
name: data.code,
|
|
419
|
+
label: data.label,
|
|
420
|
+
description: data.description ?? null,
|
|
421
|
+
permissions: data.permissions,
|
|
422
|
+
})
|
|
423
|
+
return c.json(success(role), 201)
|
|
424
|
+
})
|
|
425
|
+
.openapi(tenantRoleUpdateRoute, async c => {
|
|
426
|
+
const { tenantId, roleId } = c.req.valid('param')
|
|
427
|
+
await requireTenantPerm(c, tenantId, TenantPermission.ROLE_EDIT)
|
|
428
|
+
// 目标角色必须属于本租户
|
|
429
|
+
const roles = await tenantService.getTenantRoles(tenantId)
|
|
430
|
+
if (!roles.some(r => r.id === roleId)) throw new NotFoundError('Role', roleId)
|
|
431
|
+
const data = c.req.valid('json')
|
|
432
|
+
const role = await tenantService.updateTenantRole(roleId, {
|
|
433
|
+
label: data.label ?? undefined,
|
|
434
|
+
description: data.description ?? undefined,
|
|
435
|
+
permissions: data.permissions ?? undefined,
|
|
436
|
+
})
|
|
437
|
+
if (!role) throw new NotFoundError('Role', roleId)
|
|
438
|
+
return c.json(success(role), 200)
|
|
439
|
+
})
|
|
440
|
+
.openapi(tenantRoleDeleteRoute, async c => {
|
|
441
|
+
const { tenantId, roleId } = c.req.valid('param')
|
|
442
|
+
await requireTenantPerm(c, tenantId, TenantPermission.ROLE_DELETE)
|
|
443
|
+
const roles = await tenantService.getTenantRoles(tenantId)
|
|
444
|
+
if (!roles.some(r => r.id === roleId)) throw new NotFoundError('Role', roleId)
|
|
445
|
+
const result = await tenantService.deleteTenantRole(roleId)
|
|
446
|
+
if (!result) throw new NotFoundError('Role', roleId)
|
|
447
|
+
return c.json(success({ id: roleId }), 200)
|
|
448
|
+
})
|
|
449
|
+
.openapi(tenantMembersListRoute, async c => {
|
|
450
|
+
const { tenantId } = c.req.valid('param')
|
|
451
|
+
await requireTenantPerm(c, tenantId, TenantPermission.MEMBER_VIEW)
|
|
452
|
+
const members = await tenantService.getTenantMembers(tenantId)
|
|
453
|
+
return c.json(success(members), 200)
|
|
454
|
+
})
|
|
455
|
+
.openapi(memberInviteRoute, async c => {
|
|
456
|
+
const { tenantId } = c.req.valid('param')
|
|
457
|
+
await requireTenantPerm(c, tenantId, TenantPermission.MEMBER_INVITE)
|
|
458
|
+
const user = c.get('authUser')
|
|
459
|
+
const data = c.req.valid('json')
|
|
460
|
+
const invitation = await tenantService.inviteMember(tenantId, data.email, data.roleId, user.id)
|
|
461
|
+
return c.json(success(invitation), 201)
|
|
462
|
+
})
|
|
463
|
+
.openapi(memberUpdateRoute, async c => {
|
|
464
|
+
const { tenantId, memberId } = c.req.valid('param')
|
|
465
|
+
await requireTenantPerm(c, tenantId, TenantPermission.MEMBER_ROLE_ASSIGN)
|
|
466
|
+
const data = c.req.valid('json')
|
|
467
|
+
const member = await tenantService.updateMemberRole(tenantId, memberId, data.roleId)
|
|
468
|
+
if (!member) throw new NotFoundError('Member', memberId)
|
|
469
|
+
return c.json(success(member), 200)
|
|
470
|
+
})
|
|
471
|
+
.openapi(memberRemoveRoute, async c => {
|
|
472
|
+
const { tenantId, memberId } = c.req.valid('param')
|
|
473
|
+
await requireTenantPerm(c, tenantId, TenantPermission.MEMBER_REMOVE)
|
|
474
|
+
const result = await tenantService.removeMember(tenantId, memberId)
|
|
475
|
+
if (!result) throw new NotFoundError('Member', memberId)
|
|
476
|
+
return c.json(success({ id: memberId }), 200)
|
|
477
|
+
})
|
|
478
|
+
.openapi(invitationCancelRoute, async c => {
|
|
479
|
+
const { tenantId, invitationId } = c.req.valid('param')
|
|
480
|
+
await requireTenantPerm(c, tenantId, TenantPermission.MEMBER_INVITE)
|
|
481
|
+
const result = await tenantService.cancelInvitation(tenantId, invitationId)
|
|
482
|
+
if (!result) throw new NotFoundError('Invitation', invitationId)
|
|
483
|
+
return c.json(success({ id: invitationId }), 200)
|
|
484
|
+
})
|
|
485
|
+
.openapi(invitationDetailRoute, async c => {
|
|
486
|
+
const { token } = c.req.valid('param')
|
|
487
|
+
const detail = await tenantService.getInvitationByToken(token)
|
|
488
|
+
if (!detail) throw new NotFoundError('Invitation', token)
|
|
489
|
+
const payload: PublicInvitation = {
|
|
490
|
+
tenantName: detail.tenantName,
|
|
491
|
+
tenantSlug: detail.tenantSlug,
|
|
492
|
+
email: detail.invitation.email,
|
|
493
|
+
roleLabel: detail.roleLabel,
|
|
494
|
+
status: detail.invitation.status,
|
|
495
|
+
expiresAt: detail.invitation.expiresAt,
|
|
496
|
+
}
|
|
497
|
+
return c.json(success(payload), 200)
|
|
498
|
+
})
|
|
499
|
+
.openapi(invitationAcceptRoute, async c => {
|
|
500
|
+
const { token } = c.req.valid('param')
|
|
501
|
+
const user = c.get('authUser')
|
|
502
|
+
const member = await tenantService.acceptInvitation(token, user.id)
|
|
503
|
+
if (!member) throw new NotFoundError('Invitation (invalid, used or expired)', token)
|
|
504
|
+
return c.json(success(member), 200)
|
|
505
|
+
})
|
|
169
506
|
.doc('/tenant/docs', {
|
|
170
507
|
openapi: '3.0.0',
|
|
171
508
|
info: {
|