@simple-auth-kit/cli 1.4.3 → 1.5.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/lib/copy.ts +1 -1
- package/package.json +1 -1
- package/registry/combos/express-drizzle/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
- package/registry/combos/express-drizzle/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +23 -7
- package/registry/combos/{express-prisma/shared/src → express-drizzle/shared/src/middleware}/response-envelope.middleware.ts +15 -3
- package/registry/combos/express-drizzle/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +32 -9
- package/registry/combos/express-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
- package/registry/combos/express-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
- package/registry/combos/express-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +20 -6
- package/registry/combos/express-drizzle/shared/src/request-context.ts +1 -1
- package/registry/combos/express-drizzle/shared/src/routers/auth.router.ts +395 -0
- package/registry/combos/express-drizzle/variants/base/src/create-auth-app.ts +52 -19
- package/registry/combos/express-drizzle/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
- package/registry/combos/express-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +28 -10
- package/registry/combos/express-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +239 -48
- package/registry/combos/express-drizzle/variants/base/src/routers/admin.router.ts +497 -0
- package/registry/combos/express-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +480 -109
- package/registry/combos/express-drizzle/variants/workspaces/src/create-auth-app.ts +69 -23
- package/registry/combos/express-drizzle/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
- package/registry/combos/express-drizzle/variants/workspaces/src/openapi-workspace.ts +147 -28
- package/registry/combos/express-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +30 -11
- package/registry/combos/express-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +319 -70
- package/registry/combos/express-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +127 -30
- package/registry/combos/express-drizzle/variants/workspaces/src/routers/admin.router.ts +534 -0
- package/registry/combos/express-drizzle/variants/workspaces/src/routers/workspace.router.ts +194 -0
- package/registry/combos/express-drizzle/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +544 -122
- package/registry/combos/express-prisma/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
- package/registry/combos/express-prisma/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +19 -6
- package/registry/combos/{express-drizzle/shared/src → express-prisma/shared/src/middleware}/response-envelope.middleware.ts +15 -3
- package/registry/combos/express-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +27 -9
- package/registry/combos/express-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
- package/registry/combos/express-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
- package/registry/combos/express-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
- package/registry/combos/express-prisma/shared/src/request-context.ts +1 -1
- package/registry/combos/express-prisma/shared/src/routers/auth.router.ts +398 -0
- package/registry/combos/express-prisma/variants/base/src/create-auth-app.ts +56 -21
- package/registry/combos/express-prisma/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
- package/registry/combos/express-prisma/variants/base/src/rbac.defaults.ts +66 -18
- package/registry/combos/express-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
- package/registry/combos/express-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +199 -48
- package/registry/combos/express-prisma/variants/base/src/routers/admin.router.ts +508 -0
- package/registry/combos/express-prisma/variants/base/src/seed.ts +77 -19
- package/registry/combos/express-prisma/variants/base/src/{auth.service.ts → services/auth.service.ts} +454 -105
- package/registry/combos/express-prisma/variants/workspaces/src/create-auth-app.ts +80 -25
- package/registry/combos/express-prisma/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
- package/registry/combos/express-prisma/variants/workspaces/src/openapi-workspace.ts +147 -28
- package/registry/combos/express-prisma/variants/workspaces/src/rbac.defaults.ts +72 -20
- package/registry/combos/express-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
- package/registry/combos/express-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +272 -64
- package/registry/combos/express-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +86 -23
- package/registry/combos/express-prisma/variants/workspaces/src/routers/admin.router.ts +545 -0
- package/registry/combos/express-prisma/variants/workspaces/src/routers/workspace.router.ts +194 -0
- package/registry/combos/express-prisma/variants/workspaces/src/seed.ts +108 -28
- package/registry/combos/express-prisma/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +518 -119
- package/registry/combos/nestjs-drizzle/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
- package/registry/combos/nestjs-drizzle/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
- package/registry/combos/nestjs-drizzle/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
- package/registry/combos/nestjs-drizzle/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
- package/registry/combos/{nestjs-prisma/shared/src → nestjs-drizzle/shared/src/interceptors}/response.interceptor.ts +10 -2
- package/registry/combos/nestjs-drizzle/shared/src/repositories/oauth.repository.ts +63 -0
- package/registry/combos/nestjs-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
- package/registry/combos/nestjs-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
- package/registry/combos/nestjs-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +21 -7
- package/registry/combos/nestjs-drizzle/shared/src/request-context.ts +1 -1
- package/registry/combos/nestjs-drizzle/test/prove-cycle.ts +1097 -307
- package/registry/combos/nestjs-drizzle/variants/base/src/auth.module.ts +44 -20
- package/registry/combos/nestjs-drizzle/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +228 -62
- package/registry/combos/nestjs-drizzle/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
- package/registry/combos/nestjs-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +27 -10
- package/registry/combos/nestjs-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +249 -51
- package/registry/combos/nestjs-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +499 -116
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.module.ts +48 -22
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +269 -70
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +29 -11
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +323 -71
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +131 -30
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/services/auth.service.ts +988 -0
- package/registry/combos/nestjs-prisma/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
- package/registry/combos/nestjs-prisma/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
- package/registry/combos/nestjs-prisma/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
- package/registry/combos/nestjs-prisma/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
- package/registry/combos/{nestjs-drizzle/shared/src → nestjs-prisma/shared/src/interceptors}/response.interceptor.ts +10 -2
- package/registry/combos/nestjs-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +25 -8
- package/registry/combos/nestjs-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
- package/registry/combos/nestjs-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
- package/registry/combos/nestjs-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
- package/registry/combos/nestjs-prisma/shared/src/request-context.ts +1 -1
- package/registry/combos/nestjs-prisma/test/prove-cycle.ts +1097 -307
- package/registry/combos/nestjs-prisma/variants/base/src/auth.module.ts +58 -27
- package/registry/combos/nestjs-prisma/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +469 -110
- package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.gateway.ts → gateways/audit-log.gateway.ts} +22 -6
- package/registry/combos/nestjs-prisma/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
- package/registry/combos/nestjs-prisma/variants/base/src/rbac.defaults.ts +97 -26
- package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
- package/registry/combos/nestjs-prisma/variants/base/src/{country.repository.ts → repositories/country.repository.ts} +98 -25
- package/registry/combos/nestjs-prisma/variants/base/src/{customer.repository.ts → repositories/customer.repository.ts} +106 -27
- package/registry/combos/nestjs-prisma/variants/base/src/{language.repository.ts → repositories/language.repository.ts} +93 -25
- package/registry/combos/nestjs-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +229 -55
- package/registry/combos/nestjs-prisma/variants/base/src/seed.ts +77 -19
- package/registry/combos/nestjs-prisma/variants/base/src/services/auth.service.ts +1064 -0
- package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.module.ts +54 -26
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +544 -128
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
- package/registry/combos/nestjs-prisma/variants/workspaces/src/rbac.defaults.ts +103 -28
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{country.repository.ts → repositories/country.repository.ts} +127 -26
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{customer.repository.ts → repositories/customer.repository.ts} +137 -28
- package/registry/combos/nestjs-prisma/variants/workspaces/src/repositories/language.repository.ts +243 -0
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +314 -74
- package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +94 -24
- package/registry/combos/nestjs-prisma/variants/workspaces/src/seed.ts +108 -28
- package/registry/combos/nestjs-prisma/variants/workspaces/src/services/auth.service.ts +961 -0
- package/registry.json +4 -4
- package/simple-auth-kit.ts +42 -11
- package/registry/combos/express-drizzle/shared/src/auth.router.ts +0 -266
- package/registry/combos/express-drizzle/shared/src/password-reset.repository.ts +0 -35
- package/registry/combos/express-drizzle/variants/base/src/admin.router.ts +0 -307
- package/registry/combos/express-drizzle/variants/workspaces/src/admin.router.ts +0 -323
- package/registry/combos/express-drizzle/variants/workspaces/src/workspace.router.ts +0 -123
- package/registry/combos/express-prisma/shared/src/auth.router.ts +0 -269
- package/registry/combos/express-prisma/shared/src/password-reset.repository.ts +0 -29
- package/registry/combos/express-prisma/variants/base/src/admin.router.ts +0 -314
- package/registry/combos/express-prisma/variants/workspaces/src/admin.router.ts +0 -330
- package/registry/combos/express-prisma/variants/workspaces/src/workspace.router.ts +0 -123
- package/registry/combos/nestjs-drizzle/shared/src/oauth.repository.ts +0 -36
- package/registry/combos/nestjs-drizzle/shared/src/password-reset.repository.ts +0 -29
- package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.service.ts +0 -553
- package/registry/combos/nestjs-prisma/shared/src/password-reset.repository.ts +0 -29
- package/registry/combos/nestjs-prisma/variants/base/src/auth.service.ts +0 -618
- package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.service.ts +0 -509
- package/registry/combos/nestjs-prisma/variants/workspaces/src/language.repository.ts +0 -143
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import type { NextFunction, Request, RequestHandler, Response } from "express";
|
|
3
|
+
import { HttpError } from "../http-error.js";
|
|
4
|
+
import { RbacRepository } from "../repositories/rbac.repository.js";
|
|
5
|
+
import { ability, authenticated, createTieredRouter } from "../route-tiers.js";
|
|
6
|
+
import { WorkspaceRepository } from "../repositories/workspace.repository.js";
|
|
7
|
+
import "../request-context.js";
|
|
8
|
+
|
|
9
|
+
function requireString(value: unknown, field: string): string {
|
|
10
|
+
if (typeof value !== "string" || value.length === 0)
|
|
11
|
+
throw new HttpError(400, `${field} is required`);
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function requireStringArray(value: unknown, field: string): string[] {
|
|
16
|
+
if (!Array.isArray(value) || value.some((v) => typeof v !== "string"))
|
|
17
|
+
throw new HttpError(400, `${field} must be an array of strings`);
|
|
18
|
+
return value as string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface WorkspaceRouterDeps {
|
|
22
|
+
workspaces: WorkspaceRepository;
|
|
23
|
+
rbac: RbacRepository;
|
|
24
|
+
authentication: RequestHandler;
|
|
25
|
+
/** Resolves a workspace when the request names one, and does nothing when it does not. */
|
|
26
|
+
authorization: RequestHandler;
|
|
27
|
+
/** The same resolution, but the `X-Workspace-Id` header is mandatory. */
|
|
28
|
+
workspaceScope: RequestHandler;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Workspace membership. Every route below that acts *inside* a workspace names it with the
|
|
33
|
+
* `X-Workspace-Id` header rather than a path segment — the same rule the `/auth/admin/*` routes
|
|
34
|
+
* follow, so there is exactly one way a request says which workspace it means.
|
|
35
|
+
*
|
|
36
|
+
* Three routes here are deliberately tier 2 — `authenticated()`, any logged-in caller — and the
|
|
37
|
+
* permission catalog mints no slugs for them. That is a decision written on each route, not an
|
|
38
|
+
* omission: the tier is a required argument, so a route with none is a compile error
|
|
39
|
+
* (route-tiers.ts).
|
|
40
|
+
* • `POST /workspaces` and `GET /workspaces` are outside every workspace — any authenticated
|
|
41
|
+
* user may create one or list their own. There is no workspace whose permissions could be
|
|
42
|
+
* checked, and gating them on a permission granted *inside* some other workspace would mean
|
|
43
|
+
* your first workspace could only be created by someone who already had one.
|
|
44
|
+
* • `GET /workspaces/members` is gated on membership itself: the workspace middleware already
|
|
45
|
+
* proved the caller belongs to the workspace, and seeing who else is in a room you are in is
|
|
46
|
+
* not an administrative capability.
|
|
47
|
+
* The three that mutate membership are permission-gated — see `rbac.defaults.ts` for the catalog.
|
|
48
|
+
*
|
|
49
|
+
* Two tiered routers, because the tier's middleware is what enforces the tier: the unscoped
|
|
50
|
+
* routes resolve a workspace only if one is named, while everything under a workspace uses the
|
|
51
|
+
* mandatory resolution. Mounting them in this order matters — the scoped router is consulted
|
|
52
|
+
* first, so `/workspaces/members` reaches it rather than falling through.
|
|
53
|
+
*
|
|
54
|
+
* Replaces the reference combo's `WorkspaceController`.
|
|
55
|
+
*/
|
|
56
|
+
export function createWorkspaceRouter(
|
|
57
|
+
deps: WorkspaceRouterDeps,
|
|
58
|
+
): RequestHandler {
|
|
59
|
+
const { workspaces, rbac, authentication, authorization, workspaceScope } =
|
|
60
|
+
deps;
|
|
61
|
+
|
|
62
|
+
const scoped = createTieredRouter({
|
|
63
|
+
authentication,
|
|
64
|
+
authorization: workspaceScope,
|
|
65
|
+
});
|
|
66
|
+
const unscoped = createTieredRouter({ authentication, authorization });
|
|
67
|
+
|
|
68
|
+
unscoped.route(
|
|
69
|
+
"post",
|
|
70
|
+
"/",
|
|
71
|
+
authenticated(),
|
|
72
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
73
|
+
try {
|
|
74
|
+
const body = req.body as Record<string, unknown>;
|
|
75
|
+
res
|
|
76
|
+
.status(201)
|
|
77
|
+
.json(
|
|
78
|
+
await workspaces.create(
|
|
79
|
+
req.auth!.sub,
|
|
80
|
+
requireString(body.name, "name"),
|
|
81
|
+
),
|
|
82
|
+
);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
next(err);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
unscoped.route(
|
|
90
|
+
"get",
|
|
91
|
+
"/",
|
|
92
|
+
authenticated(),
|
|
93
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
94
|
+
try {
|
|
95
|
+
res.status(200).json(await workspaces.listForUser(req.auth!.sub));
|
|
96
|
+
} catch (err) {
|
|
97
|
+
next(err);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
scoped.route(
|
|
103
|
+
"get",
|
|
104
|
+
"/members",
|
|
105
|
+
authenticated(),
|
|
106
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
107
|
+
try {
|
|
108
|
+
res
|
|
109
|
+
.status(200)
|
|
110
|
+
.json(await workspaces.listMembers(req.authz!.workspaceId));
|
|
111
|
+
} catch (err) {
|
|
112
|
+
next(err);
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
scoped.route(
|
|
118
|
+
"post",
|
|
119
|
+
"/members",
|
|
120
|
+
ability("members:manage"),
|
|
121
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
122
|
+
try {
|
|
123
|
+
const body = req.body as Record<string, unknown>;
|
|
124
|
+
// No `roles` in the body means "whatever this workspace flags as the default role" — the
|
|
125
|
+
// default lives in a row, not in this line.
|
|
126
|
+
const roles =
|
|
127
|
+
body.roles === undefined
|
|
128
|
+
? undefined
|
|
129
|
+
: requireStringArray(body.roles, "roles");
|
|
130
|
+
res
|
|
131
|
+
.status(201)
|
|
132
|
+
.json(
|
|
133
|
+
await workspaces.addMember(
|
|
134
|
+
req.authz!.workspaceId,
|
|
135
|
+
requireString(body.email, "email"),
|
|
136
|
+
roles,
|
|
137
|
+
),
|
|
138
|
+
);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
next(err);
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
scoped.route(
|
|
146
|
+
"put",
|
|
147
|
+
"/members/:memberId/roles",
|
|
148
|
+
ability("roles:assign"),
|
|
149
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
150
|
+
try {
|
|
151
|
+
const memberId = requireString(req.params.memberId, "memberId");
|
|
152
|
+
if (memberId === req.authz!.memberId)
|
|
153
|
+
throw new HttpError(403, "cannot change your own roles");
|
|
154
|
+
const body = req.body as Record<string, unknown>;
|
|
155
|
+
res
|
|
156
|
+
.status(200)
|
|
157
|
+
.json(
|
|
158
|
+
await rbac.setMemberRoles(
|
|
159
|
+
req.authz!.workspaceId,
|
|
160
|
+
memberId,
|
|
161
|
+
requireStringArray(body.roles, "roles"),
|
|
162
|
+
),
|
|
163
|
+
);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
next(err);
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
scoped.route(
|
|
171
|
+
"delete",
|
|
172
|
+
"/members/:memberId",
|
|
173
|
+
ability("members:manage"),
|
|
174
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
175
|
+
try {
|
|
176
|
+
const memberId = requireString(req.params.memberId, "memberId");
|
|
177
|
+
if (memberId === req.authz!.memberId)
|
|
178
|
+
throw new HttpError(
|
|
179
|
+
403,
|
|
180
|
+
"cannot remove yourself from a workspace you administer",
|
|
181
|
+
);
|
|
182
|
+
await workspaces.removeMember(req.authz!.workspaceId, memberId);
|
|
183
|
+
res.status(200).json({ ok: true });
|
|
184
|
+
} catch (err) {
|
|
185
|
+
next(err);
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const router = Router();
|
|
191
|
+
router.use(scoped.handler);
|
|
192
|
+
router.use(unscoped.handler);
|
|
193
|
+
return router;
|
|
194
|
+
}
|
|
@@ -30,20 +30,34 @@
|
|
|
30
30
|
import { PrismaPg } from "@prisma/adapter-pg";
|
|
31
31
|
import { hashPassword } from "@/lib/auth/core/crypto.js";
|
|
32
32
|
import { PrismaClient } from "../generated/prisma/client.js";
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
DEFAULT_ROLES,
|
|
35
|
+
PERMISSION_SLUGS,
|
|
36
|
+
provisionDefaultRoles,
|
|
37
|
+
SEED_SUPERADMIN_ROLES,
|
|
38
|
+
WORKSPACE_CREATOR_ROLES,
|
|
39
|
+
} from "./rbac.defaults.js";
|
|
34
40
|
|
|
35
41
|
const DEFAULT_WORKSPACE_NAME = "Default workspace";
|
|
36
42
|
|
|
37
43
|
function requireEnv(name: string): string {
|
|
38
44
|
const value = process.env[name];
|
|
39
|
-
if (!value)
|
|
45
|
+
if (!value)
|
|
46
|
+
throw new Error(
|
|
47
|
+
`${name} is not set — the seeder cannot reach the database without it`,
|
|
48
|
+
);
|
|
40
49
|
return value;
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
/** Workspace names are not unique in the schema, so re-running adopts the one it made last time rather than making another. */
|
|
44
|
-
async function seedWorkspace(
|
|
53
|
+
async function seedWorkspace(
|
|
54
|
+
prisma: PrismaClient,
|
|
55
|
+
): Promise<{ id: bigint; name: string }> {
|
|
45
56
|
const name = process.env["SEED_WORKSPACE_NAME"] || DEFAULT_WORKSPACE_NAME;
|
|
46
|
-
const existing = await prisma.workspace.findFirst({
|
|
57
|
+
const existing = await prisma.workspace.findFirst({
|
|
58
|
+
where: { name },
|
|
59
|
+
orderBy: { createdAt: "asc" },
|
|
60
|
+
});
|
|
47
61
|
if (existing) {
|
|
48
62
|
console.log(`workspace: "${name}" already exists (${existing.id})`);
|
|
49
63
|
return existing;
|
|
@@ -54,70 +68,134 @@ async function seedWorkspace(prisma: PrismaClient): Promise<{ id: bigint; name:
|
|
|
54
68
|
}
|
|
55
69
|
|
|
56
70
|
/** The catalog and this workspace's roles come from rbac.defaults.ts; this only reports what it wrote. */
|
|
57
|
-
async function seedRbacDefaults(
|
|
71
|
+
async function seedRbacDefaults(
|
|
72
|
+
prisma: PrismaClient,
|
|
73
|
+
workspaceId: bigint,
|
|
74
|
+
): Promise<void> {
|
|
58
75
|
await provisionDefaultRoles(prisma, workspaceId);
|
|
59
76
|
console.log(`permissions: ${PERMISSION_SLUGS.length} slug(s) in the catalog`);
|
|
60
|
-
for (const role of DEFAULT_ROLES)
|
|
77
|
+
for (const role of DEFAULT_ROLES)
|
|
78
|
+
console.log(
|
|
79
|
+
`role "${role.slug}": ${role.permissions.length} permission(s)${role.isDefault ? " (new-member default)" : ""}`,
|
|
80
|
+
);
|
|
61
81
|
|
|
62
82
|
// Slugs that exist in the database but not in this build's catalog. They are not an error —
|
|
63
83
|
// `POST /auth/admin/permissions` exists so a deployment can define its own, and a grant can
|
|
64
84
|
// create one — but a slug no route names grants nothing, so a typo would otherwise be a
|
|
65
85
|
// permission that mysteriously never works. Printing them is what makes it visible.
|
|
66
|
-
const rows = await prisma.permission.findMany({
|
|
67
|
-
|
|
68
|
-
|
|
86
|
+
const rows = await prisma.permission.findMany({
|
|
87
|
+
select: { slug: true, isActive: true },
|
|
88
|
+
});
|
|
89
|
+
const unknown = rows.filter(
|
|
90
|
+
(row) => !(PERMISSION_SLUGS as string[]).includes(row.slug),
|
|
91
|
+
);
|
|
92
|
+
if (unknown.length)
|
|
93
|
+
console.log(
|
|
94
|
+
`permissions: ${unknown.length} slug(s) outside this build's catalog (no route names them): ${unknown.map((r) => r.slug).join(", ")}`,
|
|
95
|
+
);
|
|
69
96
|
const inactive = rows.filter((row) => !row.isActive);
|
|
70
|
-
if (inactive.length)
|
|
97
|
+
if (inactive.length)
|
|
98
|
+
console.log(
|
|
99
|
+
`permissions: ${inactive.length} deactivated, granting nothing: ${inactive.map((r) => r.slug).join(", ")}`,
|
|
100
|
+
);
|
|
71
101
|
}
|
|
72
102
|
|
|
73
103
|
/** The user is the authentication principal; the membership below is the authorization one. */
|
|
74
|
-
async function seedAdminUser(
|
|
104
|
+
async function seedAdminUser(
|
|
105
|
+
prisma: PrismaClient,
|
|
106
|
+
workspace: { id: bigint; name: string },
|
|
107
|
+
): Promise<void> {
|
|
75
108
|
const email = process.env["SEED_ADMIN_EMAIL"];
|
|
76
109
|
const password = process.env["SEED_ADMIN_PASSWORD"];
|
|
77
110
|
if (!email || !password) {
|
|
78
|
-
console.log(
|
|
79
|
-
|
|
111
|
+
console.log(
|
|
112
|
+
`admin: skipped — set SEED_ADMIN_EMAIL and SEED_ADMIN_PASSWORD to create one (there is no default password)`,
|
|
113
|
+
);
|
|
114
|
+
console.log(
|
|
115
|
+
`admin: "${workspace.name}" has no members until you re-run with them set`,
|
|
116
|
+
);
|
|
80
117
|
return;
|
|
81
118
|
}
|
|
82
119
|
|
|
83
120
|
const existing = await prisma.user.findUnique({ where: { email } });
|
|
84
121
|
// Their password may have been changed since; rewriting it here would silently reset it.
|
|
85
|
-
const user =
|
|
86
|
-
|
|
122
|
+
const user =
|
|
123
|
+
existing ??
|
|
124
|
+
(await prisma.user.create({
|
|
125
|
+
data: { email, passwordHash: await hashPassword(password) },
|
|
126
|
+
}));
|
|
127
|
+
console.log(
|
|
128
|
+
existing
|
|
129
|
+
? `admin: ${email} already exists — password left unchanged`
|
|
130
|
+
: `admin: created ${email}`,
|
|
131
|
+
);
|
|
87
132
|
|
|
88
133
|
const membership = await prisma.workspaceMember.upsert({
|
|
89
|
-
where: {
|
|
134
|
+
where: {
|
|
135
|
+
userId_workspaceId: { userId: user.id, workspaceId: workspace.id },
|
|
136
|
+
},
|
|
90
137
|
create: { userId: user.id, workspaceId: workspace.id },
|
|
91
138
|
update: {},
|
|
92
139
|
});
|
|
93
|
-
const roles = await prisma.role.findMany({
|
|
94
|
-
|
|
95
|
-
|
|
140
|
+
const roles = await prisma.role.findMany({
|
|
141
|
+
where: { workspaceId: workspace.id, slug: { in: WORKSPACE_CREATOR_ROLES } },
|
|
142
|
+
select: { id: true, slug: true },
|
|
143
|
+
});
|
|
144
|
+
await prisma.roleMember.createMany({
|
|
145
|
+
data: roles.map((role) => ({ memberId: membership.id, roleId: role.id })),
|
|
146
|
+
skipDuplicates: true,
|
|
147
|
+
});
|
|
148
|
+
console.log(
|
|
149
|
+
`admin: member of "${workspace.name}" with roles ${roles.map((r) => r.slug).join(", ")}`,
|
|
150
|
+
);
|
|
96
151
|
}
|
|
97
152
|
|
|
98
153
|
/** A second seeded account and membership, held by the "superadmin" role — same full-catalog authority as admin. */
|
|
99
|
-
async function seedSuperAdminUser(
|
|
154
|
+
async function seedSuperAdminUser(
|
|
155
|
+
prisma: PrismaClient,
|
|
156
|
+
workspace: { id: bigint; name: string },
|
|
157
|
+
): Promise<void> {
|
|
100
158
|
const email = process.env["SEED_SUPERADMIN_EMAIL"];
|
|
101
159
|
const password = process.env["SEED_SUPERADMIN_PASSWORD"];
|
|
102
160
|
if (!email || !password) {
|
|
103
|
-
console.log(
|
|
161
|
+
console.log(
|
|
162
|
+
`super admin: skipped — set SEED_SUPERADMIN_EMAIL and SEED_SUPERADMIN_PASSWORD to create one (there is no default password)`,
|
|
163
|
+
);
|
|
104
164
|
return;
|
|
105
165
|
}
|
|
106
166
|
const username = process.env["SEED_SUPERADMIN_USERNAME"] || undefined;
|
|
107
167
|
|
|
108
168
|
const existing = await prisma.user.findUnique({ where: { email } });
|
|
109
169
|
// Their password may have been changed since; rewriting it here would silently reset it.
|
|
110
|
-
const user =
|
|
111
|
-
|
|
170
|
+
const user =
|
|
171
|
+
existing ??
|
|
172
|
+
(await prisma.user.create({
|
|
173
|
+
data: { email, username, passwordHash: await hashPassword(password) },
|
|
174
|
+
}));
|
|
175
|
+
console.log(
|
|
176
|
+
existing
|
|
177
|
+
? `super admin: ${email} already exists — password left unchanged`
|
|
178
|
+
: `super admin: created ${email}${username ? ` (username ${username})` : ""}`,
|
|
179
|
+
);
|
|
112
180
|
|
|
113
181
|
const membership = await prisma.workspaceMember.upsert({
|
|
114
|
-
where: {
|
|
182
|
+
where: {
|
|
183
|
+
userId_workspaceId: { userId: user.id, workspaceId: workspace.id },
|
|
184
|
+
},
|
|
115
185
|
create: { userId: user.id, workspaceId: workspace.id },
|
|
116
186
|
update: {},
|
|
117
187
|
});
|
|
118
|
-
const roles = await prisma.role.findMany({
|
|
119
|
-
|
|
120
|
-
|
|
188
|
+
const roles = await prisma.role.findMany({
|
|
189
|
+
where: { workspaceId: workspace.id, slug: { in: SEED_SUPERADMIN_ROLES } },
|
|
190
|
+
select: { id: true, slug: true },
|
|
191
|
+
});
|
|
192
|
+
await prisma.roleMember.createMany({
|
|
193
|
+
data: roles.map((role) => ({ memberId: membership.id, roleId: role.id })),
|
|
194
|
+
skipDuplicates: true,
|
|
195
|
+
});
|
|
196
|
+
console.log(
|
|
197
|
+
`super admin: member of "${workspace.name}" with roles ${roles.map((r) => r.slug).join(", ")}`,
|
|
198
|
+
);
|
|
121
199
|
}
|
|
122
200
|
|
|
123
201
|
async function main(): Promise<void> {
|
|
@@ -129,7 +207,9 @@ async function main(): Promise<void> {
|
|
|
129
207
|
// No .env here — the environment is expected to carry the variables itself (docker, CI, ...).
|
|
130
208
|
}
|
|
131
209
|
|
|
132
|
-
const prisma = new PrismaClient({
|
|
210
|
+
const prisma = new PrismaClient({
|
|
211
|
+
adapter: new PrismaPg({ connectionString: requireEnv("DATABASE_URL") }),
|
|
212
|
+
});
|
|
133
213
|
try {
|
|
134
214
|
const workspace = await seedWorkspace(prisma);
|
|
135
215
|
await seedRbacDefaults(prisma, workspace.id);
|