@simple-auth-kit/cli 1.4.2 → 1.5.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/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 +57 -22
- 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 +67 -19
- 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 +78 -20
- 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 +81 -26
- 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 +73 -21
- 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 +109 -29
- 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 +59 -28
- 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 +98 -27
- 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 +78 -20
- 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 +55 -27
- 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 +104 -29
- 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 +109 -29
- package/registry/combos/nestjs-prisma/variants/workspaces/src/services/auth.service.ts +961 -0
- package/registry.json +6 -4
- package/simple-auth-kit.ts +582 -124
- 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
|
@@ -1,330 +0,0 @@
|
|
|
1
|
-
import type { NextFunction, Request, RequestHandler, Response } from "express";
|
|
2
|
-
import { AuthService } from "./auth.service.js";
|
|
3
|
-
import { HttpError } from "./http-error.js";
|
|
4
|
-
import { ability, createTieredRouter } from "./route-tiers.js";
|
|
5
|
-
import { WorkspaceRepository } from "./workspace.repository.js";
|
|
6
|
-
import { hashPassword } from "@/lib/auth/core/crypto.js";
|
|
7
|
-
import "./request-context.js";
|
|
8
|
-
|
|
9
|
-
function requireString(value: unknown, field: string): string {
|
|
10
|
-
if (typeof value !== "string" || value.length === 0) throw new HttpError(400, `${field} is required`);
|
|
11
|
-
return value;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const optionalString = (value: unknown): string | undefined => (typeof value === "string" ? value : undefined);
|
|
15
|
-
|
|
16
|
-
export interface AdminRouterDeps {
|
|
17
|
-
auth: AuthService;
|
|
18
|
-
workspaces: WorkspaceRepository;
|
|
19
|
-
authentication: RequestHandler;
|
|
20
|
-
/** The mandatory workspace resolution — every route here acts inside one. See authz.middleware.ts. */
|
|
21
|
-
workspaceScope: RequestHandler;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Administration *of one workspace*, named by the `X-Workspace-Id` header. Authority is a
|
|
26
|
-
* permission, never a role name: the workspace middleware resolves the caller's membership and the
|
|
27
|
-
* roles and permissions it carries **in that workspace**, builds the CASL ability from them, and
|
|
28
|
-
* each route's `ability(...)` tier checks its slugs against it. There is no role-based bypass —
|
|
29
|
-
* holding a role called "admin" that carries no permissions gets exactly the same 403 as holding no
|
|
30
|
-
* role at all, which is why `npm run seed` (which provisions the catalog and the roles that carry
|
|
31
|
-
* it) is a prerequisite for this router doing anything.
|
|
32
|
-
*
|
|
33
|
-
* Every handler passes `req.authz` down to the service, and every service method reaches the
|
|
34
|
-
* database through a workspace-scoped query. That is the security property this model rests on: an
|
|
35
|
-
* admin of one workspace has no expressible way to name a row in another.
|
|
36
|
-
*
|
|
37
|
-
* Each route says what it demands; the rows say who is granted it. The slugs below are checked at
|
|
38
|
-
* compile time against `PERMISSION_CATALOG` in `rbac.defaults.ts`, while who holds them — and
|
|
39
|
-
* whether the permission is active at all — is edited through this very router and takes effect on
|
|
40
|
-
* the next request. `GET /auth/me` returns the caller's slugs, and a console rebuilds the same
|
|
41
|
-
* ability from them with the same function the middleware used.
|
|
42
|
-
*
|
|
43
|
-
* Replaces the reference combo's `AdminController`. Mounted under `/auth/admin` by
|
|
44
|
-
* create-auth-app.ts; `createTieredRouter` stands in for its
|
|
45
|
-
* `@UseGuards(AuthGuard, AuthzGuard, AbilityGuard)` — see route-tiers.ts for why the tier is a
|
|
46
|
-
* required argument rather than a decorator.
|
|
47
|
-
*/
|
|
48
|
-
export function createAdminRouter(deps: AdminRouterDeps): RequestHandler {
|
|
49
|
-
const { auth, workspaces, authentication, workspaceScope } = deps;
|
|
50
|
-
const admin = createTieredRouter({ authentication, authorization: workspaceScope });
|
|
51
|
-
|
|
52
|
-
admin.route("get", "/users", ability("users:read"), async (req: Request, res: Response, next: NextFunction) => {
|
|
53
|
-
try {
|
|
54
|
-
const { search, page, limit } = req.query as Record<string, string | undefined>;
|
|
55
|
-
res.status(200).json(await auth.listUsers(req.authz!, { search, page: page ? Number(page) : undefined, limit: limit ? Number(limit) : undefined }));
|
|
56
|
-
} catch (err) {
|
|
57
|
-
next(err);
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
// No invitation email — the account is usable immediately with the password given here.
|
|
62
|
-
admin.route("post", "/users", ability("users:manage"), async (req: Request, res: Response, next: NextFunction) => {
|
|
63
|
-
try {
|
|
64
|
-
const body = req.body as Record<string, unknown>;
|
|
65
|
-
const passwordHash = await hashPassword(requireString(body.password, "password"));
|
|
66
|
-
const member = await workspaces.createMember(
|
|
67
|
-
req.authz!.workspaceId,
|
|
68
|
-
{
|
|
69
|
-
email: requireString(body.email, "email"),
|
|
70
|
-
passwordHash,
|
|
71
|
-
firstName: optionalString(body.firstName),
|
|
72
|
-
lastName: optionalString(body.lastName),
|
|
73
|
-
displayName: optionalString(body.displayName),
|
|
74
|
-
phone: optionalString(body.phone),
|
|
75
|
-
username: optionalString(body.username),
|
|
76
|
-
roles: Array.isArray(body.roles) ? body.roles.filter((role): role is string => typeof role === "string") : undefined,
|
|
77
|
-
},
|
|
78
|
-
req.auth!.sub,
|
|
79
|
-
);
|
|
80
|
-
res.status(201).json(await auth.getUser(req.authz!, member.userId));
|
|
81
|
-
} catch (err) {
|
|
82
|
-
next(err);
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
admin.route("get", "/users/:userId", ability("users:read"), async (req: Request, res: Response, next: NextFunction) => {
|
|
87
|
-
try {
|
|
88
|
-
res.status(200).json(await auth.getUser(req.authz!, requireString(req.params.userId, "userId")));
|
|
89
|
-
} catch (err) {
|
|
90
|
-
next(err);
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
// Profile fields only — email is the login identifier and is not editable here.
|
|
95
|
-
admin.route("patch", "/users/:userId", ability("users:manage"), async (req: Request, res: Response, next: NextFunction) => {
|
|
96
|
-
try {
|
|
97
|
-
const userId = requireString(req.params.userId, "userId");
|
|
98
|
-
const body = req.body as Record<string, unknown>;
|
|
99
|
-
res.status(200).json(
|
|
100
|
-
await auth.updateUser(
|
|
101
|
-
req.authz!,
|
|
102
|
-
userId,
|
|
103
|
-
{
|
|
104
|
-
firstName: body.firstName === null ? null : optionalString(body.firstName),
|
|
105
|
-
lastName: body.lastName === null ? null : optionalString(body.lastName),
|
|
106
|
-
displayName: body.displayName === null ? null : optionalString(body.displayName),
|
|
107
|
-
phone: body.phone === null ? null : optionalString(body.phone),
|
|
108
|
-
username: body.username === null ? null : optionalString(body.username),
|
|
109
|
-
photo: body.photo === null ? null : optionalString(body.photo),
|
|
110
|
-
},
|
|
111
|
-
req.auth!.sub,
|
|
112
|
-
),
|
|
113
|
-
);
|
|
114
|
-
} catch (err) {
|
|
115
|
-
next(err);
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// Soft-delete: the row survives for audit purposes, stops appearing in listings, and can no
|
|
120
|
-
// longer authenticate. This disables the account across every workspace it belongs to, not
|
|
121
|
-
// just this one — the same reach `block` already has.
|
|
122
|
-
admin.route("delete", "/users/:userId", ability("users:manage"), async (req: Request, res: Response, next: NextFunction) => {
|
|
123
|
-
try {
|
|
124
|
-
const userId = requireString(req.params.userId, "userId");
|
|
125
|
-
if (userId === req.auth!.sub) throw new HttpError(403, "cannot delete your own account");
|
|
126
|
-
await auth.deleteUser(req.authz!, userId, req.auth!.sub, optionalString((req.body as Record<string, unknown> | undefined)?.reason));
|
|
127
|
-
res.status(200).json({ ok: true });
|
|
128
|
-
} catch (err) {
|
|
129
|
-
next(err);
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
admin.route("get", "/audit-log", ability("audit-log:read"), async (req: Request, res: Response, next: NextFunction) => {
|
|
134
|
-
try {
|
|
135
|
-
const { userId, action, since, until, page, limit } = req.query as Record<string, string | undefined>;
|
|
136
|
-
res.status(200).json(
|
|
137
|
-
await auth.listAuditLog(req.authz!, { userId, action, since, until, page: page ? Number(page) : undefined, limit: limit ? Number(limit) : undefined }),
|
|
138
|
-
);
|
|
139
|
-
} catch (err) {
|
|
140
|
-
next(err);
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
admin.route("get", "/permissions", ability("permissions:read"), async (_req: Request, res: Response, next: NextFunction) => {
|
|
145
|
-
try {
|
|
146
|
-
res.status(200).json(await auth.listPermissions());
|
|
147
|
-
} catch (err) {
|
|
148
|
-
next(err);
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
admin.route("post", "/permissions", ability("permissions:define"), async (req: Request, res: Response, next: NextFunction) => {
|
|
153
|
-
try {
|
|
154
|
-
const body = req.body as Record<string, unknown>;
|
|
155
|
-
res.status(201).json(
|
|
156
|
-
await auth.definePermission(
|
|
157
|
-
{
|
|
158
|
-
slug: requireString(body.slug, "slug"),
|
|
159
|
-
name: optionalString(body.name),
|
|
160
|
-
displayName: optionalString(body.displayName),
|
|
161
|
-
description: body.description === null ? null : optionalString(body.description),
|
|
162
|
-
group: optionalString(body.group),
|
|
163
|
-
groupOrder: typeof body.groupOrder === "number" ? body.groupOrder : undefined,
|
|
164
|
-
order: typeof body.order === "number" ? body.order : undefined,
|
|
165
|
-
isActive: typeof body.isActive === "boolean" ? body.isActive : undefined,
|
|
166
|
-
},
|
|
167
|
-
req.auth!.sub,
|
|
168
|
-
),
|
|
169
|
-
);
|
|
170
|
-
} catch (err) {
|
|
171
|
-
next(err);
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
admin.route("get", "/roles", ability("roles:manage"), async (req: Request, res: Response, next: NextFunction) => {
|
|
176
|
-
try {
|
|
177
|
-
res.status(200).json(await auth.listRoles(req.authz!));
|
|
178
|
-
} catch (err) {
|
|
179
|
-
next(err);
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
admin.route("post", "/roles", ability("roles:manage"), async (req: Request, res: Response, next: NextFunction) => {
|
|
184
|
-
try {
|
|
185
|
-
const body = req.body as Record<string, unknown>;
|
|
186
|
-
res.status(201).json(
|
|
187
|
-
await auth.createRole(
|
|
188
|
-
req.authz!,
|
|
189
|
-
{
|
|
190
|
-
slug: requireString(body.slug, "slug"),
|
|
191
|
-
name: optionalString(body.name),
|
|
192
|
-
displayName: optionalString(body.displayName),
|
|
193
|
-
description: optionalString(body.description) ?? null,
|
|
194
|
-
},
|
|
195
|
-
req.auth!.sub,
|
|
196
|
-
),
|
|
197
|
-
);
|
|
198
|
-
} catch (err) {
|
|
199
|
-
next(err);
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
admin.route("patch", "/roles/:roleId", ability("roles:manage"), async (req: Request, res: Response, next: NextFunction) => {
|
|
204
|
-
try {
|
|
205
|
-
const roleId = requireString(req.params.roleId, "roleId");
|
|
206
|
-
const body = req.body as Record<string, unknown>;
|
|
207
|
-
res.status(200).json(
|
|
208
|
-
await auth.updateRole(
|
|
209
|
-
req.authz!,
|
|
210
|
-
roleId,
|
|
211
|
-
{
|
|
212
|
-
name: optionalString(body.name),
|
|
213
|
-
displayName: optionalString(body.displayName),
|
|
214
|
-
description: body.description === null ? null : optionalString(body.description),
|
|
215
|
-
isActive: typeof body.isActive === "boolean" ? body.isActive : undefined,
|
|
216
|
-
},
|
|
217
|
-
req.auth!.sub,
|
|
218
|
-
),
|
|
219
|
-
);
|
|
220
|
-
} catch (err) {
|
|
221
|
-
next(err);
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
// Soft-delete: existing assignments are left in place rather than cascade-deleted, and the role
|
|
226
|
-
// simply stops being resolved.
|
|
227
|
-
admin.route("delete", "/roles/:roleId", ability("roles:manage"), async (req: Request, res: Response, next: NextFunction) => {
|
|
228
|
-
try {
|
|
229
|
-
const roleId = requireString(req.params.roleId, "roleId");
|
|
230
|
-
await auth.deleteRole(req.authz!, roleId, req.auth!.sub, optionalString((req.body as Record<string, unknown> | undefined)?.reason));
|
|
231
|
-
res.status(200).json({ ok: true });
|
|
232
|
-
} catch (err) {
|
|
233
|
-
next(err);
|
|
234
|
-
}
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
admin.route("post", "/roles/:roleId/permissions", ability("roles:manage"), async (req: Request, res: Response, next: NextFunction) => {
|
|
238
|
-
try {
|
|
239
|
-
const body = req.body as Record<string, unknown>;
|
|
240
|
-
await auth.attachPermissionToRole(req.authz!, requireString(req.params.roleId, "roleId"), requireString(body.permission, "permission"), req.auth!.sub);
|
|
241
|
-
res.status(201).json({ ok: true });
|
|
242
|
-
} catch (err) {
|
|
243
|
-
next(err);
|
|
244
|
-
}
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
admin.route("post", "/users/:userId/roles", ability("roles:assign"), async (req: Request, res: Response, next: NextFunction) => {
|
|
248
|
-
try {
|
|
249
|
-
const body = req.body as Record<string, unknown>;
|
|
250
|
-
await auth.assignRole(req.authz!, requireString(req.params.userId, "userId"), requireString(body.role, "role"));
|
|
251
|
-
res.status(201).json({ ok: true });
|
|
252
|
-
} catch (err) {
|
|
253
|
-
next(err);
|
|
254
|
-
}
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
admin.route("post", "/users/:userId/roles/:roleSlug/revoke", ability("roles:assign"), async (req: Request, res: Response, next: NextFunction) => {
|
|
258
|
-
try {
|
|
259
|
-
const userId = requireString(req.params.userId, "userId");
|
|
260
|
-
// Revoking your own `admin` would strip the very permission that let you call this,
|
|
261
|
-
// with no route back in. Assigning to yourself is fine — it can't lock anyone out.
|
|
262
|
-
if (userId === req.auth!.sub) throw new HttpError(403, "cannot change your own roles");
|
|
263
|
-
await auth.revokeRole(req.authz!, userId, requireString(req.params.roleSlug, "roleSlug"));
|
|
264
|
-
res.status(201).json({ ok: true });
|
|
265
|
-
} catch (err) {
|
|
266
|
-
next(err);
|
|
267
|
-
}
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
admin.route("post", "/users/:userId/permissions", ability("permissions:grant"), async (req: Request, res: Response, next: NextFunction) => {
|
|
271
|
-
try {
|
|
272
|
-
const body = req.body as Record<string, unknown>;
|
|
273
|
-
await auth.grantPermission(req.authz!, requireString(req.params.userId, "userId"), requireString(body.permission, "permission"), req.auth!.sub);
|
|
274
|
-
res.status(201).json({ ok: true });
|
|
275
|
-
} catch (err) {
|
|
276
|
-
next(err);
|
|
277
|
-
}
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
admin.route("post", "/users/:userId/permissions/:permissionSlug/revoke", ability("permissions:grant"), async (req: Request, res: Response, next: NextFunction) => {
|
|
281
|
-
try {
|
|
282
|
-
await auth.revokePermission(req.authz!, requireString(req.params.userId, "userId"), requireString(req.params.permissionSlug, "permissionSlug"));
|
|
283
|
-
res.status(201).json({ ok: true });
|
|
284
|
-
} catch (err) {
|
|
285
|
-
next(err);
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
admin.route("post", "/users/:userId/block", ability("users:block"), async (req: Request, res: Response, next: NextFunction) => {
|
|
290
|
-
try {
|
|
291
|
-
const userId = requireString(req.params.userId, "userId");
|
|
292
|
-
if (userId === req.auth!.sub) throw new HttpError(403, "cannot block your own account");
|
|
293
|
-
await auth.block(req.authz!, userId, { userId: req.auth!.sub, ip: req.ip });
|
|
294
|
-
res.status(201).json({ ok: true });
|
|
295
|
-
} catch (err) {
|
|
296
|
-
next(err);
|
|
297
|
-
}
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
admin.route("post", "/users/:userId/unblock", ability("users:block"), async (req: Request, res: Response, next: NextFunction) => {
|
|
301
|
-
try {
|
|
302
|
-
await auth.unblock(req.authz!, requireString(req.params.userId, "userId"), { userId: req.auth!.sub, ip: req.ip });
|
|
303
|
-
res.status(201).json({ ok: true });
|
|
304
|
-
} catch (err) {
|
|
305
|
-
next(err);
|
|
306
|
-
}
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
admin.route("post", "/users/:userId/deactivate", ability("users:block"), async (req: Request, res: Response, next: NextFunction) => {
|
|
310
|
-
try {
|
|
311
|
-
const userId = requireString(req.params.userId, "userId");
|
|
312
|
-
if (userId === req.auth!.sub) throw new HttpError(403, "cannot deactivate your own account");
|
|
313
|
-
await auth.deactivate(req.authz!, userId, { userId: req.auth!.sub, ip: req.ip });
|
|
314
|
-
res.status(201).json({ ok: true });
|
|
315
|
-
} catch (err) {
|
|
316
|
-
next(err);
|
|
317
|
-
}
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
admin.route("post", "/users/:userId/activate", ability("users:block"), async (req: Request, res: Response, next: NextFunction) => {
|
|
321
|
-
try {
|
|
322
|
-
await auth.activate(req.authz!, requireString(req.params.userId, "userId"), { userId: req.auth!.sub, ip: req.ip });
|
|
323
|
-
res.status(201).json({ ok: true });
|
|
324
|
-
} catch (err) {
|
|
325
|
-
next(err);
|
|
326
|
-
}
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
return admin.handler;
|
|
330
|
-
}
|
|
@@ -1,123 +0,0 @@
|
|
|
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 "./rbac.repository.js";
|
|
5
|
-
import { ability, authenticated, createTieredRouter } from "./route-tiers.js";
|
|
6
|
-
import { WorkspaceRepository } from "./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) throw new HttpError(400, `${field} is required`);
|
|
11
|
-
return value;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function requireStringArray(value: unknown, field: string): string[] {
|
|
15
|
-
if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) throw new HttpError(400, `${field} must be an array of strings`);
|
|
16
|
-
return value as string[];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface WorkspaceRouterDeps {
|
|
20
|
-
workspaces: WorkspaceRepository;
|
|
21
|
-
rbac: RbacRepository;
|
|
22
|
-
authentication: RequestHandler;
|
|
23
|
-
/** Resolves a workspace when the request names one, and does nothing when it does not. */
|
|
24
|
-
authorization: RequestHandler;
|
|
25
|
-
/** The same resolution, but the `X-Workspace-Id` header is mandatory. */
|
|
26
|
-
workspaceScope: RequestHandler;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Workspace membership. Every route below that acts *inside* a workspace names it with the
|
|
31
|
-
* `X-Workspace-Id` header rather than a path segment — the same rule the `/auth/admin/*` routes
|
|
32
|
-
* follow, so there is exactly one way a request says which workspace it means.
|
|
33
|
-
*
|
|
34
|
-
* Three routes here are deliberately tier 2 — `authenticated()`, any logged-in caller — and the
|
|
35
|
-
* permission catalog mints no slugs for them. That is a decision written on each route, not an
|
|
36
|
-
* omission: the tier is a required argument, so a route with none is a compile error
|
|
37
|
-
* (route-tiers.ts).
|
|
38
|
-
* • `POST /workspaces` and `GET /workspaces` are outside every workspace — any authenticated
|
|
39
|
-
* user may create one or list their own. There is no workspace whose permissions could be
|
|
40
|
-
* checked, and gating them on a permission granted *inside* some other workspace would mean
|
|
41
|
-
* your first workspace could only be created by someone who already had one.
|
|
42
|
-
* • `GET /workspaces/members` is gated on membership itself: the workspace middleware already
|
|
43
|
-
* proved the caller belongs to the workspace, and seeing who else is in a room you are in is
|
|
44
|
-
* not an administrative capability.
|
|
45
|
-
* The three that mutate membership are permission-gated — see `rbac.defaults.ts` for the catalog.
|
|
46
|
-
*
|
|
47
|
-
* Two tiered routers, because the tier's middleware is what enforces the tier: the unscoped
|
|
48
|
-
* routes resolve a workspace only if one is named, while everything under a workspace uses the
|
|
49
|
-
* mandatory resolution. Mounting them in this order matters — the scoped router is consulted
|
|
50
|
-
* first, so `/workspaces/members` reaches it rather than falling through.
|
|
51
|
-
*
|
|
52
|
-
* Replaces the reference combo's `WorkspaceController`.
|
|
53
|
-
*/
|
|
54
|
-
export function createWorkspaceRouter(deps: WorkspaceRouterDeps): RequestHandler {
|
|
55
|
-
const { workspaces, rbac, authentication, authorization, workspaceScope } = deps;
|
|
56
|
-
|
|
57
|
-
const scoped = createTieredRouter({ authentication, authorization: workspaceScope });
|
|
58
|
-
const unscoped = createTieredRouter({ authentication, authorization });
|
|
59
|
-
|
|
60
|
-
unscoped.route("post", "/", authenticated(), async (req: Request, res: Response, next: NextFunction) => {
|
|
61
|
-
try {
|
|
62
|
-
const body = req.body as Record<string, unknown>;
|
|
63
|
-
res.status(201).json(await workspaces.create(req.auth!.sub, requireString(body.name, "name")));
|
|
64
|
-
} catch (err) {
|
|
65
|
-
next(err);
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
unscoped.route("get", "/", authenticated(), async (req: Request, res: Response, next: NextFunction) => {
|
|
70
|
-
try {
|
|
71
|
-
res.status(200).json(await workspaces.listForUser(req.auth!.sub));
|
|
72
|
-
} catch (err) {
|
|
73
|
-
next(err);
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
scoped.route("get", "/members", authenticated(), async (req: Request, res: Response, next: NextFunction) => {
|
|
78
|
-
try {
|
|
79
|
-
res.status(200).json(await workspaces.listMembers(req.authz!.workspaceId));
|
|
80
|
-
} catch (err) {
|
|
81
|
-
next(err);
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
scoped.route("post", "/members", ability("members:manage"), async (req: Request, res: Response, next: NextFunction) => {
|
|
86
|
-
try {
|
|
87
|
-
const body = req.body as Record<string, unknown>;
|
|
88
|
-
// No `roles` in the body means "whatever this workspace flags as the default role" — the
|
|
89
|
-
// default lives in a row, not in this line.
|
|
90
|
-
const roles = body.roles === undefined ? undefined : requireStringArray(body.roles, "roles");
|
|
91
|
-
res.status(201).json(await workspaces.addMember(req.authz!.workspaceId, requireString(body.email, "email"), roles));
|
|
92
|
-
} catch (err) {
|
|
93
|
-
next(err);
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
scoped.route("put", "/members/:memberId/roles", ability("roles:assign"), async (req: Request, res: Response, next: NextFunction) => {
|
|
98
|
-
try {
|
|
99
|
-
const memberId = requireString(req.params.memberId, "memberId");
|
|
100
|
-
if (memberId === req.authz!.memberId) throw new HttpError(403, "cannot change your own roles");
|
|
101
|
-
const body = req.body as Record<string, unknown>;
|
|
102
|
-
res.status(200).json(await rbac.setMemberRoles(req.authz!.workspaceId, memberId, requireStringArray(body.roles, "roles")));
|
|
103
|
-
} catch (err) {
|
|
104
|
-
next(err);
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
scoped.route("delete", "/members/:memberId", ability("members:manage"), async (req: Request, res: Response, next: NextFunction) => {
|
|
109
|
-
try {
|
|
110
|
-
const memberId = requireString(req.params.memberId, "memberId");
|
|
111
|
-
if (memberId === req.authz!.memberId) throw new HttpError(403, "cannot remove yourself from a workspace you administer");
|
|
112
|
-
await workspaces.removeMember(req.authz!.workspaceId, memberId);
|
|
113
|
-
res.status(200).json({ ok: true });
|
|
114
|
-
} catch (err) {
|
|
115
|
-
next(err);
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
const router = Router();
|
|
120
|
-
router.use(scoped.handler);
|
|
121
|
-
router.use(unscoped.handler);
|
|
122
|
-
return router;
|
|
123
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { BadRequestException, Inject, Injectable } from "@nestjs/common";
|
|
2
|
-
import { and, eq } from "drizzle-orm";
|
|
3
|
-
import type { OAuthStoreDeps } from "@/lib/auth/core/oauth.js";
|
|
4
|
-
import { DRIZZLE_DB, type Database } from "./db.js";
|
|
5
|
-
import { oauthAccounts, users } from "./schema.js";
|
|
6
|
-
import { toId } from "./id.helper.js";
|
|
7
|
-
|
|
8
|
-
@Injectable()
|
|
9
|
-
export class OAuthRepository implements OAuthStoreDeps {
|
|
10
|
-
constructor(@Inject(DRIZZLE_DB) private readonly db: Database) {}
|
|
11
|
-
|
|
12
|
-
async findAccountByProvider(provider: string, providerAccountId: string): Promise<{ userId: string } | null> {
|
|
13
|
-
const [row] = await this.db
|
|
14
|
-
.select()
|
|
15
|
-
.from(oauthAccounts)
|
|
16
|
-
.where(and(eq(oauthAccounts.provider, provider), eq(oauthAccounts.providerAccountId, providerAccountId)))
|
|
17
|
-
.limit(1);
|
|
18
|
-
return row ? { userId: row.userId.toString() } : null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async linkAccount(input: { userId: string; provider: string; providerAccountId: string; email?: string }): Promise<void> {
|
|
22
|
-
await this.db.insert(oauthAccounts).values({ ...input, userId: toId(input.userId) });
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async findUserByVerifiedEmail(email: string): Promise<{ id: string } | null> {
|
|
26
|
-
const [row] = await this.db.select().from(users).where(eq(users.email, email)).limit(1);
|
|
27
|
-
return row ? { id: row.id.toString() } : null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** OAuth-only signup: no password is ever set, which is why `User.passwordHash` is nullable. */
|
|
31
|
-
async createUserFromOAuth(input: { email?: string }): Promise<{ id: string }> {
|
|
32
|
-
if (!input.email) throw new BadRequestException("OAuth provider did not return an email address");
|
|
33
|
-
const [row] = await this.db.insert(users).values({ email: input.email, passwordHash: null }).returning();
|
|
34
|
-
return { id: row.id.toString() };
|
|
35
|
-
}
|
|
36
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { Inject, Injectable } from "@nestjs/common";
|
|
2
|
-
import { eq } from "drizzle-orm";
|
|
3
|
-
import type { PasswordResetStoreDeps } from "@/lib/auth/core/password-reset.js";
|
|
4
|
-
import { DRIZZLE_DB, type Database } from "./db.js";
|
|
5
|
-
import { passwordResetTokens, users } from "./schema.js";
|
|
6
|
-
import { toId } from "./id.helper.js";
|
|
7
|
-
|
|
8
|
-
@Injectable()
|
|
9
|
-
export class PasswordResetRepository implements PasswordResetStoreDeps {
|
|
10
|
-
constructor(@Inject(DRIZZLE_DB) private readonly db: Database) {}
|
|
11
|
-
|
|
12
|
-
async saveResetToken(input: { userId: string; tokenHash: string; expiresAt: string }): Promise<void> {
|
|
13
|
-
await this.db.insert(passwordResetTokens).values({ userId: toId(input.userId), tokenHash: input.tokenHash, expiresAt: new Date(input.expiresAt) });
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
async findValidResetToken(tokenHash: string): Promise<{ userId: string; expiresAt: string; consumedAt: string | null } | null> {
|
|
17
|
-
const [row] = await this.db.select().from(passwordResetTokens).where(eq(passwordResetTokens.tokenHash, tokenHash)).limit(1);
|
|
18
|
-
if (!row) return null;
|
|
19
|
-
return { userId: row.userId.toString(), expiresAt: row.expiresAt.toISOString(), consumedAt: row.consumedAt?.toISOString() ?? null };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async consumeResetToken(tokenHash: string): Promise<void> {
|
|
23
|
-
await this.db.update(passwordResetTokens).set({ consumedAt: new Date() }).where(eq(passwordResetTokens.tokenHash, tokenHash));
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async setPasswordHash(userId: string, passwordHash: string): Promise<void> {
|
|
27
|
-
await this.db.update(users).set({ passwordHash }).where(eq(users.id, toId(userId)));
|
|
28
|
-
}
|
|
29
|
-
}
|