create-authenik8-app 2.4.12 → 2.4.13

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.
Files changed (235) hide show
  1. package/README.md +160 -57
  2. package/assets/logo.svg +53 -0
  3. package/dist/src/bin/index.js +199 -125
  4. package/dist/src/bin/index.js.map +1 -1
  5. package/dist/src/lib/args.d.ts +18 -0
  6. package/dist/src/lib/args.d.ts.map +1 -0
  7. package/dist/src/lib/args.js +82 -0
  8. package/dist/src/lib/args.js.map +1 -0
  9. package/dist/src/lib/constants.d.ts +1 -0
  10. package/dist/src/lib/constants.d.ts.map +1 -1
  11. package/dist/src/lib/constants.js +10 -11
  12. package/dist/src/lib/constants.js.map +1 -1
  13. package/dist/src/lib/preflight.d.ts +3 -0
  14. package/dist/src/lib/preflight.d.ts.map +1 -0
  15. package/dist/src/lib/preflight.js +21 -0
  16. package/dist/src/lib/preflight.js.map +1 -0
  17. package/dist/src/lib/process.d.ts +6 -2
  18. package/dist/src/lib/process.d.ts.map +1 -1
  19. package/dist/src/lib/process.js +66 -8
  20. package/dist/src/lib/process.js.map +1 -1
  21. package/dist/src/lib/projectName.d.ts +2 -0
  22. package/dist/src/lib/projectName.d.ts.map +1 -0
  23. package/dist/src/lib/projectName.js +6 -0
  24. package/dist/src/lib/projectName.js.map +1 -0
  25. package/dist/src/lib/schemas.d.ts +192 -0
  26. package/dist/src/lib/schemas.d.ts.map +1 -0
  27. package/dist/src/lib/schemas.js +171 -0
  28. package/dist/src/lib/schemas.js.map +1 -0
  29. package/dist/src/lib/state.d.ts.map +1 -1
  30. package/dist/src/lib/state.js +20 -3
  31. package/dist/src/lib/state.js.map +1 -1
  32. package/dist/src/lib/types.d.ts +6 -13
  33. package/dist/src/lib/types.d.ts.map +1 -1
  34. package/dist/src/lib/ui.d.ts +8 -3
  35. package/dist/src/lib/ui.d.ts.map +1 -1
  36. package/dist/src/lib/ui.js +76 -112
  37. package/dist/src/lib/ui.js.map +1 -1
  38. package/dist/src/steps/configurePrisma.d.ts.map +1 -1
  39. package/dist/src/steps/configurePrisma.js +83 -9
  40. package/dist/src/steps/configurePrisma.js.map +1 -1
  41. package/dist/src/steps/createProject.d.ts +2 -1
  42. package/dist/src/steps/createProject.d.ts.map +1 -1
  43. package/dist/src/steps/createProject.js +149 -28
  44. package/dist/src/steps/createProject.js.map +1 -1
  45. package/dist/src/steps/finalSetup.d.ts +3 -3
  46. package/dist/src/steps/finalSetup.d.ts.map +1 -1
  47. package/dist/src/steps/finalSetup.js +24 -28
  48. package/dist/src/steps/finalSetup.js.map +1 -1
  49. package/dist/src/steps/installAuth.d.ts +1 -1
  50. package/dist/src/steps/installAuth.d.ts.map +1 -1
  51. package/dist/src/steps/installAuth.js +5 -4
  52. package/dist/src/steps/installAuth.js.map +1 -1
  53. package/dist/src/steps/installDeps.d.ts +11 -4
  54. package/dist/src/steps/installDeps.d.ts.map +1 -1
  55. package/dist/src/steps/installDeps.js +45 -77
  56. package/dist/src/steps/installDeps.js.map +1 -1
  57. package/dist/src/steps/prompts.d.ts +1 -1
  58. package/dist/src/steps/prompts.d.ts.map +1 -1
  59. package/dist/src/steps/prompts.js +42 -30
  60. package/dist/src/steps/prompts.js.map +1 -1
  61. package/dist/src/utils/jwk.d.ts +2 -0
  62. package/dist/src/utils/jwk.d.ts.map +1 -0
  63. package/dist/src/utils/jwk.js +44 -0
  64. package/dist/src/utils/jwk.js.map +1 -0
  65. package/dist/src/utils/output.d.ts +1 -1
  66. package/dist/src/utils/output.d.ts.map +1 -1
  67. package/dist/src/utils/output.js +75 -68
  68. package/dist/src/utils/output.js.map +1 -1
  69. package/package.json +17 -9
  70. package/templates/AGENT_IDENTITY.md +112 -0
  71. package/templates/THREAT_MODEL.md +27 -5
  72. package/templates/express-auth/README.md +13 -10
  73. package/templates/express-auth/docker-compose.yml +10 -0
  74. package/templates/express-auth/gitignore.template +6 -0
  75. package/templates/express-auth/package.json +13 -6
  76. package/templates/express-auth/src/app.ts +2 -0
  77. package/templates/express-auth/src/controllers/auth.controller.ts +2 -6
  78. package/templates/express-auth/src/controllers/protected.controller.ts +25 -10
  79. package/templates/express-auth/src/middleware/auth.middleware.ts +1 -0
  80. package/templates/express-auth/src/routes/protected.routes.ts +3 -1
  81. package/templates/express-auth/src/server.ts +6 -12
  82. package/templates/express-auth/src/utils/security.ts +119 -32
  83. package/templates/express-auth+/README.md +13 -10
  84. package/templates/express-auth+/docker-compose.yml +10 -0
  85. package/templates/express-auth+/gitignore.template +6 -0
  86. package/templates/express-auth+/package.json +12 -5
  87. package/templates/express-auth+/src/auth/auth.ts +3 -2
  88. package/templates/express-auth+/src/auth/controllers/password.controller.ts +3 -7
  89. package/templates/express-auth+/src/auth/controllers/protected.controller.ts +26 -11
  90. package/templates/express-auth+/src/auth/identity.adapter.ts +90 -0
  91. package/templates/express-auth+/src/auth/middleware/auth.middleware.ts +3 -4
  92. package/templates/express-auth+/src/auth/routes/protected.routes.ts +2 -2
  93. package/templates/express-auth+/src/oauth-providers/github/src/auth/auth.ts +3 -2
  94. package/templates/express-auth+/src/oauth-providers/google/src/auth/auth.ts +3 -2
  95. package/templates/express-auth+/src/oauth-providers/google-github/src/auth/auth.ts +3 -2
  96. package/templates/express-auth+/src/server.ts +5 -10
  97. package/templates/express-auth+/src/utils/security.ts +109 -28
  98. package/templates/express-base/README.md +13 -6
  99. package/templates/express-base/app.ts +2 -0
  100. package/templates/express-base/controllers/base.controller.ts +33 -19
  101. package/templates/express-base/docker-compose.yml +10 -0
  102. package/templates/express-base/gitignore.template +6 -0
  103. package/templates/express-base/package.json +11 -5
  104. package/templates/express-base/routes/base.routes.ts +1 -1
  105. package/templates/express-base/src/.env.example +7 -2
  106. package/templates/express-base/src/server.ts +6 -12
  107. package/templates/express-base/tsconfig.json +7 -40
  108. package/templates/express-base/utils/security.ts +111 -15
  109. package/templates/fullstack/.env.example +24 -0
  110. package/templates/fullstack/PRESET_CONTRACT.md +46 -0
  111. package/templates/fullstack/README.md +65 -0
  112. package/templates/fullstack/THREAT_MODEL.md +30 -0
  113. package/templates/fullstack/apps/api/package.json +48 -0
  114. package/templates/fullstack/apps/api/prisma/schema.prisma +124 -0
  115. package/templates/fullstack/apps/api/prisma/seed.ts +36 -0
  116. package/templates/fullstack/apps/api/prisma.config.ts +18 -0
  117. package/templates/fullstack/apps/api/src/app.ts +68 -0
  118. package/templates/fullstack/apps/api/src/auth/auth.controller.ts +125 -0
  119. package/templates/fullstack/apps/api/src/auth/auth.routes.ts +50 -0
  120. package/templates/fullstack/apps/api/src/auth/auth.service.ts +257 -0
  121. package/templates/fullstack/apps/api/src/auth/authenik8.ts +67 -0
  122. package/templates/fullstack/apps/api/src/auth/cookies.ts +36 -0
  123. package/templates/fullstack/apps/api/src/config/env.ts +72 -0
  124. package/templates/fullstack/apps/api/src/config/logger.ts +17 -0
  125. package/templates/fullstack/apps/api/src/config/mailer.ts +32 -0
  126. package/templates/fullstack/apps/api/src/config/origins.ts +27 -0
  127. package/templates/fullstack/apps/api/src/config/prisma.ts +9 -0
  128. package/templates/fullstack/apps/api/src/middleware/authenticate.ts +35 -0
  129. package/templates/fullstack/apps/api/src/middleware/csrf.ts +60 -0
  130. package/templates/fullstack/apps/api/src/middleware/origin.ts +12 -0
  131. package/templates/fullstack/apps/api/src/middleware/request-id.ts +9 -0
  132. package/templates/fullstack/apps/api/src/modules/admin/admin.controller.ts +17 -0
  133. package/templates/fullstack/apps/api/src/modules/admin/admin.routes.ts +14 -0
  134. package/templates/fullstack/apps/api/src/modules/admin/admin.service.ts +54 -0
  135. package/templates/fullstack/apps/api/src/modules/projects/project.controller.ts +22 -0
  136. package/templates/fullstack/apps/api/src/modules/projects/project.policy.ts +14 -0
  137. package/templates/fullstack/apps/api/src/modules/projects/project.repository.ts +20 -0
  138. package/templates/fullstack/apps/api/src/modules/projects/project.routes.ts +21 -0
  139. package/templates/fullstack/apps/api/src/modules/projects/project.service.ts +41 -0
  140. package/templates/fullstack/apps/api/src/modules/users/user.controller.ts +26 -0
  141. package/templates/fullstack/apps/api/src/modules/users/user.presenter.ts +13 -0
  142. package/templates/fullstack/apps/api/src/modules/users/user.routes.ts +21 -0
  143. package/templates/fullstack/apps/api/src/modules/users/user.service.ts +71 -0
  144. package/templates/fullstack/apps/api/src/openapi.ts +32 -0
  145. package/templates/fullstack/apps/api/src/server.ts +29 -0
  146. package/templates/fullstack/apps/api/src/types.ts +15 -0
  147. package/templates/fullstack/apps/api/src/utils/crypto.ts +4 -0
  148. package/templates/fullstack/apps/api/src/utils/http.ts +33 -0
  149. package/templates/fullstack/apps/api/src/utils/sealed-value.ts +37 -0
  150. package/templates/fullstack/apps/api/tests/project.policy.test.ts +26 -0
  151. package/templates/fullstack/apps/api/tests/registration.test.ts +69 -0
  152. package/templates/fullstack/apps/api/tests/security.test.ts +98 -0
  153. package/templates/fullstack/apps/api/tests/validation.test.ts +37 -0
  154. package/templates/fullstack/apps/api/tsconfig.json +16 -0
  155. package/templates/fullstack/apps/api/vitest.config.ts +9 -0
  156. package/templates/fullstack/apps/web/index.html +14 -0
  157. package/templates/fullstack/apps/web/package.json +31 -0
  158. package/templates/fullstack/apps/web/public/authenik8-logo.svg +53 -0
  159. package/templates/fullstack/apps/web/src/App.tsx +45 -0
  160. package/templates/fullstack/apps/web/src/auth/AuthProvider.tsx +53 -0
  161. package/templates/fullstack/apps/web/src/auth/RouteGuards.tsx +16 -0
  162. package/templates/fullstack/apps/web/src/auth/providers.ts +3 -0
  163. package/templates/fullstack/apps/web/src/components/AppShell.tsx +80 -0
  164. package/templates/fullstack/apps/web/src/components/AuthShell.tsx +26 -0
  165. package/templates/fullstack/apps/web/src/components/Page.tsx +30 -0
  166. package/templates/fullstack/apps/web/src/features/projects/ProjectPages.tsx +79 -0
  167. package/templates/fullstack/apps/web/src/main.tsx +24 -0
  168. package/templates/fullstack/apps/web/src/pages/DashboardPage.tsx +48 -0
  169. package/templates/fullstack/apps/web/src/pages/StatusPages.tsx +8 -0
  170. package/templates/fullstack/apps/web/src/pages/admin/AuditPage.tsx +19 -0
  171. package/templates/fullstack/apps/web/src/pages/admin/UsersPage.tsx +24 -0
  172. package/templates/fullstack/apps/web/src/pages/auth/LoginPage.tsx +51 -0
  173. package/templates/fullstack/apps/web/src/pages/auth/OAuthCallbackPage.tsx +26 -0
  174. package/templates/fullstack/apps/web/src/pages/auth/RecoveryPages.tsx +53 -0
  175. package/templates/fullstack/apps/web/src/pages/auth/RegisterPage.tsx +86 -0
  176. package/templates/fullstack/apps/web/src/pages/auth/VerifyEmailPage.tsx +24 -0
  177. package/templates/fullstack/apps/web/src/pages/settings/ProfilePage.tsx +34 -0
  178. package/templates/fullstack/apps/web/src/pages/settings/SecurityPage.tsx +57 -0
  179. package/templates/fullstack/apps/web/src/styles.css +289 -0
  180. package/templates/fullstack/apps/web/src/token-storage.test.ts +12 -0
  181. package/templates/fullstack/apps/web/tsconfig.app.json +19 -0
  182. package/templates/fullstack/apps/web/tsconfig.json +7 -0
  183. package/templates/fullstack/apps/web/tsconfig.node.json +10 -0
  184. package/templates/fullstack/apps/web/vite.config.ts +18 -0
  185. package/templates/fullstack/docker-compose.yml +35 -0
  186. package/templates/fullstack/docs/PRODUCTION.md +30 -0
  187. package/templates/fullstack/gitignore.template +8 -0
  188. package/templates/fullstack/package.json +39 -0
  189. package/templates/fullstack/packages/api-client/package.json +20 -0
  190. package/templates/fullstack/packages/api-client/src/index.ts +203 -0
  191. package/templates/fullstack/packages/api-client/tsconfig.json +14 -0
  192. package/templates/fullstack/packages/contracts/package.json +20 -0
  193. package/templates/fullstack/packages/contracts/src/index.ts +146 -0
  194. package/templates/fullstack/packages/contracts/tsconfig.json +13 -0
  195. package/templates/fullstack/packages/ui/package.json +22 -0
  196. package/templates/fullstack/packages/ui/src/index.tsx +33 -0
  197. package/templates/fullstack/packages/ui/tsconfig.json +14 -0
  198. package/templates/prisma/postgresql/.env.example +6 -2
  199. package/templates/prisma/postgresql/client.ts +17 -0
  200. package/templates/prisma/postgresql/prisma.config.ts +12 -0
  201. package/templates/prisma/postgresql/schema.prisma +0 -1
  202. package/templates/prisma/sqlite/.env.example +6 -2
  203. package/templates/prisma/sqlite/client.ts +16 -0
  204. package/templates/prisma/sqlite/prisma.config.ts +12 -0
  205. package/templates/prisma/sqlite/schema.prisma +0 -1
  206. package/dist/src/tests/templated-routes.test.d.ts +0 -2
  207. package/dist/src/tests/templated-routes.test.d.ts.map +0 -1
  208. package/dist/src/tests/templated-routes.test.js +0 -325
  209. package/dist/src/tests/templated-routes.test.js.map +0 -1
  210. package/dist/templates/THREAT_MODEL.md +0 -138
  211. package/dist/templates/express-auth/src/controllers/protected.controller.d.ts +0 -8
  212. package/dist/templates/express-auth/src/controllers/protected.controller.d.ts.map +0 -1
  213. package/dist/templates/express-auth/src/controllers/protected.controller.js +0 -50
  214. package/dist/templates/express-auth/src/controllers/protected.controller.js.map +0 -1
  215. package/dist/templates/express-auth/src/routes/protected.routes.d.ts +0 -2
  216. package/dist/templates/express-auth/src/routes/protected.routes.d.ts.map +0 -1
  217. package/dist/templates/express-auth/src/routes/protected.routes.js +0 -16
  218. package/dist/templates/express-auth/src/routes/protected.routes.js.map +0 -1
  219. package/dist/templates/express-auth/src/utils/security.d.ts +0 -8
  220. package/dist/templates/express-auth/src/utils/security.d.ts.map +0 -1
  221. package/dist/templates/express-auth/src/utils/security.js +0 -55
  222. package/dist/templates/express-auth/src/utils/security.js.map +0 -1
  223. package/dist/templates/express-base/controllers/base.controller.d.ts +0 -12
  224. package/dist/templates/express-base/controllers/base.controller.d.ts.map +0 -1
  225. package/dist/templates/express-base/controllers/base.controller.js +0 -77
  226. package/dist/templates/express-base/controllers/base.controller.js.map +0 -1
  227. package/dist/templates/express-base/routes/base.routes.d.ts +0 -2
  228. package/dist/templates/express-base/routes/base.routes.d.ts.map +0 -1
  229. package/dist/templates/express-base/routes/base.routes.js +0 -20
  230. package/dist/templates/express-base/routes/base.routes.js.map +0 -1
  231. package/dist/templates/express-base/utils/security.d.ts +0 -5
  232. package/dist/templates/express-base/utils/security.d.ts.map +0 -1
  233. package/dist/templates/express-base/utils/security.js +0 -40
  234. package/dist/templates/express-base/utils/security.js.map +0 -1
  235. package/dist/templates/package.json +0 -27
@@ -0,0 +1,257 @@
1
+ import bcrypt from "bcryptjs";
2
+ import type { Request } from "express";
3
+ import type { User } from "@prisma/client";
4
+ import { z } from "zod";
5
+ import type { OAuthProvider, RegisterInput } from "@authenik8/contracts";
6
+ import { env } from "../config/env.js";
7
+ import { prisma } from "../config/prisma.js";
8
+ import { AppError } from "../utils/http.js";
9
+ import { hashToken, randomToken } from "../utils/crypto.js";
10
+ import { openSealedValue, sealValue } from "../utils/sealed-value.js";
11
+ import { getAuthenik8, redis } from "./authenik8.js";
12
+ import { presentUser } from "../modules/users/user.presenter.js";
13
+ import { sendPasswordResetEmail, sendVerificationEmail } from "../config/mailer.js";
14
+
15
+ const genericCredentials = new AppError(401, "INVALID_CREDENTIALS", "Email or password is incorrect");
16
+ const requestMetadataSchema = z.object({
17
+ userAgent: z.string().trim().min(1).max(300),
18
+ ipAddress: z.string().trim().min(1).max(64),
19
+ });
20
+ const oauthExchangeSessionSchema = z.strictObject({
21
+ accessToken: z.string().min(1).max(8192),
22
+ refreshToken: z.string().min(1).max(4096),
23
+ user: z.strictObject({
24
+ id: z.string().uuid(),
25
+ email: z.string().email().max(254),
26
+ name: z.string().min(1).max(80),
27
+ role: z.enum(["USER", "ADMIN"]),
28
+ status: z.enum(["ACTIVE", "SUSPENDED"]),
29
+ verified: z.boolean(),
30
+ createdAt: z.string().datetime(),
31
+ }),
32
+ });
33
+
34
+ function sessionMetadata(req: Request) {
35
+ return requestMetadataSchema.parse({
36
+ userAgent: (req.get("user-agent") ?? "Unknown device").slice(0, 300),
37
+ ipAddress: (req.ip ?? "unknown").slice(0, 64),
38
+ });
39
+ }
40
+
41
+ export async function issueSession(user: User, req: Request) {
42
+ const tokens = await getAuthenik8().issueTokens({ userId: user.id, email: user.email, role: user.role });
43
+ const accessToken = await tokens.accessToken;
44
+ const refreshToken = tokens.refreshToken;
45
+ const accessPayload = await getAuthenik8().verifyToken(accessToken);
46
+ if (!accessPayload?.sessionId) {
47
+ throw new AppError(500, "SESSION_ISSUE_FAILED", "Unable to create an authenticated session");
48
+ }
49
+
50
+ await prisma.session.create({
51
+ data: {
52
+ userId: user.id,
53
+ coreSessionId: accessPayload.sessionId,
54
+ refreshHash: hashToken(refreshToken),
55
+ ...sessionMetadata(req),
56
+ expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
57
+ },
58
+ });
59
+
60
+ return { accessToken, refreshToken, user: presentUser(user) };
61
+ }
62
+
63
+ export async function register(input: RegisterInput) {
64
+ const existing = await prisma.user.findUnique({ where: { email: input.email } });
65
+ if (existing) return { message: "Check your inbox to continue" };
66
+
67
+ const rawToken = randomToken();
68
+ await prisma.user.create({
69
+ data: {
70
+ email: input.email,
71
+ name: input.name,
72
+ passwordHash: await bcrypt.hash(input.password, 12),
73
+ verificationTokens: {
74
+ create: { tokenHash: hashToken(rawToken), expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000) },
75
+ },
76
+ },
77
+ });
78
+ await sendVerificationEmail(input.email, rawToken);
79
+
80
+ return {
81
+ message: "Check your inbox to continue",
82
+ ...(env.NODE_ENV === "development" ? { devVerificationToken: rawToken } : {}),
83
+ };
84
+ }
85
+
86
+ export async function login(input: { email: string; password: string }, req: Request) {
87
+ const user = await prisma.user.findUnique({ where: { email: input.email } });
88
+ if (!user?.passwordHash || user.status !== "ACTIVE") throw genericCredentials;
89
+ if (!(await bcrypt.compare(input.password, user.passwordHash))) throw genericCredentials;
90
+ return issueSession(user, req);
91
+ }
92
+
93
+ export async function rotateSession(refreshToken: string | undefined) {
94
+ if (!refreshToken) throw new AppError(401, "REFRESH_REQUIRED", "Refresh session is missing");
95
+ const session = await prisma.session.findUnique({
96
+ where: { refreshHash: hashToken(refreshToken) },
97
+ include: { user: true },
98
+ });
99
+ if (!session || session.revokedAt || session.expiresAt <= new Date() || session.user.status !== "ACTIVE") {
100
+ throw new AppError(401, "REFRESH_REJECTED", "Refresh session is invalid or expired");
101
+ }
102
+
103
+ try {
104
+ const rotated = await getAuthenik8().refreshToken(refreshToken);
105
+ await prisma.session.update({
106
+ where: { id: session.id },
107
+ data: { refreshHash: hashToken(rotated.refreshToken), lastUsedAt: new Date() },
108
+ });
109
+ return { accessToken: rotated.accessToken as string, refreshToken: rotated.refreshToken as string, user: presentUser(session.user) };
110
+ } catch {
111
+ await prisma.session.update({ where: { id: session.id }, data: { revokedAt: new Date() } });
112
+ throw new AppError(401, "REFRESH_REJECTED", "Refresh session is invalid or expired");
113
+ }
114
+ }
115
+
116
+ export async function revokeRefreshToken(refreshToken: string | undefined) {
117
+ if (!refreshToken) return;
118
+ const session = await prisma.session.findUnique({ where: { refreshHash: hashToken(refreshToken) } });
119
+ if (!session) return;
120
+ await prisma.session.update({ where: { id: session.id }, data: { revokedAt: new Date() } });
121
+ await getAuthenik8().revokeSession(session.userId, session.coreSessionId);
122
+ }
123
+
124
+ export async function requestPasswordReset(email: string) {
125
+ const user = await prisma.user.findUnique({ where: { email } });
126
+ if (!user?.passwordHash) return { message: "If that account exists, a reset link has been sent" };
127
+
128
+ const rawToken = randomToken();
129
+ await prisma.passwordResetToken.create({
130
+ data: { userId: user.id, tokenHash: hashToken(rawToken), expiresAt: new Date(Date.now() + 30 * 60 * 1000) },
131
+ });
132
+ await sendPasswordResetEmail(user.email, rawToken);
133
+ return {
134
+ message: "If that account exists, a reset link has been sent",
135
+ ...(env.NODE_ENV === "development" ? { devResetToken: rawToken } : {}),
136
+ };
137
+ }
138
+
139
+ export async function resetPassword(token: string, password: string) {
140
+ const reset = await prisma.passwordResetToken.findUnique({ where: { tokenHash: hashToken(token) } });
141
+ if (!reset || reset.usedAt || reset.expiresAt <= new Date()) {
142
+ throw new AppError(400, "RESET_TOKEN_INVALID", "This reset link is invalid or expired");
143
+ }
144
+ const now = new Date();
145
+ await prisma.$transaction([
146
+ prisma.passwordResetToken.update({ where: { id: reset.id }, data: { usedAt: now } }),
147
+ prisma.user.update({ where: { id: reset.userId }, data: { passwordHash: await bcrypt.hash(password, 12), passwordUpdatedAt: now } }),
148
+ prisma.session.updateMany({ where: { userId: reset.userId, revokedAt: null }, data: { revokedAt: now } }),
149
+ ]);
150
+ await getAuthenik8().revokeAllSessions(reset.userId);
151
+ }
152
+
153
+ export async function verifyEmail(token: string) {
154
+ const verification = await prisma.emailVerificationToken.findUnique({ where: { tokenHash: hashToken(token) } });
155
+ if (!verification || verification.usedAt || verification.expiresAt <= new Date()) {
156
+ throw new AppError(400, "VERIFICATION_INVALID", "This verification link is invalid or expired");
157
+ }
158
+ const now = new Date();
159
+ await prisma.$transaction([
160
+ prisma.emailVerificationToken.update({ where: { id: verification.id }, data: { usedAt: now } }),
161
+ prisma.user.update({ where: { id: verification.userId }, data: { emailVerifiedAt: now } }),
162
+ ]);
163
+ }
164
+
165
+ export async function resendVerification(userId: string) {
166
+ const user = await prisma.user.findUniqueOrThrow({ where: { id: userId } });
167
+ if (user.emailVerifiedAt) return { message: "Email is already verified" };
168
+ const rawToken = randomToken();
169
+ await prisma.emailVerificationToken.create({
170
+ data: { userId, tokenHash: hashToken(rawToken), expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000) },
171
+ });
172
+ await sendVerificationEmail(user.email, rawToken);
173
+ return {
174
+ message: "Verification email sent",
175
+ ...(env.NODE_ENV === "development" ? { devVerificationToken: rawToken } : {}),
176
+ };
177
+ }
178
+
179
+ export function oauthProvider(name: OAuthProvider) {
180
+ const provider = getAuthenik8().oauth?.[name];
181
+ if (!provider) throw new AppError(404, "PROVIDER_NOT_CONFIGURED", "OAuth provider is not configured");
182
+ return { name, provider };
183
+ }
184
+
185
+ export async function createLinkIntent(userId: string, provider: OAuthProvider) {
186
+ oauthProvider(provider);
187
+ const ticket = randomToken();
188
+ await redis.setex(`oauth:link:${ticket}`, 120, userId);
189
+ return `/api/auth/oauth/${provider}/link?ticket=${ticket}`;
190
+ }
191
+
192
+ export async function consumeLinkIntent(ticket: string | undefined) {
193
+ if (!ticket) throw new AppError(400, "LINK_TICKET_INVALID", "Account-link request is missing");
194
+ const key = `oauth:link:${ticket}`;
195
+ const userId = await redis.get(key);
196
+ if (!userId) throw new AppError(400, "LINK_TICKET_INVALID", "Account-link request is invalid or expired");
197
+ await redis.del(key);
198
+ return userId;
199
+ }
200
+
201
+ export async function completeOAuthCallback(provider: OAuthProvider, result: Awaited<ReturnType<ReturnType<typeof oauthProvider>["provider"]["handleCallback"]>>, req: Request) {
202
+ const profile = result.profile;
203
+ if (profile.email_verified !== true && profile.email_verified !== "true") {
204
+ throw new AppError(403, "OAUTH_EMAIL_UNVERIFIED", "OAuth provider email must be verified");
205
+ }
206
+
207
+ if (result.mode === "link" && result.userId) {
208
+ const claimed = await prisma.oAuthAccount.findUnique({
209
+ where: { provider_providerAccountId: { provider, providerAccountId: profile.providerId } },
210
+ });
211
+ if (claimed && claimed.userId !== result.userId) throw new AppError(409, "PROVIDER_ALREADY_LINKED", "Provider account is linked to another user");
212
+ await prisma.oAuthAccount.upsert({
213
+ where: { userId_provider: { userId: result.userId, provider } },
214
+ update: { providerAccountId: profile.providerId, providerEmail: profile.email },
215
+ create: { userId: result.userId, provider, providerAccountId: profile.providerId, providerEmail: profile.email },
216
+ });
217
+ return { linked: true as const, provider };
218
+ }
219
+
220
+ const account = await prisma.oAuthAccount.findUnique({
221
+ where: { provider_providerAccountId: { provider, providerAccountId: profile.providerId } },
222
+ include: { user: true },
223
+ });
224
+ let user = account?.user;
225
+ if (!user) {
226
+ user = await prisma.user.upsert({
227
+ where: { email: profile.email.toLowerCase() },
228
+ update: { emailVerifiedAt: new Date() },
229
+ create: { email: profile.email.toLowerCase(), name: profile.name ?? "New user", emailVerifiedAt: new Date() },
230
+ });
231
+ await prisma.oAuthAccount.upsert({
232
+ where: { userId_provider: { userId: user.id, provider } },
233
+ update: { providerAccountId: profile.providerId, providerEmail: profile.email },
234
+ create: { userId: user.id, provider, providerAccountId: profile.providerId, providerEmail: profile.email },
235
+ });
236
+ }
237
+ if (user.status !== "ACTIVE") throw new AppError(403, "ACCOUNT_SUSPENDED", "Account access is unavailable");
238
+
239
+ const session = await issueSession(user, req);
240
+ const code = randomToken();
241
+ await redis.setex(
242
+ `oauth:exchange:${code}`,
243
+ 60,
244
+ sealValue(JSON.stringify(session), env.REFRESH_SECRET),
245
+ );
246
+ return { linked: false as const, code };
247
+ }
248
+
249
+ export async function exchangeOAuthCode(code: string) {
250
+ const key = `oauth:exchange:${code}`;
251
+ const value = await redis.get(key);
252
+ if (!value) throw new AppError(400, "OAUTH_CODE_INVALID", "OAuth exchange is invalid or expired");
253
+ await redis.del(key);
254
+ const payload = openSealedValue(value, env.REFRESH_SECRET);
255
+ if (!payload) throw new AppError(400, "OAUTH_CODE_INVALID", "OAuth exchange is invalid or expired");
256
+ return oauthExchangeSessionSchema.parse(JSON.parse(payload));
257
+ }
@@ -0,0 +1,67 @@
1
+ import { createAuthenik8 } from "authenik8-core";
2
+ import { Redis } from "ioredis";
3
+ import { env } from "../config/env.js";
4
+
5
+ export const redis = new Redis(env.REDIS_URL, {
6
+ maxRetriesPerRequest: 2,
7
+ enableReadyCheck: true,
8
+ });
9
+
10
+ type AuthInstance = Awaited<ReturnType<typeof createAuthenik8>>;
11
+ let instance: AuthInstance | undefined;
12
+
13
+ function oauthConfig() {
14
+ return {
15
+ ...(env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET && env.GOOGLE_REDIRECT_URI
16
+ ? { google: { clientId: env.GOOGLE_CLIENT_ID, clientSecret: env.GOOGLE_CLIENT_SECRET, redirectUri: env.GOOGLE_REDIRECT_URI } }
17
+ : {}),
18
+ ...(env.GITHUB_CLIENT_ID && env.GITHUB_CLIENT_SECRET && env.GITHUB_REDIRECT_URI
19
+ ? { github: { clientId: env.GITHUB_CLIENT_ID, clientSecret: env.GITHUB_CLIENT_SECRET, redirectUri: env.GITHUB_REDIRECT_URI } }
20
+ : {}),
21
+ };
22
+ }
23
+
24
+ export async function initAuthenik8(): Promise<AuthInstance> {
25
+ if (instance) return instance;
26
+ const oauth = oauthConfig();
27
+ const signingKeys = JSON.parse(env.AUTHENIK8_SIGNING_JWKS) as Array<{
28
+ kid: string;
29
+ d?: string;
30
+ [key: string]: unknown;
31
+ }>;
32
+ const activeKey = signingKeys.find((key) => key.kid === env.AUTHENIK8_ACTIVE_KID);
33
+ if (!activeKey?.d) {
34
+ throw new Error("AUTHENIK8_ACTIVE_KID must select a private signing JWK");
35
+ }
36
+ const agentRegistry = JSON.parse(env.AUTHENIK8_AGENTS) as Record<string, string[]>;
37
+ const agent = Object.keys(agentRegistry).length
38
+ ? {
39
+ resolveAgent: async (agentId: string) => {
40
+ const scopes = Object.prototype.hasOwnProperty.call(agentRegistry, agentId)
41
+ ? agentRegistry[agentId]
42
+ : undefined;
43
+ return scopes ? { agentId, scopes, active: true } : null;
44
+ },
45
+ }
46
+ : undefined;
47
+ instance = await createAuthenik8({
48
+ jwt: {
49
+ keys: signingKeys,
50
+ activeKid: env.AUTHENIK8_ACTIVE_KID,
51
+ issuer: env.AUTHENIK8_ISSUER,
52
+ audience: env.AUTHENIK8_AUDIENCE,
53
+ },
54
+ refreshSecret: env.REFRESH_SECRET,
55
+ agent,
56
+ jwtExpiry: "15m",
57
+ redis,
58
+ oauth: Object.keys(oauth).length ? oauth : undefined,
59
+ trustProxyHeaders: env.TRUST_PROXY,
60
+ });
61
+ return instance;
62
+ }
63
+
64
+ export function getAuthenik8(): AuthInstance {
65
+ if (!instance) throw new Error("Authenik8 has not been initialized");
66
+ return instance;
67
+ }
@@ -0,0 +1,36 @@
1
+ import type { CookieOptions, Request, Response } from "express";
2
+ import { z } from "zod";
3
+ import { env } from "../config/env.js";
4
+ import { openSealedValue, sealValue } from "../utils/sealed-value.js";
5
+
6
+ export const refreshCookieName = "authenik8_refresh";
7
+ const refreshTokenSchema = z.string().min(1).max(4096);
8
+
9
+ export function refreshCookieOptions(): CookieOptions {
10
+ return {
11
+ httpOnly: true,
12
+ secure: env.COOKIE_SECURE || env.NODE_ENV === "production",
13
+ sameSite: "strict",
14
+ path: "/api",
15
+ maxAge: 7 * 24 * 60 * 60 * 1000,
16
+ };
17
+ }
18
+
19
+ export function setRefreshCookie(res: Response, token: string) {
20
+ const sealedToken = sealValue(refreshTokenSchema.parse(token), env.REFRESH_SECRET);
21
+ // The bearer token is AES-256-GCM encrypted before it reaches cookie storage.
22
+ // codeql[js/clear-text-storage-of-sensitive-data]
23
+ res.cookie(refreshCookieName, sealedToken, refreshCookieOptions());
24
+ }
25
+
26
+ export function readRefreshCookie(req: Request): string | undefined {
27
+ const sealedToken = req.cookies?.[refreshCookieName];
28
+ const token = openSealedValue(sealedToken, env.REFRESH_SECRET);
29
+ const result = refreshTokenSchema.safeParse(token);
30
+ return result.success ? result.data : undefined;
31
+ }
32
+
33
+ export function clearRefreshCookie(res: Response) {
34
+ const { maxAge: _maxAge, ...options } = refreshCookieOptions();
35
+ res.clearCookie(refreshCookieName, options);
36
+ }
@@ -0,0 +1,72 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import dotenv from "dotenv";
4
+ import { z } from "zod";
5
+
6
+ const rootEnv = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../../.env");
7
+ dotenv.config({ path: process.env.AUTHENIK8_ENV_FILE ?? rootEnv });
8
+
9
+ const booleanString = z.enum(["true", "false"]).transform((value) => value === "true");
10
+ const signingJwks = z.string().min(1).superRefine((value, context) => {
11
+ try {
12
+ const keys = JSON.parse(value) as Array<Record<string, unknown>>;
13
+ if (!Array.isArray(keys) || !keys.length || keys.some((key) =>
14
+ key.kty !== "EC" || key.crv !== "P-256" || key.alg !== "ES256" ||
15
+ typeof key.kid !== "string" || typeof key.x !== "string" || typeof key.y !== "string"
16
+ )) {
17
+ throw new Error("invalid key");
18
+ }
19
+ } catch {
20
+ context.addIssue({ code: "custom", message: "must be a JSON array of ES256 P-256 JWKs" });
21
+ }
22
+ });
23
+ const agentRegistry = z.string().default("{}").superRefine((value, context) => {
24
+ try {
25
+ const agents = JSON.parse(value) as Record<string, unknown>;
26
+ const valid = agents && typeof agents === "object" && !Array.isArray(agents) &&
27
+ Object.entries(agents).every(([agentId, scopes]) =>
28
+ /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(agentId) &&
29
+ Array.isArray(scopes) && scopes.length > 0 && scopes.length <= 64 &&
30
+ scopes.every((scope) =>
31
+ typeof scope === "string" && scope.length <= 128 &&
32
+ /^[a-z][a-z0-9._/-]*(?::[a-z][a-z0-9._/-]*)+$/.test(scope)
33
+ )
34
+ );
35
+ if (!valid) throw new Error("invalid agent registry");
36
+ } catch {
37
+ context.addIssue({ code: "custom", message: "must map agent IDs to resource:action scope arrays" });
38
+ }
39
+ });
40
+
41
+ const schema = z.object({
42
+ NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
43
+ PORT: z.coerce.number().int().positive().default(3000),
44
+ WEB_ORIGIN: z.string().url().default("http://localhost:5173"),
45
+ DATABASE_URL: z.string().min(1),
46
+ REDIS_URL: z.string().min(1).default("redis://localhost:6379"),
47
+ AUTHENIK8_SIGNING_JWKS: signingJwks,
48
+ AUTHENIK8_ACTIVE_KID: z.string().min(1),
49
+ AUTHENIK8_ISSUER: z.string().url(),
50
+ AUTHENIK8_AUDIENCE: z.string().min(1),
51
+ AUTHENIK8_AGENTS: agentRegistry,
52
+ REFRESH_SECRET: z.string().min(32),
53
+ COOKIE_SECURE: booleanString.default(false),
54
+ TRUST_PROXY: booleanString.default(false),
55
+ LOG_LEVEL: z.string().default("info"),
56
+ EMAIL_FROM: z.string().default("Authenik8 <auth@example.com>"),
57
+ RESEND_API_KEY: z.string().optional(),
58
+ GOOGLE_CLIENT_ID: z.string().optional(),
59
+ GOOGLE_CLIENT_SECRET: z.string().optional(),
60
+ GOOGLE_REDIRECT_URI: z.string().url().optional().or(z.literal("")),
61
+ GITHUB_CLIENT_ID: z.string().optional(),
62
+ GITHUB_CLIENT_SECRET: z.string().optional(),
63
+ GITHUB_REDIRECT_URI: z.string().url().optional().or(z.literal("")),
64
+ });
65
+
66
+ const result = schema.safeParse(process.env);
67
+ if (!result.success) {
68
+ console.error("Invalid environment configuration", result.error.flatten().fieldErrors);
69
+ throw new Error("Environment validation failed");
70
+ }
71
+
72
+ export const env = result.data;
@@ -0,0 +1,17 @@
1
+ import pino from "pino";
2
+ import { env } from "./env.js";
3
+
4
+ export const logger = pino({
5
+ level: env.LOG_LEVEL,
6
+ redact: {
7
+ paths: [
8
+ "req.headers.authorization",
9
+ "req.headers.cookie",
10
+ "password",
11
+ "passwordHash",
12
+ "accessToken",
13
+ "refreshToken",
14
+ ],
15
+ censor: "[REDACTED]",
16
+ },
17
+ });
@@ -0,0 +1,32 @@
1
+ import { env } from "./env.js";
2
+ import { logger } from "./logger.js";
3
+
4
+ async function sendEmail(to: string, subject: string, html: string) {
5
+ if (!env.RESEND_API_KEY) {
6
+ if (env.NODE_ENV === "production") throw new Error("RESEND_API_KEY is required for production email delivery");
7
+ logger.info({ to, subject }, "Development email generated");
8
+ return;
9
+ }
10
+
11
+ const response = await fetch("https://api.resend.com/emails", {
12
+ method: "POST",
13
+ headers: {
14
+ Authorization: `Bearer ${env.RESEND_API_KEY}`,
15
+ "Content-Type": "application/json",
16
+ },
17
+ body: JSON.stringify({ from: env.EMAIL_FROM, to: [to], subject, html }),
18
+ });
19
+ if (!response.ok) throw new Error(`Email delivery failed with status ${response.status}`);
20
+ }
21
+
22
+ export function sendVerificationEmail(to: string, token: string) {
23
+ const url = new URL("/verify-email", env.WEB_ORIGIN);
24
+ url.searchParams.set("token", token);
25
+ return sendEmail(to, "Verify your email", `<p>Confirm your Authenik8 account:</p><p><a href="${url}">Verify email</a></p>`);
26
+ }
27
+
28
+ export function sendPasswordResetEmail(to: string, token: string) {
29
+ const url = new URL("/reset-password", env.WEB_ORIGIN);
30
+ url.searchParams.set("token", token);
31
+ return sendEmail(to, "Reset your password", `<p>A password reset was requested:</p><p><a href="${url}">Reset password</a></p><p>This link expires in 30 minutes.</p>`);
32
+ }
@@ -0,0 +1,27 @@
1
+ import { env } from "./env.js";
2
+
3
+ const loopbackHosts = new Set(["localhost", "127.0.0.1", "[::1]"]);
4
+
5
+ function toUrl(value: string): URL | undefined {
6
+ try {
7
+ return new URL(value);
8
+ } catch {
9
+ return undefined;
10
+ }
11
+ }
12
+
13
+ export function isAllowedOrigin(origin: string | undefined): boolean {
14
+ if (!origin) return false;
15
+ if (origin === env.WEB_ORIGIN) return true;
16
+ if (env.NODE_ENV === "production") return false;
17
+
18
+ const configured = toUrl(env.WEB_ORIGIN);
19
+ const candidate = toUrl(origin);
20
+ return Boolean(
21
+ configured
22
+ && candidate
23
+ && configured.protocol === candidate.protocol
24
+ && loopbackHosts.has(configured.hostname)
25
+ && loopbackHosts.has(candidate.hostname),
26
+ );
27
+ }
@@ -0,0 +1,9 @@
1
+ import { PrismaPg } from "@prisma/adapter-pg";
2
+ import { PrismaClient } from "@prisma/client";
3
+ import { env } from "./env.js";
4
+
5
+ const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
6
+
7
+ const adapter = new PrismaPg({ connectionString: env.DATABASE_URL });
8
+ export const prisma = globalForPrisma.prisma ?? new PrismaClient({ adapter });
9
+ if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
@@ -0,0 +1,35 @@
1
+ import type { RequestHandler } from "express";
2
+ import { prisma } from "../config/prisma.js";
3
+ import { getAuthenik8 } from "../auth/authenik8.js";
4
+
5
+ export const authenticate: RequestHandler = (req, res, next) =>
6
+ getAuthenik8().requireAuth(req, res, async () => {
7
+ const payload = req.user;
8
+ if (!payload?.userId) {
9
+ return res.status(401).json({
10
+ error: { code: "UNAUTHENTICATED", message: "Authentication required" },
11
+ requestId: req.id,
12
+ });
13
+ }
14
+
15
+ const user = await prisma.user.findUnique({ where: { id: payload.userId } });
16
+ if (!user || user.status !== "ACTIVE") {
17
+ return res.status(401).json({
18
+ error: { code: "SESSION_INVALID", message: "This session is no longer active" },
19
+ requestId: req.id,
20
+ });
21
+ }
22
+
23
+ req.user = { userId: user.id, email: user.email, name: user.name, role: user.role };
24
+ next();
25
+ });
26
+
27
+ export const requireAdmin: RequestHandler = (req, res, next) => {
28
+ if (req.user?.role !== "ADMIN") {
29
+ return res.status(403).json({
30
+ error: { code: "FORBIDDEN", message: "Administrator access required" },
31
+ requestId: req.id,
32
+ });
33
+ }
34
+ next();
35
+ };
@@ -0,0 +1,60 @@
1
+ import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
2
+ import { csrfTokenSchema } from "@authenik8/contracts";
3
+ import type { CookieOptions, Request, RequestHandler, Response } from "express";
4
+ import { env } from "../config/env.js";
5
+
6
+ export const csrfCookieName = "authenik8_csrf";
7
+
8
+ function csrfCookieOptions(): CookieOptions {
9
+ return {
10
+ httpOnly: true,
11
+ secure: env.COOKIE_SECURE || env.NODE_ENV === "production",
12
+ sameSite: "strict",
13
+ path: "/api",
14
+ maxAge: 2 * 60 * 60 * 1000,
15
+ };
16
+ }
17
+
18
+ function signature(nonce: string): string {
19
+ return createHmac("sha256", env.REFRESH_SECRET)
20
+ .update("authenik8:csrf:v1:")
21
+ .update(nonce)
22
+ .digest("base64url");
23
+ }
24
+
25
+ function safeEqual(left: string, right: string): boolean {
26
+ const leftBuffer = Buffer.from(left);
27
+ const rightBuffer = Buffer.from(right);
28
+ return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
29
+ }
30
+
31
+ function isAuthenticToken(value: unknown): value is string {
32
+ const parsed = csrfTokenSchema.safeParse(value);
33
+ if (!parsed.success) return false;
34
+ const [nonce, providedSignature] = parsed.data.split(".");
35
+ return safeEqual(providedSignature!, signature(nonce!));
36
+ }
37
+
38
+ export function issueCsrfToken(req: Request, res: Response): string {
39
+ const existingToken = req.cookies?.authenik8_csrf;
40
+ if (isAuthenticToken(existingToken)) return existingToken;
41
+
42
+ const nonce = randomBytes(32).toString("base64url");
43
+ const token = csrfTokenSchema.parse(`${nonce}.${signature(nonce)}`);
44
+ res.cookie(csrfCookieName, token, csrfCookieOptions());
45
+ return token;
46
+ }
47
+
48
+ export const requireCsrf: RequestHandler = (req, res, next) => {
49
+ const cookieToken = req.cookies?.authenik8_csrf;
50
+ const headerToken = req.get("x-csrf-token");
51
+ if (isAuthenticToken(cookieToken) && csrfTokenSchema.safeParse(headerToken).success && safeEqual(cookieToken, headerToken!)) {
52
+ next();
53
+ return;
54
+ }
55
+
56
+ res.status(403).json({
57
+ error: { code: "CSRF_REJECTED", message: "Request verification failed" },
58
+ requestId: req.id,
59
+ });
60
+ };
@@ -0,0 +1,12 @@
1
+ import type { RequestHandler } from "express";
2
+ import { isAllowedOrigin } from "../config/origins.js";
3
+
4
+ export const requireAllowedOrigin: RequestHandler = (req, res, next) => {
5
+ if (!isAllowedOrigin(req.get("origin"))) {
6
+ return res.status(403).json({
7
+ error: { code: "ORIGIN_REJECTED", message: "Request origin is not allowed" },
8
+ requestId: req.id,
9
+ });
10
+ }
11
+ next();
12
+ };
@@ -0,0 +1,9 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { RequestHandler } from "express";
3
+
4
+ export const requestId: RequestHandler = (req, res, next) => {
5
+ const incoming = req.get("x-request-id");
6
+ req.id = incoming && incoming.length <= 128 ? incoming : randomUUID();
7
+ res.setHeader("x-request-id", req.id);
8
+ next();
9
+ };
@@ -0,0 +1,17 @@
1
+ import { asyncHandler } from "../../utils/http.js";
2
+ import { identifierSchema, pageSchema } from "@authenik8/contracts";
3
+ import { listAuditEvents, listUsers, revokeAllSessions, updateUser } from "./admin.service.js";
4
+
5
+ export const listUsersController = asyncHandler(async (req, res) => {
6
+ res.json(await listUsers(pageSchema.parse(req.query.page)));
7
+ });
8
+ export const updateUserController = asyncHandler(async (req, res) => {
9
+ res.json({ user: await updateUser(req.user!.userId, identifierSchema.parse(req.params.id), req.body, req.ip ?? "unknown") });
10
+ });
11
+ export const revokeUserSessionsController = asyncHandler(async (req, res) => {
12
+ await revokeAllSessions(req.user!.userId, identifierSchema.parse(req.params.id), req.ip ?? "unknown");
13
+ res.json({ message: "Sessions revoked" });
14
+ });
15
+ export const auditController = asyncHandler(async (_req, res) => {
16
+ res.json({ events: await listAuditEvents() });
17
+ });