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,14 @@
1
+ import { Router } from "express";
2
+ import { authenticate, requireAdmin } from "../../middleware/authenticate.js";
3
+ import { requireCsrf } from "../../middleware/csrf.js";
4
+ import { requireAllowedOrigin } from "../../middleware/origin.js";
5
+ import { auditController, listUsersController, revokeUserSessionsController, updateUserController } from "./admin.controller.js";
6
+
7
+ export const adminRoutes = Router();
8
+ // authenik8-core's global Redis-backed limiter runs before this router.
9
+ // codeql[js/missing-rate-limiting]
10
+ adminRoutes.use(authenticate, requireAdmin);
11
+ adminRoutes.get("/users", listUsersController);
12
+ adminRoutes.patch("/users/:id", requireAllowedOrigin, requireCsrf, updateUserController);
13
+ adminRoutes.delete("/users/:id/sessions", requireAllowedOrigin, requireCsrf, revokeUserSessionsController);
14
+ adminRoutes.get("/audit", auditController);
@@ -0,0 +1,54 @@
1
+ import { adminUserUpdateSchema } from "@authenik8/contracts";
2
+ import { prisma } from "../../config/prisma.js";
3
+ import { getAuthenik8 } from "../../auth/authenik8.js";
4
+ import { AppError } from "../../utils/http.js";
5
+ import { presentUser } from "../users/user.presenter.js";
6
+
7
+ export async function listUsers(page: number) {
8
+ const pageSize = 20;
9
+ const safePage = Math.max(1, page);
10
+ const [items, total] = await prisma.$transaction([
11
+ prisma.user.findMany({ orderBy: { createdAt: "desc" }, skip: (safePage - 1) * pageSize, take: pageSize }),
12
+ prisma.user.count(),
13
+ ]);
14
+ return { items: items.map(presentUser), total, page: safePage, pageSize };
15
+ }
16
+
17
+ export async function updateUser(actorId: string, targetId: string, body: unknown, ipAddress: string) {
18
+ const input = adminUserUpdateSchema.parse(body);
19
+ if (actorId === targetId && (input.role === "USER" || input.status === "SUSPENDED")) {
20
+ throw new AppError(400, "SELF_LOCKOUT", "You cannot remove your own administrator access");
21
+ }
22
+ const user = await prisma.user.update({ where: { id: targetId }, data: input });
23
+ await prisma.auditEvent.create({
24
+ data: { actorId, action: "admin.user.updated", targetType: "User", targetId, metadata: input, ipAddress },
25
+ });
26
+ if (input.status === "SUSPENDED") await revokeAllSessions(actorId, targetId, ipAddress, false);
27
+ return presentUser(user);
28
+ }
29
+
30
+ export async function revokeAllSessions(actorId: string, targetId: string, ipAddress: string, audit = true) {
31
+ await prisma.session.updateMany({ where: { userId: targetId, revokedAt: null }, data: { revokedAt: new Date() } });
32
+ await getAuthenik8().revokeAllSessions(targetId);
33
+ if (audit) {
34
+ await prisma.auditEvent.create({
35
+ data: { actorId, action: "admin.sessions.revoked", targetType: "User", targetId, ipAddress },
36
+ });
37
+ }
38
+ }
39
+
40
+ export async function listAuditEvents() {
41
+ const events = await prisma.auditEvent.findMany({
42
+ include: { actor: { select: { email: true } } },
43
+ orderBy: { createdAt: "desc" },
44
+ take: 100,
45
+ });
46
+ return events.map((event) => ({
47
+ id: event.id,
48
+ action: event.action,
49
+ actorEmail: event.actor?.email ?? null,
50
+ targetType: event.targetType,
51
+ targetId: event.targetId,
52
+ createdAt: event.createdAt.toISOString(),
53
+ }));
54
+ }
@@ -0,0 +1,22 @@
1
+ import { asyncHandler } from "../../utils/http.js";
2
+ import { identifierSchema } from "@authenik8/contracts";
3
+ import { createProject, deleteProject, getProject, listProjects, updateProject } from "./project.service.js";
4
+
5
+ const actor = (user: NonNullable<Express.Request["user"]>) => ({ userId: user.userId, role: user.role });
6
+
7
+ export const listProjectsController = asyncHandler(async (req, res) => {
8
+ res.json({ projects: await listProjects(actor(req.user!)) });
9
+ });
10
+ export const getProjectController = asyncHandler(async (req, res) => {
11
+ res.json({ project: await getProject(actor(req.user!), identifierSchema.parse(req.params.id)) });
12
+ });
13
+ export const createProjectController = asyncHandler(async (req, res) => {
14
+ res.status(201).json({ project: await createProject(actor(req.user!), req.body) });
15
+ });
16
+ export const updateProjectController = asyncHandler(async (req, res) => {
17
+ res.json({ project: await updateProject(actor(req.user!), identifierSchema.parse(req.params.id), req.body) });
18
+ });
19
+ export const deleteProjectController = asyncHandler(async (req, res) => {
20
+ await deleteProject(actor(req.user!), identifierSchema.parse(req.params.id));
21
+ res.status(204).send();
22
+ });
@@ -0,0 +1,14 @@
1
+ type Actor = { userId: string; role: "USER" | "ADMIN" };
2
+ type Resource = { ownerId: string };
3
+
4
+ export function canReadProject(actor: Actor, project: Resource): boolean {
5
+ return actor.role === "ADMIN" || project.ownerId === actor.userId;
6
+ }
7
+
8
+ export function canWriteProject(actor: Actor, project: Resource): boolean {
9
+ return actor.role === "ADMIN" || project.ownerId === actor.userId;
10
+ }
11
+
12
+ export function projectListScope(actor: Actor) {
13
+ return actor.role === "ADMIN" ? {} : { ownerId: actor.userId };
14
+ }
@@ -0,0 +1,20 @@
1
+ import type { Prisma, Project } from "@prisma/client";
2
+ import { prisma } from "../../config/prisma.js";
3
+
4
+ export const projectRepository = {
5
+ list(where: Prisma.ProjectWhereInput): Promise<Project[]> {
6
+ return prisma.project.findMany({ where, orderBy: { updatedAt: "desc" } });
7
+ },
8
+ findById(id: string): Promise<Project | null> {
9
+ return prisma.project.findUnique({ where: { id } });
10
+ },
11
+ create(data: Prisma.ProjectUncheckedCreateInput): Promise<Project> {
12
+ return prisma.project.create({ data });
13
+ },
14
+ update(id: string, data: Prisma.ProjectUpdateInput): Promise<Project> {
15
+ return prisma.project.update({ where: { id }, data });
16
+ },
17
+ remove(id: string): Promise<Project> {
18
+ return prisma.project.delete({ where: { id } });
19
+ },
20
+ };
@@ -0,0 +1,21 @@
1
+ import { Router } from "express";
2
+ import { authenticate } from "../../middleware/authenticate.js";
3
+ import { requireCsrf } from "../../middleware/csrf.js";
4
+ import { requireAllowedOrigin } from "../../middleware/origin.js";
5
+ import {
6
+ createProjectController,
7
+ deleteProjectController,
8
+ getProjectController,
9
+ listProjectsController,
10
+ updateProjectController,
11
+ } from "./project.controller.js";
12
+
13
+ export const projectRoutes = Router();
14
+ // authenik8-core's global Redis-backed limiter runs before this router.
15
+ // codeql[js/missing-rate-limiting]
16
+ projectRoutes.use(authenticate);
17
+ projectRoutes.get("/", listProjectsController);
18
+ projectRoutes.post("/", requireAllowedOrigin, requireCsrf, createProjectController);
19
+ projectRoutes.get("/:id", getProjectController);
20
+ projectRoutes.patch("/:id", requireAllowedOrigin, requireCsrf, updateProjectController);
21
+ projectRoutes.delete("/:id", requireAllowedOrigin, requireCsrf, deleteProjectController);
@@ -0,0 +1,41 @@
1
+ import { projectCreateSchema, projectUpdateSchema } from "@authenik8/contracts";
2
+ import type { Project } from "@prisma/client";
3
+ import { AppError } from "../../utils/http.js";
4
+ import { canReadProject, canWriteProject, projectListScope } from "./project.policy.js";
5
+ import { projectRepository } from "./project.repository.js";
6
+
7
+ type Actor = { userId: string; role: "USER" | "ADMIN" };
8
+
9
+ function present(project: Project) {
10
+ return { ...project, createdAt: project.createdAt.toISOString(), updatedAt: project.updatedAt.toISOString() };
11
+ }
12
+
13
+ async function authorizedProject(actor: Actor, id: string, write = false) {
14
+ const project = await projectRepository.findById(id);
15
+ const allowed = project && (write ? canWriteProject(actor, project) : canReadProject(actor, project));
16
+ if (!project || !allowed) throw new AppError(404, "PROJECT_NOT_FOUND", "Project not found");
17
+ return project;
18
+ }
19
+
20
+ export async function listProjects(actor: Actor) {
21
+ return (await projectRepository.list(projectListScope(actor))).map(present);
22
+ }
23
+
24
+ export async function getProject(actor: Actor, id: string) {
25
+ return present(await authorizedProject(actor, id));
26
+ }
27
+
28
+ export async function createProject(actor: Actor, body: unknown) {
29
+ const input = projectCreateSchema.parse(body);
30
+ return present(await projectRepository.create({ ...input, ownerId: actor.userId }));
31
+ }
32
+
33
+ export async function updateProject(actor: Actor, id: string, body: unknown) {
34
+ await authorizedProject(actor, id, true);
35
+ return present(await projectRepository.update(id, projectUpdateSchema.parse(body)));
36
+ }
37
+
38
+ export async function deleteProject(actor: Actor, id: string) {
39
+ await authorizedProject(actor, id, true);
40
+ await projectRepository.remove(id);
41
+ }
@@ -0,0 +1,26 @@
1
+ import { clearRefreshCookie } from "../../auth/cookies.js";
2
+ import { identifierSchema } from "@authenik8/contracts";
3
+ import { asyncHandler } from "../../utils/http.js";
4
+ import { changePassword, listProviders, listSessions, revokeSession, updateProfile } from "./user.service.js";
5
+
6
+ export const updateProfileController = asyncHandler(async (req, res) => {
7
+ res.json({ user: await updateProfile(req.user!.userId, req.body) });
8
+ });
9
+
10
+ export const changePasswordController = asyncHandler(async (req, res) => {
11
+ await changePassword(req.user!.userId, req.body);
12
+ res.json({ message: "Password updated" });
13
+ });
14
+
15
+ export const sessionsController = asyncHandler(async (req, res) => {
16
+ res.json({ sessions: await listSessions(req.user!.userId, req) });
17
+ });
18
+
19
+ export const revokeSessionController = asyncHandler(async (req, res) => {
20
+ if (await revokeSession(req.user!.userId, identifierSchema.parse(req.params.id), req)) clearRefreshCookie(res);
21
+ res.json({ message: "Session revoked" });
22
+ });
23
+
24
+ export const providersController = asyncHandler(async (req, res) => {
25
+ res.json({ providers: await listProviders(req.user!.userId) });
26
+ });
@@ -0,0 +1,13 @@
1
+ import type { User } from "@prisma/client";
2
+
3
+ export function presentUser(user: User) {
4
+ return {
5
+ id: user.id,
6
+ email: user.email,
7
+ name: user.name,
8
+ role: user.role,
9
+ status: user.status,
10
+ verified: user.emailVerifiedAt !== null,
11
+ createdAt: user.createdAt.toISOString(),
12
+ };
13
+ }
@@ -0,0 +1,21 @@
1
+ import { Router } from "express";
2
+ import { authenticate } from "../../middleware/authenticate.js";
3
+ import { requireCsrf } from "../../middleware/csrf.js";
4
+ import { requireAllowedOrigin } from "../../middleware/origin.js";
5
+ import {
6
+ changePasswordController,
7
+ providersController,
8
+ revokeSessionController,
9
+ sessionsController,
10
+ updateProfileController,
11
+ } from "./user.controller.js";
12
+
13
+ export const userRoutes = Router();
14
+ // authenik8-core's global Redis-backed limiter runs before this router.
15
+ // codeql[js/missing-rate-limiting]
16
+ userRoutes.use(authenticate);
17
+ userRoutes.patch("/profile", requireAllowedOrigin, requireCsrf, updateProfileController);
18
+ userRoutes.put("/password", requireAllowedOrigin, requireCsrf, changePasswordController);
19
+ userRoutes.get("/sessions", sessionsController);
20
+ userRoutes.delete("/sessions/:id", requireAllowedOrigin, requireCsrf, revokeSessionController);
21
+ userRoutes.get("/providers", providersController);
@@ -0,0 +1,71 @@
1
+ import bcrypt from "bcryptjs";
2
+ import type { Request } from "express";
3
+ import { changePasswordSchema, profileSchema } from "@authenik8/contracts";
4
+ import { readRefreshCookie } from "../../auth/cookies.js";
5
+ import { prisma } from "../../config/prisma.js";
6
+ import { getAuthenik8 } from "../../auth/authenik8.js";
7
+ import { hashToken } from "../../utils/crypto.js";
8
+ import { AppError } from "../../utils/http.js";
9
+ import { presentUser } from "./user.presenter.js";
10
+
11
+ export async function updateProfile(userId: string, body: unknown) {
12
+ const input = profileSchema.parse(body);
13
+ return presentUser(await prisma.user.update({ where: { id: userId }, data: { name: input.name } }));
14
+ }
15
+
16
+ export async function changePassword(userId: string, body: unknown) {
17
+ const input = changePasswordSchema.parse(body);
18
+ const user = await prisma.user.findUniqueOrThrow({ where: { id: userId } });
19
+ if (!user.passwordHash || !(await bcrypt.compare(input.currentPassword, user.passwordHash))) {
20
+ throw new AppError(400, "PASSWORD_INCORRECT", "Current password is incorrect");
21
+ }
22
+ const now = new Date();
23
+ await prisma.$transaction([
24
+ prisma.user.update({
25
+ where: { id: userId },
26
+ data: { passwordHash: await bcrypt.hash(input.newPassword, 12), passwordUpdatedAt: now },
27
+ }),
28
+ prisma.session.updateMany({
29
+ where: { userId, revokedAt: null },
30
+ data: { revokedAt: now },
31
+ }),
32
+ ]);
33
+ await getAuthenik8().revokeAllSessions(userId);
34
+ }
35
+
36
+ export async function listSessions(userId: string, req: Request) {
37
+ const refreshToken = readRefreshCookie(req);
38
+ const currentHash = refreshToken ? hashToken(refreshToken) : "";
39
+ const sessions = await prisma.session.findMany({
40
+ where: { userId, revokedAt: null, expiresAt: { gt: new Date() } },
41
+ orderBy: { lastUsedAt: "desc" },
42
+ });
43
+ return sessions.map((session) => ({
44
+ id: session.id,
45
+ userAgent: session.userAgent,
46
+ ipAddress: session.ipAddress,
47
+ createdAt: session.createdAt.toISOString(),
48
+ lastUsedAt: session.lastUsedAt.toISOString(),
49
+ expiresAt: session.expiresAt.toISOString(),
50
+ current: session.refreshHash === currentHash,
51
+ }));
52
+ }
53
+
54
+ export async function revokeSession(userId: string, sessionId: string, req: Request) {
55
+ const session = await prisma.session.findFirst({ where: { id: sessionId, userId, revokedAt: null } });
56
+ if (!session) throw new AppError(404, "SESSION_NOT_FOUND", "Session not found");
57
+ await prisma.session.update({ where: { id: session.id }, data: { revokedAt: new Date() } });
58
+ const refreshToken = readRefreshCookie(req);
59
+ const current = refreshToken && session.refreshHash === hashToken(refreshToken);
60
+ await getAuthenik8().revokeSession(userId, session.coreSessionId);
61
+ return Boolean(current);
62
+ }
63
+
64
+ export async function listProviders(userId: string) {
65
+ const providers = await prisma.oAuthAccount.findMany({ where: { userId }, orderBy: { createdAt: "asc" } });
66
+ return providers.map((account) => ({
67
+ provider: account.provider as "google" | "github",
68
+ providerEmail: account.providerEmail,
69
+ linkedAt: account.createdAt.toISOString(),
70
+ }));
71
+ }
@@ -0,0 +1,32 @@
1
+ export const openApiDocument = {
2
+ openapi: "3.1.0",
3
+ info: { title: "Authenik8 Full-stack API", version: "1.0.0" },
4
+ servers: [{ url: "/api" }],
5
+ components: {
6
+ securitySchemes: {
7
+ bearerAuth: { type: "http", scheme: "bearer", bearerFormat: "JWT" },
8
+ csrfToken: { type: "apiKey", in: "header", name: "X-CSRF-Token" },
9
+ },
10
+ schemas: {
11
+ Error: { type: "object", properties: { error: { type: "object", properties: { code: { type: "string" }, message: { type: "string" } } }, requestId: { type: "string" } } },
12
+ },
13
+ },
14
+ paths: {
15
+ "/auth/csrf": { get: { summary: "Issue a signed CSRF token", responses: { "200": { description: "CSRF token and protected cookie" } } } },
16
+ "/auth/register": { post: { summary: "Register with email and password", security: [{ csrfToken: [] }], responses: { "201": { description: "Verification requested" }, "422": { description: "Validation error" } } } },
17
+ "/auth/login": { post: { summary: "Create a refresh session", security: [{ csrfToken: [] }], responses: { "200": { description: "Access token and user" }, "401": { description: "Invalid credentials" } } } },
18
+ "/auth/refresh": { post: { summary: "Rotate the refresh cookie", security: [{ csrfToken: [] }], responses: { "200": { description: "Rotated session" }, "401": { description: "Replay or invalid session" } } } },
19
+ "/auth/logout": { post: { summary: "Revoke the current refresh session", security: [{ csrfToken: [] }], responses: { "200": { description: "Signed out" } } } },
20
+ "/projects": {
21
+ get: { summary: "List authorized projects", security: [{ bearerAuth: [] }], responses: { "200": { description: "Project list" } } },
22
+ post: { summary: "Create an owned project", security: [{ bearerAuth: [], csrfToken: [] }], responses: { "201": { description: "Project created" } } },
23
+ },
24
+ "/projects/{id}": {
25
+ get: { summary: "Read an authorized project", security: [{ bearerAuth: [] }], parameters: [{ in: "path", name: "id", required: true, schema: { type: "string", format: "uuid" } }], responses: { "200": { description: "Project" }, "404": { description: "Not found or not authorized" } } },
26
+ patch: { summary: "Update an authorized project", security: [{ bearerAuth: [], csrfToken: [] }], responses: { "200": { description: "Project updated" } } },
27
+ delete: { summary: "Delete an authorized project", security: [{ bearerAuth: [], csrfToken: [] }], responses: { "204": { description: "Project deleted" } } },
28
+ },
29
+ "/admin/users": { get: { summary: "List users", security: [{ bearerAuth: [] }], responses: { "200": { description: "Paginated users" }, "403": { description: "Administrator required" } } } },
30
+ "/health/ready": { get: { summary: "Check database and Redis readiness", responses: { "200": { description: "Ready" } } } },
31
+ },
32
+ } as const;
@@ -0,0 +1,29 @@
1
+ import { initAuthenik8, redis } from "./auth/authenik8.js";
2
+ import { createApp } from "./app.js";
3
+ import { env } from "./config/env.js";
4
+ import { logger } from "./config/logger.js";
5
+ import { prisma } from "./config/prisma.js";
6
+
7
+ async function start() {
8
+ await initAuthenik8();
9
+ await prisma.$connect();
10
+ const server = createApp().listen(env.PORT, () => {
11
+ logger.info({ port: env.PORT }, `API running on http://localhost:${env.PORT}`);
12
+ });
13
+
14
+ const shutdown = async (signal: string) => {
15
+ logger.info({ signal }, "Shutting down");
16
+ server.close(async () => {
17
+ await Promise.all([prisma.$disconnect(), redis.quit()]);
18
+ process.exit(0);
19
+ });
20
+ setTimeout(() => process.exit(1), 10_000).unref();
21
+ };
22
+ process.once("SIGINT", () => void shutdown("SIGINT"));
23
+ process.once("SIGTERM", () => void shutdown("SIGTERM"));
24
+ }
25
+
26
+ start().catch((error) => {
27
+ logger.fatal({ err: error }, "API failed to start");
28
+ process.exit(1);
29
+ });
@@ -0,0 +1,15 @@
1
+ declare global {
2
+ namespace Express {
3
+ interface Request {
4
+ id: string;
5
+ user?: {
6
+ userId: string;
7
+ email: string;
8
+ name: string;
9
+ role: "USER" | "ADMIN";
10
+ };
11
+ }
12
+ }
13
+ }
14
+
15
+ export {};
@@ -0,0 +1,4 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+
3
+ export const randomToken = () => randomBytes(32).toString("hex");
4
+ export const hashToken = (value: string) => createHash("sha256").update(value).digest("hex");
@@ -0,0 +1,33 @@
1
+ import type { NextFunction, Request, RequestHandler, Response } from "express";
2
+ import { ZodError } from "zod";
3
+
4
+ export class AppError extends Error {
5
+ constructor(public status: number, public code: string, message: string) {
6
+ super(message);
7
+ }
8
+ }
9
+
10
+ export function asyncHandler(handler: (req: Request, res: Response, next: NextFunction) => Promise<unknown>): RequestHandler {
11
+ return (req, res, next) => void handler(req, res, next).catch(next);
12
+ }
13
+
14
+ export function errorHandler(error: unknown, req: Request, res: Response, _next: NextFunction) {
15
+ if (error instanceof ZodError) {
16
+ return res.status(422).json({
17
+ error: { code: "VALIDATION_ERROR", message: "Check the submitted fields", fields: error.flatten().fieldErrors },
18
+ requestId: req.id,
19
+ });
20
+ }
21
+ if (error instanceof AppError) {
22
+ return res.status(error.status).json({ error: { code: error.code, message: error.message }, requestId: req.id });
23
+ }
24
+ req.log?.error({ err: error }, "Unhandled request error");
25
+ return res.status(500).json({
26
+ error: { code: "INTERNAL_ERROR", message: "The request could not be completed" },
27
+ requestId: req.id,
28
+ });
29
+ }
30
+
31
+ export function notFound(req: Request, res: Response) {
32
+ return res.status(404).json({ error: { code: "NOT_FOUND", message: "Route not found" }, requestId: req.id });
33
+ }
@@ -0,0 +1,37 @@
1
+ import { createCipheriv, createDecipheriv, createHmac, randomBytes } from "node:crypto";
2
+ import { z } from "zod";
3
+
4
+ const sealedValueSchema = z.string()
5
+ .max(16_384)
6
+ .regex(/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/);
7
+
8
+ function encryptionKey(secret: string): Buffer {
9
+ return createHmac("sha256", secret).update("authenik8:sealed-value:v1").digest();
10
+ }
11
+
12
+ export function sealValue(value: string, secret: string): string {
13
+ const iv = randomBytes(12);
14
+ const cipher = createCipheriv("aes-256-gcm", encryptionKey(secret), iv);
15
+ const ciphertext = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
16
+ const tag = cipher.getAuthTag();
17
+ return [iv, tag, ciphertext].map((part) => part.toString("base64url")).join(".");
18
+ }
19
+
20
+ export function openSealedValue(value: unknown, secret: string): string | undefined {
21
+ const parsed = sealedValueSchema.safeParse(value);
22
+ if (!parsed.success) return undefined;
23
+
24
+ try {
25
+ const [ivValue, tagValue, ciphertextValue] = parsed.data.split(".");
26
+ const iv = Buffer.from(ivValue!, "base64url");
27
+ const tag = Buffer.from(tagValue!, "base64url");
28
+ const ciphertext = Buffer.from(ciphertextValue!, "base64url");
29
+ if (iv.length !== 12 || tag.length !== 16 || ciphertext.length === 0) return undefined;
30
+
31
+ const decipher = createDecipheriv("aes-256-gcm", encryptionKey(secret), iv);
32
+ decipher.setAuthTag(tag);
33
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
34
+ } catch {
35
+ return undefined;
36
+ }
37
+ }
@@ -0,0 +1,26 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { canReadProject, canWriteProject, projectListScope } from "../src/modules/projects/project.policy.js";
3
+
4
+ describe("Project ownership policy", () => {
5
+ const owner = { userId: "user-1", role: "USER" as const };
6
+ const stranger = { userId: "user-2", role: "USER" as const };
7
+ const admin = { userId: "admin-1", role: "ADMIN" as const };
8
+ const project = { ownerId: "user-1" };
9
+
10
+ it("allows the owner to read and write", () => {
11
+ expect(canReadProject(owner, project)).toBe(true);
12
+ expect(canWriteProject(owner, project)).toBe(true);
13
+ });
14
+
15
+ it("rejects another normal user", () => {
16
+ expect(canReadProject(stranger, project)).toBe(false);
17
+ expect(canWriteProject(stranger, project)).toBe(false);
18
+ expect(projectListScope(stranger)).toEqual({ ownerId: "user-2" });
19
+ });
20
+
21
+ it("allows an administrator through the explicit policy", () => {
22
+ expect(canReadProject(admin, project)).toBe(true);
23
+ expect(canWriteProject(admin, project)).toBe(true);
24
+ expect(projectListScope(admin)).toEqual({});
25
+ });
26
+ });
@@ -0,0 +1,69 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const mocks = vi.hoisted(() => ({
4
+ findUnique: vi.fn(),
5
+ create: vi.fn(),
6
+ sendVerificationEmail: vi.fn(),
7
+ hash: vi.fn(async () => "hashed-password"),
8
+ }));
9
+
10
+ vi.mock("../src/config/prisma.js", () => ({
11
+ prisma: {
12
+ user: {
13
+ findUnique: mocks.findUnique,
14
+ create: mocks.create,
15
+ },
16
+ },
17
+ }));
18
+ vi.mock("../src/config/mailer.js", () => ({
19
+ sendPasswordResetEmail: vi.fn(),
20
+ sendVerificationEmail: mocks.sendVerificationEmail,
21
+ }));
22
+ vi.mock("../src/auth/authenik8.js", () => ({
23
+ getAuthenik8: vi.fn(),
24
+ redis: { del: vi.fn(), setex: vi.fn() },
25
+ }));
26
+ vi.mock("bcryptjs", () => ({
27
+ default: { compare: vi.fn(), hash: mocks.hash },
28
+ }));
29
+
30
+ import { register } from "../src/auth/auth.service.js";
31
+
32
+ describe("registration service", () => {
33
+ beforeEach(() => {
34
+ vi.clearAllMocks();
35
+ mocks.findUnique.mockResolvedValue(null);
36
+ mocks.create.mockResolvedValue({ id: "user-1" });
37
+ mocks.sendVerificationEmail.mockResolvedValue(undefined);
38
+ });
39
+
40
+ it("creates a validated account and sends its verification message", async () => {
41
+ const result = await register({
42
+ name: "Jane Example",
43
+ email: "jane@example.com",
44
+ password: "SecurePass1",
45
+ });
46
+
47
+ expect(mocks.create).toHaveBeenCalledWith({
48
+ data: expect.objectContaining({
49
+ name: "Jane Example",
50
+ email: "jane@example.com",
51
+ passwordHash: "hashed-password",
52
+ verificationTokens: { create: expect.objectContaining({ tokenHash: expect.any(String) }) },
53
+ }),
54
+ });
55
+ expect(mocks.sendVerificationEmail).toHaveBeenCalledWith("jane@example.com", expect.any(String));
56
+ expect(result).toMatchObject({ message: "Check your inbox to continue" });
57
+ });
58
+
59
+ it("returns the same response for an existing address", async () => {
60
+ mocks.findUnique.mockResolvedValue({ id: "existing-user" });
61
+
62
+ await expect(register({
63
+ name: "Jane Example",
64
+ email: "jane@example.com",
65
+ password: "SecurePass1",
66
+ })).resolves.toEqual({ message: "Check your inbox to continue" });
67
+ expect(mocks.create).not.toHaveBeenCalled();
68
+ });
69
+ });