@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,398 @@
|
|
|
1
|
+
import type { NextFunction, Request, RequestHandler, Response } from "express";
|
|
2
|
+
import type { AuthConfig } from "../auth.config.js";
|
|
3
|
+
import { AuthService } from "../services/auth.service.js";
|
|
4
|
+
import { HttpError } from "../http-error.js";
|
|
5
|
+
import {
|
|
6
|
+
authenticated,
|
|
7
|
+
createTieredRouter,
|
|
8
|
+
publicRoute,
|
|
9
|
+
type TierMiddleware,
|
|
10
|
+
} from "../route-tiers.js";
|
|
11
|
+
import "../request-context.js";
|
|
12
|
+
|
|
13
|
+
function requireString(value: unknown, field: string): string {
|
|
14
|
+
if (typeof value !== "string" || value.length === 0)
|
|
15
|
+
throw new HttpError(400, `${field} is required`);
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const optionalString = (value: unknown): string | undefined =>
|
|
20
|
+
typeof value === "string" ? value : undefined;
|
|
21
|
+
|
|
22
|
+
export interface AuthRouterDeps extends TierMiddleware {
|
|
23
|
+
auth: AuthService;
|
|
24
|
+
config: AuthConfig;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Identity endpoints — everything that is about *a user*, never about a group of them.
|
|
29
|
+
* Administration lives in admin.router.ts.
|
|
30
|
+
*
|
|
31
|
+
* Replaces the reference combo's `AuthController` (a Nest `@Controller`). Same endpoints, same
|
|
32
|
+
* validation, same status codes — just an Express `Router()` instead of a decorated class, and
|
|
33
|
+
* middleware passed in where the controller lists `@UseGuards(...)`. Every handler is wrapped in
|
|
34
|
+
* try/catch + `next(err)` so a thrown `HttpError`/`AuthCoreError` reaches the error-handling
|
|
35
|
+
* middleware mounted in create-auth-app.ts instead of leaking a bare 500.
|
|
36
|
+
*/
|
|
37
|
+
export function createAuthRouter(deps: AuthRouterDeps): RequestHandler {
|
|
38
|
+
const { auth, config, authentication, authorization } = deps;
|
|
39
|
+
const router = createTieredRouter({ authentication, authorization });
|
|
40
|
+
|
|
41
|
+
router.route(
|
|
42
|
+
"post",
|
|
43
|
+
"/signup",
|
|
44
|
+
publicRoute(),
|
|
45
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
46
|
+
try {
|
|
47
|
+
const body = req.body as Record<string, unknown>;
|
|
48
|
+
const tokens = await auth.signup({
|
|
49
|
+
email: requireString(body.email, "email"),
|
|
50
|
+
password: requireString(body.password, "password"),
|
|
51
|
+
firstName: optionalString(body.firstName),
|
|
52
|
+
lastName: optionalString(body.lastName),
|
|
53
|
+
displayName: optionalString(body.displayName),
|
|
54
|
+
phone: optionalString(body.phone),
|
|
55
|
+
username: optionalString(body.username),
|
|
56
|
+
userAgent: req.headers["user-agent"],
|
|
57
|
+
ip: req.ip,
|
|
58
|
+
});
|
|
59
|
+
res.status(201).json(tokens);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
next(err);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
router.route(
|
|
67
|
+
"post",
|
|
68
|
+
"/login",
|
|
69
|
+
publicRoute(),
|
|
70
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
71
|
+
try {
|
|
72
|
+
const body = req.body as Record<string, unknown>;
|
|
73
|
+
const result = await auth.login({
|
|
74
|
+
identifier: requireString(body.identifier, "identifier"),
|
|
75
|
+
password: requireString(body.password, "password"),
|
|
76
|
+
userAgent: req.headers["user-agent"],
|
|
77
|
+
ip: req.ip,
|
|
78
|
+
});
|
|
79
|
+
res.status(200).json(result);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
next(err);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
router.route(
|
|
87
|
+
"post",
|
|
88
|
+
"/login/2fa",
|
|
89
|
+
publicRoute(),
|
|
90
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
91
|
+
try {
|
|
92
|
+
const body = req.body as Record<string, unknown>;
|
|
93
|
+
const tokens = await auth.loginTwoFactor({
|
|
94
|
+
challengeToken: requireString(body.challengeToken, "challengeToken"),
|
|
95
|
+
code: requireString(body.code, "code"),
|
|
96
|
+
userAgent: req.headers["user-agent"],
|
|
97
|
+
ip: req.ip,
|
|
98
|
+
});
|
|
99
|
+
res.status(200).json(tokens);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
next(err);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
router.route(
|
|
107
|
+
"post",
|
|
108
|
+
"/refresh",
|
|
109
|
+
publicRoute(),
|
|
110
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
111
|
+
try {
|
|
112
|
+
const body = req.body as Record<string, unknown>;
|
|
113
|
+
const tokens = await auth.refresh(
|
|
114
|
+
requireString(body.refreshToken, "refreshToken"),
|
|
115
|
+
);
|
|
116
|
+
res.status(200).json(tokens);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
next(err);
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// `roles`/`permissions` are whatever applies to this request — see authz.middleware.ts for how
|
|
124
|
+
// they are resolved. A request that names no authorization scope gets empty arrays.
|
|
125
|
+
//
|
|
126
|
+
// `permissions` is the caller's whole authorization: `defineAbilitiesFor(permissions)` from
|
|
127
|
+
// ability.ts rebuilds, in the client, the identical CASL ability the server's own route check
|
|
128
|
+
// just answered with, so the UI cannot disagree with what the API will allow. The slugs go on
|
|
129
|
+
// the wire rather than a serialised ability, so the payload is something a human can read in a
|
|
130
|
+
// log — and it is the same `req.authz` the ability was built from, not a re-resolution.
|
|
131
|
+
router.route(
|
|
132
|
+
"get",
|
|
133
|
+
"/me",
|
|
134
|
+
authenticated(),
|
|
135
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
136
|
+
try {
|
|
137
|
+
const { sub, sessionId } = req.auth!;
|
|
138
|
+
const [{ twoFactorEnabled }, profile] = await Promise.all([
|
|
139
|
+
auth.getTwoFactorStatus(sub),
|
|
140
|
+
auth.getProfile(sub),
|
|
141
|
+
]);
|
|
142
|
+
res.status(200).json({
|
|
143
|
+
sub,
|
|
144
|
+
sessionId,
|
|
145
|
+
roles: req.authz?.roles ?? [],
|
|
146
|
+
permissions: req.authz?.permissions ?? [],
|
|
147
|
+
twoFactorEnabled,
|
|
148
|
+
email: profile.email,
|
|
149
|
+
firstName: profile.firstName,
|
|
150
|
+
lastName: profile.lastName,
|
|
151
|
+
displayName: profile.displayName,
|
|
152
|
+
phone: profile.phone,
|
|
153
|
+
username: profile.username,
|
|
154
|
+
photo: profile.photo,
|
|
155
|
+
});
|
|
156
|
+
} catch (err) {
|
|
157
|
+
next(err);
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
router.route(
|
|
163
|
+
"get",
|
|
164
|
+
"/sessions",
|
|
165
|
+
authenticated(),
|
|
166
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
167
|
+
try {
|
|
168
|
+
const sessions = await auth.listActiveSessions(req.auth!.sub);
|
|
169
|
+
res.status(200).json(sessions);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
next(err);
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
router.route(
|
|
177
|
+
"post",
|
|
178
|
+
"/logout",
|
|
179
|
+
authenticated(),
|
|
180
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
181
|
+
try {
|
|
182
|
+
const { sessionId, jti, sub } = req.auth!;
|
|
183
|
+
// Recorded so the row can later be told apart from a session an administrator ended —
|
|
184
|
+
// see `sessions.revoked_by`.
|
|
185
|
+
await auth.logout(sessionId, jti, config.accessTokenTtlSeconds, {
|
|
186
|
+
userId: sub,
|
|
187
|
+
ip: req.ip,
|
|
188
|
+
});
|
|
189
|
+
res.status(200).json({ ok: true });
|
|
190
|
+
} catch (err) {
|
|
191
|
+
next(err);
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
router.route(
|
|
197
|
+
"post",
|
|
198
|
+
"/logout-all",
|
|
199
|
+
authenticated(),
|
|
200
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
201
|
+
try {
|
|
202
|
+
await auth.logoutAll(req.auth!.sub, {
|
|
203
|
+
userId: req.auth!.sub,
|
|
204
|
+
ip: req.ip,
|
|
205
|
+
});
|
|
206
|
+
res.status(200).json({ ok: true });
|
|
207
|
+
} catch (err) {
|
|
208
|
+
next(err);
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
router.route(
|
|
214
|
+
"post",
|
|
215
|
+
"/logout-others",
|
|
216
|
+
authenticated(),
|
|
217
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
218
|
+
try {
|
|
219
|
+
await auth.logoutOthers(req.auth!.sub, req.auth!.sessionId, {
|
|
220
|
+
userId: req.auth!.sub,
|
|
221
|
+
ip: req.ip,
|
|
222
|
+
});
|
|
223
|
+
res.status(200).json({ ok: true });
|
|
224
|
+
} catch (err) {
|
|
225
|
+
next(err);
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
router.route(
|
|
231
|
+
"post",
|
|
232
|
+
"/password/change",
|
|
233
|
+
authenticated(),
|
|
234
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
235
|
+
try {
|
|
236
|
+
const body = req.body as Record<string, unknown>;
|
|
237
|
+
await auth.changePassword(req.auth!.sub, req.auth!.sessionId, {
|
|
238
|
+
currentPassword: requireString(
|
|
239
|
+
body.currentPassword,
|
|
240
|
+
"currentPassword",
|
|
241
|
+
),
|
|
242
|
+
newPassword: requireString(body.newPassword, "newPassword"),
|
|
243
|
+
});
|
|
244
|
+
res.status(200).json({ ok: true });
|
|
245
|
+
} catch (err) {
|
|
246
|
+
next(err);
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
// Self-service — no admin permission required. Updates the caller's own row directly; unrelated
|
|
252
|
+
// to any workspace. Same null-vs-undefined field parsing every PATCH handler in this codebase
|
|
253
|
+
// uses — see admin.router.ts's PATCH /users/:userId.
|
|
254
|
+
router.route(
|
|
255
|
+
"patch",
|
|
256
|
+
"/me",
|
|
257
|
+
authenticated(),
|
|
258
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
259
|
+
try {
|
|
260
|
+
const body = req.body as Record<string, unknown>;
|
|
261
|
+
const profile = await auth.updateProfile(req.auth!.sub, {
|
|
262
|
+
firstName:
|
|
263
|
+
body.firstName === null ? null : optionalString(body.firstName),
|
|
264
|
+
lastName:
|
|
265
|
+
body.lastName === null ? null : optionalString(body.lastName),
|
|
266
|
+
displayName:
|
|
267
|
+
body.displayName === null ? null : optionalString(body.displayName),
|
|
268
|
+
phone: body.phone === null ? null : optionalString(body.phone),
|
|
269
|
+
username:
|
|
270
|
+
body.username === null ? null : optionalString(body.username),
|
|
271
|
+
photo: body.photo === null ? null : optionalString(body.photo),
|
|
272
|
+
});
|
|
273
|
+
res.status(200).json(profile);
|
|
274
|
+
} catch (err) {
|
|
275
|
+
next(err);
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
router.route(
|
|
281
|
+
"post",
|
|
282
|
+
"/2fa/enroll",
|
|
283
|
+
authenticated(),
|
|
284
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
285
|
+
try {
|
|
286
|
+
const result = await auth.enrollTwoFactor(req.auth!.sub);
|
|
287
|
+
res.status(200).json(result);
|
|
288
|
+
} catch (err) {
|
|
289
|
+
next(err);
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
router.route(
|
|
295
|
+
"post",
|
|
296
|
+
"/2fa/confirm",
|
|
297
|
+
authenticated(),
|
|
298
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
299
|
+
try {
|
|
300
|
+
const body = req.body as Record<string, unknown>;
|
|
301
|
+
const result = await auth.confirmTwoFactor(
|
|
302
|
+
req.auth!.sub,
|
|
303
|
+
requireString(body.code, "code"),
|
|
304
|
+
);
|
|
305
|
+
res.status(200).json(result);
|
|
306
|
+
} catch (err) {
|
|
307
|
+
next(err);
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
router.route(
|
|
313
|
+
"post",
|
|
314
|
+
"/2fa/disable",
|
|
315
|
+
authenticated(),
|
|
316
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
317
|
+
try {
|
|
318
|
+
const body = req.body as Record<string, unknown>;
|
|
319
|
+
await auth.disableTwoFactor(
|
|
320
|
+
req.auth!.sub,
|
|
321
|
+
requireString(body.code, "code"),
|
|
322
|
+
);
|
|
323
|
+
res.status(200).json({ ok: true });
|
|
324
|
+
} catch (err) {
|
|
325
|
+
next(err);
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
router.route(
|
|
331
|
+
"post",
|
|
332
|
+
"/password/forgot",
|
|
333
|
+
publicRoute(),
|
|
334
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
335
|
+
try {
|
|
336
|
+
const body = req.body as Record<string, unknown>;
|
|
337
|
+
await auth.requestPasswordReset(requireString(body.email, "email"));
|
|
338
|
+
res.status(200).json({ ok: true }); // always 200 — never reveal whether the email exists
|
|
339
|
+
} catch (err) {
|
|
340
|
+
next(err);
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
router.route(
|
|
346
|
+
"post",
|
|
347
|
+
"/password/reset",
|
|
348
|
+
publicRoute(),
|
|
349
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
350
|
+
try {
|
|
351
|
+
const body = req.body as Record<string, unknown>;
|
|
352
|
+
await auth.resetPassword(
|
|
353
|
+
requireString(body.token, "token"),
|
|
354
|
+
requireString(body.newPassword, "newPassword"),
|
|
355
|
+
);
|
|
356
|
+
res.status(200).json({ ok: true });
|
|
357
|
+
} catch (err) {
|
|
358
|
+
next(err);
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
router.route(
|
|
364
|
+
"get",
|
|
365
|
+
"/oauth/:provider/start",
|
|
366
|
+
publicRoute(),
|
|
367
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
368
|
+
try {
|
|
369
|
+
const result = await auth.startOAuth(
|
|
370
|
+
requireString(req.params.provider, "provider"),
|
|
371
|
+
);
|
|
372
|
+
res.status(200).json(result);
|
|
373
|
+
} catch (err) {
|
|
374
|
+
next(err);
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
router.route(
|
|
380
|
+
"get",
|
|
381
|
+
"/oauth/:provider/callback",
|
|
382
|
+
publicRoute(),
|
|
383
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
384
|
+
try {
|
|
385
|
+
const tokens = await auth.completeOAuthCallback(
|
|
386
|
+
requireString(req.params.provider, "provider"),
|
|
387
|
+
requireString(req.query.code, "code"),
|
|
388
|
+
requireString(req.query.state, "state"),
|
|
389
|
+
);
|
|
390
|
+
res.status(200).json(tokens);
|
|
391
|
+
} catch (err) {
|
|
392
|
+
next(err);
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
return router.handler;
|
|
398
|
+
}
|
|
@@ -2,25 +2,28 @@ import { PrismaPg } from "@prisma/adapter-pg";
|
|
|
2
2
|
import express, { type Express } from "express";
|
|
3
3
|
import swaggerUi from "swagger-ui-express";
|
|
4
4
|
import type { RateLimitDeps } from "@/lib/auth/core/rate-limit.js";
|
|
5
|
-
import { createAdminRouter } from "./admin.router.js";
|
|
6
|
-
import { AuditLogRepository } from "./audit-log.repository.js";
|
|
7
|
-
import { authCoreErrorMiddleware } from "./auth-core-error.middleware.js";
|
|
5
|
+
import { createAdminRouter } from "./routers/admin.router.js";
|
|
6
|
+
import { AuditLogRepository } from "./repositories/audit-log.repository.js";
|
|
7
|
+
import { authCoreErrorMiddleware } from "./middleware/auth-core-error.middleware.js";
|
|
8
8
|
import { AuthConfig, defaultAuthConfig } from "./auth.config.js";
|
|
9
|
-
import { createAuthMiddleware } from "./auth.middleware.js";
|
|
10
|
-
import { createAuthRouter } from "./auth.router.js";
|
|
11
|
-
import { AuthService } from "./auth.service.js";
|
|
12
|
-
import { createAuthzMiddleware } from "./authz.middleware.js";
|
|
9
|
+
import { createAuthMiddleware } from "./middleware/auth.middleware.js";
|
|
10
|
+
import { createAuthRouter } from "./routers/auth.router.js";
|
|
11
|
+
import { AuthService } from "./services/auth.service.js";
|
|
12
|
+
import { createAuthzMiddleware } from "./middleware/authz.middleware.js";
|
|
13
13
|
import { PrismaClient } from "../generated/prisma/client.js";
|
|
14
14
|
import { KeyProviderService } from "./key-provider.js";
|
|
15
|
-
import { OAuthRepository } from "./oauth.repository.js";
|
|
15
|
+
import { OAuthRepository } from "./repositories/oauth.repository.js";
|
|
16
16
|
import { openApiSpec } from "./openapi-spec.js";
|
|
17
|
-
import { PasswordResetRepository } from "./password-reset.repository.js";
|
|
18
|
-
import {
|
|
17
|
+
import { PasswordResetRepository } from "./repositories/password-reset.repository.js";
|
|
18
|
+
import {
|
|
19
|
+
InMemoryPermissionCacheStore,
|
|
20
|
+
PermissionCache,
|
|
21
|
+
} from "./permission-cache.js";
|
|
19
22
|
import { InMemoryRateLimitStore } from "./rate-limit.store.js";
|
|
20
|
-
import { RbacRepository } from "./rbac.repository.js";
|
|
21
|
-
import { responseEnvelope } from "./response-envelope.middleware.js";
|
|
22
|
-
import { SessionRepository } from "./session.repository.js";
|
|
23
|
-
import { TwoFactorRepository } from "./two-factor.repository.js";
|
|
23
|
+
import { RbacRepository } from "./repositories/rbac.repository.js";
|
|
24
|
+
import { responseEnvelope } from "./middleware/response-envelope.middleware.js";
|
|
25
|
+
import { SessionRepository } from "./repositories/session.repository.js";
|
|
26
|
+
import { TwoFactorRepository } from "./repositories/two-factor.repository.js";
|
|
24
27
|
|
|
25
28
|
export interface CreateAuthAppOptions {
|
|
26
29
|
/** Overrides merged on top of `defaultAuthConfig`, same shape as the reference combo's `AuthModule.forRoot(config)`. */
|
|
@@ -39,7 +42,9 @@ export interface CreateAuthAppOptions {
|
|
|
39
42
|
export function createAuthApp(options: CreateAuthAppOptions = {}): Express {
|
|
40
43
|
const config: AuthConfig = { ...defaultAuthConfig, ...options.config };
|
|
41
44
|
|
|
42
|
-
const adapter = new PrismaPg({
|
|
45
|
+
const adapter = new PrismaPg({
|
|
46
|
+
connectionString: process.env["DATABASE_URL"],
|
|
47
|
+
});
|
|
43
48
|
const prisma = new PrismaClient({ adapter });
|
|
44
49
|
|
|
45
50
|
const auditLog = new AuditLogRepository(prisma);
|
|
@@ -47,15 +52,30 @@ export function createAuthApp(options: CreateAuthAppOptions = {}): Express {
|
|
|
47
52
|
const keys = new KeyProviderService();
|
|
48
53
|
// Swap the store for a Redis-backed one by passing `rateLimitStore` in `config` — nothing in
|
|
49
54
|
// this library's source changes.
|
|
50
|
-
const rateLimit: RateLimitDeps =
|
|
55
|
+
const rateLimit: RateLimitDeps =
|
|
56
|
+
config.rateLimitStore ?? new InMemoryRateLimitStore();
|
|
51
57
|
// The cache seam. Swap the store for a Redis-backed one by passing `permissionCacheStore` in
|
|
52
58
|
// `config` — nothing in this library's source changes. Keys are namespaced simpleauthkit:authz:*.
|
|
53
|
-
const permissionCache = new PermissionCache(
|
|
59
|
+
const permissionCache = new PermissionCache(
|
|
60
|
+
config.permissionCacheStore ?? new InMemoryPermissionCacheStore(),
|
|
61
|
+
config,
|
|
62
|
+
);
|
|
54
63
|
const rbac = new RbacRepository(prisma, permissionCache);
|
|
55
64
|
const twoFactor = new TwoFactorRepository(prisma);
|
|
56
65
|
const oauth = new OAuthRepository(prisma);
|
|
57
66
|
const passwordReset = new PasswordResetRepository(prisma);
|
|
58
|
-
const authService = new AuthService(
|
|
67
|
+
const authService = new AuthService(
|
|
68
|
+
prisma,
|
|
69
|
+
sessions,
|
|
70
|
+
keys,
|
|
71
|
+
rateLimit,
|
|
72
|
+
auditLog,
|
|
73
|
+
rbac,
|
|
74
|
+
twoFactor,
|
|
75
|
+
oauth,
|
|
76
|
+
passwordReset,
|
|
77
|
+
config,
|
|
78
|
+
);
|
|
59
79
|
|
|
60
80
|
const authentication = createAuthMiddleware({ keys, sessions });
|
|
61
81
|
// Roles are global here, but they are read from the database on the request that uses them —
|
|
@@ -79,13 +99,28 @@ export function createAuthApp(options: CreateAuthAppOptions = {}): Express {
|
|
|
79
99
|
// slash) a plain 200 instead of swagger-ui-express's default 301 to "/docs/" — the
|
|
80
100
|
// underlying static asset middleware would otherwise treat the mount root as a directory
|
|
81
101
|
// listing and redirect.
|
|
82
|
-
app.use(
|
|
102
|
+
app.use(
|
|
103
|
+
"/docs",
|
|
104
|
+
swaggerUi.serveWithOptions({ redirect: false }),
|
|
105
|
+
swaggerUi.setup(openApiSpec),
|
|
106
|
+
);
|
|
83
107
|
app.get("/docs-json", (_req, res) => res.json(openApiSpec));
|
|
84
108
|
app.use(responseEnvelope());
|
|
85
109
|
// Mounted before /auth so `/auth/admin/*` reaches the admin router rather than falling
|
|
86
110
|
// through to the identity router's 404.
|
|
87
|
-
app.use(
|
|
88
|
-
|
|
111
|
+
app.use(
|
|
112
|
+
"/auth/admin",
|
|
113
|
+
createAdminRouter({ auth: authService, authentication, authorization }),
|
|
114
|
+
);
|
|
115
|
+
app.use(
|
|
116
|
+
"/auth",
|
|
117
|
+
createAuthRouter({
|
|
118
|
+
auth: authService,
|
|
119
|
+
config,
|
|
120
|
+
authentication,
|
|
121
|
+
authorization,
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
89
124
|
// Mounted last so it catches errors forwarded via `next(err)` from every route above.
|
|
90
125
|
app.use(authCoreErrorMiddleware);
|
|
91
126
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { NextFunction, Request, RequestHandler, Response } from "express";
|
|
2
|
-
import { defineAbilitiesFor } from "
|
|
3
|
-
import type { PermissionCache } from "
|
|
4
|
-
import type { RbacRepository } from "
|
|
5
|
-
import "
|
|
2
|
+
import { defineAbilitiesFor } from "../ability.js";
|
|
3
|
+
import type { PermissionCache } from "../permission-cache.js";
|
|
4
|
+
import type { RbacRepository } from "../repositories/rbac.repository.js";
|
|
5
|
+
import "../request-context.js";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Roles and permissions are global to this deployment: a user has one set, and it applies
|
|
@@ -41,13 +41,21 @@ export interface AuthzMiddlewareDeps {
|
|
|
41
41
|
*
|
|
42
42
|
* Replaces the reference combo's `AuthzGuard`.
|
|
43
43
|
*/
|
|
44
|
-
export function createAuthzMiddleware(
|
|
44
|
+
export function createAuthzMiddleware(
|
|
45
|
+
deps: AuthzMiddlewareDeps,
|
|
46
|
+
): RequestHandler {
|
|
45
47
|
const { rbac, cache } = deps;
|
|
46
|
-
return async function authzMiddleware(
|
|
48
|
+
return async function authzMiddleware(
|
|
49
|
+
req: Request,
|
|
50
|
+
_res: Response,
|
|
51
|
+
next: NextFunction,
|
|
52
|
+
): Promise<void> {
|
|
47
53
|
try {
|
|
48
54
|
if (req.auth) {
|
|
49
55
|
const userId = req.auth.sub;
|
|
50
|
-
req.authz = (await cache.resolve<AuthzContext>(userId, () =>
|
|
56
|
+
req.authz = (await cache.resolve<AuthzContext>(userId, () =>
|
|
57
|
+
rbac.resolveAuthzContext(userId),
|
|
58
|
+
)) ?? { roles: [], permissions: [] };
|
|
51
59
|
req.ability = defineAbilitiesFor(req.authz.permissions);
|
|
52
60
|
}
|
|
53
61
|
next();
|