@simple-auth-kit/cli 1.4.3 → 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 +4 -4
- package/simple-auth-kit.ts +29 -2
- 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,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,269 +0,0 @@
|
|
|
1
|
-
import type { NextFunction, Request, RequestHandler, Response } from "express";
|
|
2
|
-
import type { AuthConfig } from "./auth.config.js";
|
|
3
|
-
import { AuthService } from "./auth.service.js";
|
|
4
|
-
import { HttpError } from "./http-error.js";
|
|
5
|
-
import { authenticated, createTieredRouter, publicRoute, type TierMiddleware } from "./route-tiers.js";
|
|
6
|
-
import "./request-context.js";
|
|
7
|
-
|
|
8
|
-
function requireString(value: unknown, field: string): string {
|
|
9
|
-
if (typeof value !== "string" || value.length === 0) throw new HttpError(400, `${field} is required`);
|
|
10
|
-
return value;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const optionalString = (value: unknown): string | undefined => (typeof value === "string" ? value : undefined);
|
|
14
|
-
|
|
15
|
-
export interface AuthRouterDeps extends TierMiddleware {
|
|
16
|
-
auth: AuthService;
|
|
17
|
-
config: AuthConfig;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Identity endpoints — everything that is about *a user*, never about a group of them.
|
|
22
|
-
* Administration lives in admin.router.ts.
|
|
23
|
-
*
|
|
24
|
-
* Replaces the reference combo's `AuthController` (a Nest `@Controller`). Same endpoints, same
|
|
25
|
-
* validation, same status codes — just an Express `Router()` instead of a decorated class, and
|
|
26
|
-
* middleware passed in where the controller lists `@UseGuards(...)`. Every handler is wrapped in
|
|
27
|
-
* try/catch + `next(err)` so a thrown `HttpError`/`AuthCoreError` reaches the error-handling
|
|
28
|
-
* middleware mounted in create-auth-app.ts instead of leaking a bare 500.
|
|
29
|
-
*/
|
|
30
|
-
export function createAuthRouter(deps: AuthRouterDeps): RequestHandler {
|
|
31
|
-
const { auth, config, authentication, authorization } = deps;
|
|
32
|
-
const router = createTieredRouter({ authentication, authorization });
|
|
33
|
-
|
|
34
|
-
router.route("post", "/signup", publicRoute(), async (req: Request, res: Response, next: NextFunction) => {
|
|
35
|
-
try {
|
|
36
|
-
const body = req.body as Record<string, unknown>;
|
|
37
|
-
const tokens = await auth.signup({
|
|
38
|
-
email: requireString(body.email, "email"),
|
|
39
|
-
password: requireString(body.password, "password"),
|
|
40
|
-
firstName: optionalString(body.firstName),
|
|
41
|
-
lastName: optionalString(body.lastName),
|
|
42
|
-
displayName: optionalString(body.displayName),
|
|
43
|
-
phone: optionalString(body.phone),
|
|
44
|
-
username: optionalString(body.username),
|
|
45
|
-
userAgent: req.headers["user-agent"],
|
|
46
|
-
ip: req.ip,
|
|
47
|
-
});
|
|
48
|
-
res.status(201).json(tokens);
|
|
49
|
-
} catch (err) {
|
|
50
|
-
next(err);
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
router.route("post", "/login", publicRoute(), async (req: Request, res: Response, next: NextFunction) => {
|
|
55
|
-
try {
|
|
56
|
-
const body = req.body as Record<string, unknown>;
|
|
57
|
-
const result = await auth.login({
|
|
58
|
-
identifier: requireString(body.identifier, "identifier"),
|
|
59
|
-
password: requireString(body.password, "password"),
|
|
60
|
-
userAgent: req.headers["user-agent"],
|
|
61
|
-
ip: req.ip,
|
|
62
|
-
});
|
|
63
|
-
res.status(200).json(result);
|
|
64
|
-
} catch (err) {
|
|
65
|
-
next(err);
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
router.route("post", "/login/2fa", publicRoute(), async (req: Request, res: Response, next: NextFunction) => {
|
|
70
|
-
try {
|
|
71
|
-
const body = req.body as Record<string, unknown>;
|
|
72
|
-
const tokens = await auth.loginTwoFactor({
|
|
73
|
-
challengeToken: requireString(body.challengeToken, "challengeToken"),
|
|
74
|
-
code: requireString(body.code, "code"),
|
|
75
|
-
userAgent: req.headers["user-agent"],
|
|
76
|
-
ip: req.ip,
|
|
77
|
-
});
|
|
78
|
-
res.status(200).json(tokens);
|
|
79
|
-
} catch (err) {
|
|
80
|
-
next(err);
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
router.route("post", "/refresh", publicRoute(), async (req: Request, res: Response, next: NextFunction) => {
|
|
85
|
-
try {
|
|
86
|
-
const body = req.body as Record<string, unknown>;
|
|
87
|
-
const tokens = await auth.refresh(requireString(body.refreshToken, "refreshToken"));
|
|
88
|
-
res.status(200).json(tokens);
|
|
89
|
-
} catch (err) {
|
|
90
|
-
next(err);
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
// `roles`/`permissions` are whatever applies to this request — see authz.middleware.ts for how
|
|
95
|
-
// they are resolved. A request that names no authorization scope gets empty arrays.
|
|
96
|
-
//
|
|
97
|
-
// `permissions` is the caller's whole authorization: `defineAbilitiesFor(permissions)` from
|
|
98
|
-
// ability.ts rebuilds, in the client, the identical CASL ability the server's own route check
|
|
99
|
-
// just answered with, so the UI cannot disagree with what the API will allow. The slugs go on
|
|
100
|
-
// the wire rather than a serialised ability, so the payload is something a human can read in a
|
|
101
|
-
// log — and it is the same `req.authz` the ability was built from, not a re-resolution.
|
|
102
|
-
router.route("get", "/me", authenticated(), async (req: Request, res: Response, next: NextFunction) => {
|
|
103
|
-
try {
|
|
104
|
-
const { sub, sessionId } = req.auth!;
|
|
105
|
-
const [{ twoFactorEnabled }, profile] = await Promise.all([auth.getTwoFactorStatus(sub), auth.getProfile(sub)]);
|
|
106
|
-
res.status(200).json({
|
|
107
|
-
sub,
|
|
108
|
-
sessionId,
|
|
109
|
-
roles: req.authz?.roles ?? [],
|
|
110
|
-
permissions: req.authz?.permissions ?? [],
|
|
111
|
-
twoFactorEnabled,
|
|
112
|
-
email: profile.email,
|
|
113
|
-
firstName: profile.firstName,
|
|
114
|
-
lastName: profile.lastName,
|
|
115
|
-
displayName: profile.displayName,
|
|
116
|
-
phone: profile.phone,
|
|
117
|
-
username: profile.username,
|
|
118
|
-
photo: profile.photo,
|
|
119
|
-
});
|
|
120
|
-
} catch (err) {
|
|
121
|
-
next(err);
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
router.route("get", "/sessions", authenticated(), async (req: Request, res: Response, next: NextFunction) => {
|
|
126
|
-
try {
|
|
127
|
-
const sessions = await auth.listActiveSessions(req.auth!.sub);
|
|
128
|
-
res.status(200).json(sessions);
|
|
129
|
-
} catch (err) {
|
|
130
|
-
next(err);
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
router.route("post", "/logout", authenticated(), async (req: Request, res: Response, next: NextFunction) => {
|
|
135
|
-
try {
|
|
136
|
-
const { sessionId, jti, sub } = req.auth!;
|
|
137
|
-
// Recorded so the row can later be told apart from a session an administrator ended —
|
|
138
|
-
// see `sessions.revoked_by`.
|
|
139
|
-
await auth.logout(sessionId, jti, config.accessTokenTtlSeconds, { userId: sub, ip: req.ip });
|
|
140
|
-
res.status(200).json({ ok: true });
|
|
141
|
-
} catch (err) {
|
|
142
|
-
next(err);
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
router.route("post", "/logout-all", authenticated(), async (req: Request, res: Response, next: NextFunction) => {
|
|
147
|
-
try {
|
|
148
|
-
await auth.logoutAll(req.auth!.sub, { userId: req.auth!.sub, ip: req.ip });
|
|
149
|
-
res.status(200).json({ ok: true });
|
|
150
|
-
} catch (err) {
|
|
151
|
-
next(err);
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
router.route("post", "/logout-others", authenticated(), async (req: Request, res: Response, next: NextFunction) => {
|
|
156
|
-
try {
|
|
157
|
-
await auth.logoutOthers(req.auth!.sub, req.auth!.sessionId, { userId: req.auth!.sub, ip: req.ip });
|
|
158
|
-
res.status(200).json({ ok: true });
|
|
159
|
-
} catch (err) {
|
|
160
|
-
next(err);
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
router.route("post", "/password/change", authenticated(), async (req: Request, res: Response, next: NextFunction) => {
|
|
165
|
-
try {
|
|
166
|
-
const body = req.body as Record<string, unknown>;
|
|
167
|
-
await auth.changePassword(req.auth!.sub, req.auth!.sessionId, {
|
|
168
|
-
currentPassword: requireString(body.currentPassword, "currentPassword"),
|
|
169
|
-
newPassword: requireString(body.newPassword, "newPassword"),
|
|
170
|
-
});
|
|
171
|
-
res.status(200).json({ ok: true });
|
|
172
|
-
} catch (err) {
|
|
173
|
-
next(err);
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
// Self-service — no admin permission required. Updates the caller's own row directly; unrelated
|
|
178
|
-
// to any workspace. Same null-vs-undefined field parsing every PATCH handler in this codebase
|
|
179
|
-
// uses — see admin.router.ts's PATCH /users/:userId.
|
|
180
|
-
router.route("patch", "/me", authenticated(), async (req: Request, res: Response, next: NextFunction) => {
|
|
181
|
-
try {
|
|
182
|
-
const body = req.body as Record<string, unknown>;
|
|
183
|
-
const profile = await auth.updateProfile(req.auth!.sub, {
|
|
184
|
-
firstName: body.firstName === null ? null : optionalString(body.firstName),
|
|
185
|
-
lastName: body.lastName === null ? null : optionalString(body.lastName),
|
|
186
|
-
displayName: body.displayName === null ? null : optionalString(body.displayName),
|
|
187
|
-
phone: body.phone === null ? null : optionalString(body.phone),
|
|
188
|
-
username: body.username === null ? null : optionalString(body.username),
|
|
189
|
-
photo: body.photo === null ? null : optionalString(body.photo),
|
|
190
|
-
});
|
|
191
|
-
res.status(200).json(profile);
|
|
192
|
-
} catch (err) {
|
|
193
|
-
next(err);
|
|
194
|
-
}
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
router.route("post", "/2fa/enroll", authenticated(), async (req: Request, res: Response, next: NextFunction) => {
|
|
198
|
-
try {
|
|
199
|
-
const result = await auth.enrollTwoFactor(req.auth!.sub);
|
|
200
|
-
res.status(200).json(result);
|
|
201
|
-
} catch (err) {
|
|
202
|
-
next(err);
|
|
203
|
-
}
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
router.route("post", "/2fa/confirm", authenticated(), async (req: Request, res: Response, next: NextFunction) => {
|
|
207
|
-
try {
|
|
208
|
-
const body = req.body as Record<string, unknown>;
|
|
209
|
-
const result = await auth.confirmTwoFactor(req.auth!.sub, requireString(body.code, "code"));
|
|
210
|
-
res.status(200).json(result);
|
|
211
|
-
} catch (err) {
|
|
212
|
-
next(err);
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
router.route("post", "/2fa/disable", authenticated(), async (req: Request, res: Response, next: NextFunction) => {
|
|
217
|
-
try {
|
|
218
|
-
const body = req.body as Record<string, unknown>;
|
|
219
|
-
await auth.disableTwoFactor(req.auth!.sub, requireString(body.code, "code"));
|
|
220
|
-
res.status(200).json({ ok: true });
|
|
221
|
-
} catch (err) {
|
|
222
|
-
next(err);
|
|
223
|
-
}
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
router.route("post", "/password/forgot", publicRoute(), async (req: Request, res: Response, next: NextFunction) => {
|
|
227
|
-
try {
|
|
228
|
-
const body = req.body as Record<string, unknown>;
|
|
229
|
-
await auth.requestPasswordReset(requireString(body.email, "email"));
|
|
230
|
-
res.status(200).json({ ok: true }); // always 200 — never reveal whether the email exists
|
|
231
|
-
} catch (err) {
|
|
232
|
-
next(err);
|
|
233
|
-
}
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
router.route("post", "/password/reset", publicRoute(), async (req: Request, res: Response, next: NextFunction) => {
|
|
237
|
-
try {
|
|
238
|
-
const body = req.body as Record<string, unknown>;
|
|
239
|
-
await auth.resetPassword(requireString(body.token, "token"), requireString(body.newPassword, "newPassword"));
|
|
240
|
-
res.status(200).json({ ok: true });
|
|
241
|
-
} catch (err) {
|
|
242
|
-
next(err);
|
|
243
|
-
}
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
router.route("get", "/oauth/:provider/start", publicRoute(), async (req: Request, res: Response, next: NextFunction) => {
|
|
247
|
-
try {
|
|
248
|
-
const result = await auth.startOAuth(requireString(req.params.provider, "provider"));
|
|
249
|
-
res.status(200).json(result);
|
|
250
|
-
} catch (err) {
|
|
251
|
-
next(err);
|
|
252
|
-
}
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
router.route("get", "/oauth/:provider/callback", publicRoute(), async (req: Request, res: Response, next: NextFunction) => {
|
|
256
|
-
try {
|
|
257
|
-
const tokens = await auth.completeOAuthCallback(
|
|
258
|
-
requireString(req.params.provider, "provider"),
|
|
259
|
-
requireString(req.query.code, "code"),
|
|
260
|
-
requireString(req.query.state, "state"),
|
|
261
|
-
);
|
|
262
|
-
res.status(200).json(tokens);
|
|
263
|
-
} catch (err) {
|
|
264
|
-
next(err);
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
return router.handler;
|
|
269
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import type { PasswordResetStoreDeps } from "@/lib/auth/core/password-reset.js";
|
|
2
|
-
import { PrismaClient } from "../generated/prisma/client.js";
|
|
3
|
-
import { toId } from "./id.helper.js";
|
|
4
|
-
|
|
5
|
-
// Plain class, no DI container — constructed directly with a PrismaClient in
|
|
6
|
-
// create-auth-app.ts. Identical Prisma queries to the reference combo.
|
|
7
|
-
export class PasswordResetRepository implements PasswordResetStoreDeps {
|
|
8
|
-
constructor(private readonly prisma: PrismaClient) {}
|
|
9
|
-
|
|
10
|
-
async saveResetToken(input: { userId: string; tokenHash: string; expiresAt: string }): Promise<void> {
|
|
11
|
-
await this.prisma.passwordResetToken.create({
|
|
12
|
-
data: { userId: toId(input.userId), tokenHash: input.tokenHash, expiresAt: new Date(input.expiresAt) },
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
async findValidResetToken(tokenHash: string): Promise<{ userId: string; expiresAt: string; consumedAt: string | null } | null> {
|
|
17
|
-
const row = await this.prisma.passwordResetToken.findUnique({ where: { tokenHash } });
|
|
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.prisma.passwordResetToken.update({ where: { tokenHash }, data: { consumedAt: new Date() } });
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async setPasswordHash(userId: string, passwordHash: string): Promise<void> {
|
|
27
|
-
await this.prisma.user.update({ where: { id: toId(userId) }, data: { passwordHash } });
|
|
28
|
-
}
|
|
29
|
-
}
|