@simple-auth-kit/cli 1.4.3 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/package.json +1 -1
  2. package/registry/combos/express-drizzle/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
  3. package/registry/combos/express-drizzle/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +23 -7
  4. package/registry/combos/{express-prisma/shared/src → express-drizzle/shared/src/middleware}/response-envelope.middleware.ts +15 -3
  5. package/registry/combos/express-drizzle/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +32 -9
  6. package/registry/combos/express-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
  7. package/registry/combos/express-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
  8. package/registry/combos/express-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +20 -6
  9. package/registry/combos/express-drizzle/shared/src/request-context.ts +1 -1
  10. package/registry/combos/express-drizzle/shared/src/routers/auth.router.ts +395 -0
  11. package/registry/combos/express-drizzle/variants/base/src/create-auth-app.ts +52 -19
  12. package/registry/combos/express-drizzle/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
  13. package/registry/combos/express-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +28 -10
  14. package/registry/combos/express-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +239 -48
  15. package/registry/combos/express-drizzle/variants/base/src/routers/admin.router.ts +497 -0
  16. package/registry/combos/express-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +480 -109
  17. package/registry/combos/express-drizzle/variants/workspaces/src/create-auth-app.ts +69 -23
  18. package/registry/combos/express-drizzle/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
  19. package/registry/combos/express-drizzle/variants/workspaces/src/openapi-workspace.ts +147 -28
  20. package/registry/combos/express-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +30 -11
  21. package/registry/combos/express-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +319 -70
  22. package/registry/combos/express-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +127 -30
  23. package/registry/combos/express-drizzle/variants/workspaces/src/routers/admin.router.ts +534 -0
  24. package/registry/combos/express-drizzle/variants/workspaces/src/routers/workspace.router.ts +194 -0
  25. package/registry/combos/express-drizzle/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +544 -122
  26. package/registry/combos/express-prisma/shared/src/{auth-core-error.middleware.ts → middleware/auth-core-error.middleware.ts} +17 -4
  27. package/registry/combos/express-prisma/shared/src/{auth.middleware.ts → middleware/auth.middleware.ts} +19 -6
  28. package/registry/combos/{express-drizzle/shared/src → express-prisma/shared/src/middleware}/response-envelope.middleware.ts +15 -3
  29. package/registry/combos/express-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +27 -9
  30. package/registry/combos/express-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
  31. package/registry/combos/express-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
  32. package/registry/combos/express-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
  33. package/registry/combos/express-prisma/shared/src/request-context.ts +1 -1
  34. package/registry/combos/express-prisma/shared/src/routers/auth.router.ts +398 -0
  35. package/registry/combos/express-prisma/variants/base/src/create-auth-app.ts +57 -22
  36. package/registry/combos/express-prisma/variants/base/src/{authz.middleware.ts → middleware/authz.middleware.ts} +15 -7
  37. package/registry/combos/express-prisma/variants/base/src/rbac.defaults.ts +67 -19
  38. package/registry/combos/express-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  39. package/registry/combos/express-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +199 -48
  40. package/registry/combos/express-prisma/variants/base/src/routers/admin.router.ts +508 -0
  41. package/registry/combos/express-prisma/variants/base/src/seed.ts +78 -20
  42. package/registry/combos/express-prisma/variants/base/src/{auth.service.ts → services/auth.service.ts} +454 -105
  43. package/registry/combos/express-prisma/variants/workspaces/src/create-auth-app.ts +81 -26
  44. package/registry/combos/express-prisma/variants/workspaces/src/{authz.middleware.ts → middleware/authz.middleware.ts} +32 -12
  45. package/registry/combos/express-prisma/variants/workspaces/src/openapi-workspace.ts +147 -28
  46. package/registry/combos/express-prisma/variants/workspaces/src/rbac.defaults.ts +73 -21
  47. package/registry/combos/express-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  48. package/registry/combos/express-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +272 -64
  49. package/registry/combos/express-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +86 -23
  50. package/registry/combos/express-prisma/variants/workspaces/src/routers/admin.router.ts +545 -0
  51. package/registry/combos/express-prisma/variants/workspaces/src/routers/workspace.router.ts +194 -0
  52. package/registry/combos/express-prisma/variants/workspaces/src/seed.ts +109 -29
  53. package/registry/combos/express-prisma/variants/workspaces/src/{auth.service.ts → services/auth.service.ts} +518 -119
  54. package/registry/combos/nestjs-drizzle/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
  55. package/registry/combos/nestjs-drizzle/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
  56. package/registry/combos/nestjs-drizzle/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
  57. package/registry/combos/nestjs-drizzle/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
  58. package/registry/combos/{nestjs-prisma/shared/src → nestjs-drizzle/shared/src/interceptors}/response.interceptor.ts +10 -2
  59. package/registry/combos/nestjs-drizzle/shared/src/repositories/oauth.repository.ts +63 -0
  60. package/registry/combos/nestjs-drizzle/shared/src/repositories/password-reset.repository.ts +59 -0
  61. package/registry/combos/nestjs-drizzle/shared/src/{session.repository.ts → repositories/session.repository.ts} +52 -14
  62. package/registry/combos/nestjs-drizzle/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +21 -7
  63. package/registry/combos/nestjs-drizzle/shared/src/request-context.ts +1 -1
  64. package/registry/combos/nestjs-drizzle/test/prove-cycle.ts +1097 -307
  65. package/registry/combos/nestjs-drizzle/variants/base/src/auth.module.ts +44 -20
  66. package/registry/combos/nestjs-drizzle/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +228 -62
  67. package/registry/combos/nestjs-drizzle/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
  68. package/registry/combos/nestjs-drizzle/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +27 -10
  69. package/registry/combos/nestjs-drizzle/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +249 -51
  70. package/registry/combos/nestjs-drizzle/variants/base/src/{auth.service.ts → services/auth.service.ts} +499 -116
  71. package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.module.ts +48 -22
  72. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +269 -70
  73. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
  74. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
  75. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +29 -11
  76. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +323 -71
  77. package/registry/combos/nestjs-drizzle/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +131 -30
  78. package/registry/combos/nestjs-drizzle/variants/workspaces/src/services/auth.service.ts +988 -0
  79. package/registry/combos/nestjs-prisma/shared/src/{auth.controller.ts → controllers/auth.controller.ts} +6 -6
  80. package/registry/combos/nestjs-prisma/shared/src/{auth-core-error.filter.ts → filters/auth-core-error.filter.ts} +29 -5
  81. package/registry/combos/nestjs-prisma/shared/src/{ability.guard.ts → guards/ability.guard.ts} +20 -7
  82. package/registry/combos/nestjs-prisma/shared/src/{auth.guard.ts → guards/auth.guard.ts} +19 -7
  83. package/registry/combos/{nestjs-drizzle/shared/src → nestjs-prisma/shared/src/interceptors}/response.interceptor.ts +10 -2
  84. package/registry/combos/nestjs-prisma/shared/src/{oauth.repository.ts → repositories/oauth.repository.ts} +25 -8
  85. package/registry/combos/nestjs-prisma/shared/src/repositories/password-reset.repository.ts +55 -0
  86. package/registry/combos/nestjs-prisma/shared/src/{session.repository.ts → repositories/session.repository.ts} +30 -9
  87. package/registry/combos/nestjs-prisma/shared/src/{two-factor.repository.ts → repositories/two-factor.repository.ts} +11 -4
  88. package/registry/combos/nestjs-prisma/shared/src/request-context.ts +1 -1
  89. package/registry/combos/nestjs-prisma/test/prove-cycle.ts +1097 -307
  90. package/registry/combos/nestjs-prisma/variants/base/src/auth.module.ts +59 -28
  91. package/registry/combos/nestjs-prisma/variants/base/src/{admin.controller.ts → controllers/admin.controller.ts} +469 -110
  92. package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.gateway.ts → gateways/audit-log.gateway.ts} +22 -6
  93. package/registry/combos/nestjs-prisma/variants/base/src/{authz.guard.ts → guards/authz.guard.ts} +12 -5
  94. package/registry/combos/nestjs-prisma/variants/base/src/rbac.defaults.ts +98 -27
  95. package/registry/combos/nestjs-prisma/variants/base/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  96. package/registry/combos/nestjs-prisma/variants/base/src/{country.repository.ts → repositories/country.repository.ts} +98 -25
  97. package/registry/combos/nestjs-prisma/variants/base/src/{customer.repository.ts → repositories/customer.repository.ts} +106 -27
  98. package/registry/combos/nestjs-prisma/variants/base/src/{language.repository.ts → repositories/language.repository.ts} +93 -25
  99. package/registry/combos/nestjs-prisma/variants/base/src/{rbac.repository.ts → repositories/rbac.repository.ts} +229 -55
  100. package/registry/combos/nestjs-prisma/variants/base/src/seed.ts +78 -20
  101. package/registry/combos/nestjs-prisma/variants/base/src/services/auth.service.ts +1064 -0
  102. package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.module.ts +55 -27
  103. package/registry/combos/nestjs-prisma/variants/workspaces/src/{admin.controller.ts → controllers/admin.controller.ts} +544 -128
  104. package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.controller.ts → controllers/workspace.controller.ts} +93 -26
  105. package/registry/combos/nestjs-prisma/variants/workspaces/src/{authz.guard.ts → guards/authz.guard.ts} +26 -8
  106. package/registry/combos/nestjs-prisma/variants/workspaces/src/rbac.defaults.ts +104 -29
  107. package/registry/combos/nestjs-prisma/variants/workspaces/src/{audit-log.repository.ts → repositories/audit-log.repository.ts} +16 -5
  108. package/registry/combos/nestjs-prisma/variants/workspaces/src/{country.repository.ts → repositories/country.repository.ts} +127 -26
  109. package/registry/combos/nestjs-prisma/variants/workspaces/src/{customer.repository.ts → repositories/customer.repository.ts} +137 -28
  110. package/registry/combos/nestjs-prisma/variants/workspaces/src/repositories/language.repository.ts +243 -0
  111. package/registry/combos/nestjs-prisma/variants/workspaces/src/{rbac.repository.ts → repositories/rbac.repository.ts} +314 -74
  112. package/registry/combos/nestjs-prisma/variants/workspaces/src/{workspace.repository.ts → repositories/workspace.repository.ts} +94 -24
  113. package/registry/combos/nestjs-prisma/variants/workspaces/src/seed.ts +109 -29
  114. package/registry/combos/nestjs-prisma/variants/workspaces/src/services/auth.service.ts +961 -0
  115. package/registry.json +4 -4
  116. package/simple-auth-kit.ts +29 -2
  117. package/registry/combos/express-drizzle/shared/src/auth.router.ts +0 -266
  118. package/registry/combos/express-drizzle/shared/src/password-reset.repository.ts +0 -35
  119. package/registry/combos/express-drizzle/variants/base/src/admin.router.ts +0 -307
  120. package/registry/combos/express-drizzle/variants/workspaces/src/admin.router.ts +0 -323
  121. package/registry/combos/express-drizzle/variants/workspaces/src/workspace.router.ts +0 -123
  122. package/registry/combos/express-prisma/shared/src/auth.router.ts +0 -269
  123. package/registry/combos/express-prisma/shared/src/password-reset.repository.ts +0 -29
  124. package/registry/combos/express-prisma/variants/base/src/admin.router.ts +0 -314
  125. package/registry/combos/express-prisma/variants/workspaces/src/admin.router.ts +0 -330
  126. package/registry/combos/express-prisma/variants/workspaces/src/workspace.router.ts +0 -123
  127. package/registry/combos/nestjs-drizzle/shared/src/oauth.repository.ts +0 -36
  128. package/registry/combos/nestjs-drizzle/shared/src/password-reset.repository.ts +0 -29
  129. package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.service.ts +0 -553
  130. package/registry/combos/nestjs-prisma/shared/src/password-reset.repository.ts +0 -29
  131. package/registry/combos/nestjs-prisma/variants/base/src/auth.service.ts +0 -618
  132. package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.service.ts +0 -509
  133. package/registry/combos/nestjs-prisma/variants/workspaces/src/language.repository.ts +0 -143
package/registry.json CHANGED
@@ -39,7 +39,7 @@
39
39
  "Add \"@/prisma/*\": [\"./generated/prisma/*\"] to your tsconfig.json's compilerOptions.paths — the copied source imports the generated Prisma client via this alias, since prisma.config.ts/prisma/ (and the client it generates) install at the project root regardless of where --path put everything else",
40
40
  "Run: npx prisma generate",
41
41
  "Run: npx prisma migrate deploy",
42
- "Seed RBAC: add \"seed\": \"tsx src/lib/auth/src/seed.ts\" to your package.json scripts (adjust the path if you installed elsewhere), then run: npm run seed — it is idempotent, and seeds the permission catalog and the default admin/member roles. Nothing is authorized until it has run at least once.",
42
+ "Seed RBAC: add \"seed\": \"tsx src/lib/auth/seed.ts\" to your package.json scripts (adjust the path if you installed elsewhere), then run: npm run seed — it is idempotent, and seeds the permission catalog and the default admin/member roles. Nothing is authorized until it has run at least once.",
43
43
  "Optional: set SEED_ADMIN_EMAIL and SEED_ADMIN_PASSWORD before seeding to also create an initial admin user. Without them the seeder skips the admin rather than inventing a password.",
44
44
  "Import AuthModule.forRoot({...}) into your root app module",
45
45
  "Optional: wire SwaggerModule.setup('docs', app, document) in your main.ts to serve the OpenAPI spec this combo's controller is already decorated for — see examples/nestjs-prisma-app/src/main.ts for a working example"
@@ -74,7 +74,7 @@
74
74
  "Set DATABASE_URL in .env (see .env.example)",
75
75
  "Set AUTH_JWT_SECRET in .env — a base64-encoded 256-bit (or longer) secret, e.g.: openssl rand -base64 32. The app refuses to start without it; there is no default and nothing is generated for you. Changing it invalidates every outstanding token.",
76
76
  "Run: npx drizzle-kit migrate — drizzle.config.ts installs at the project root, alongside drizzle/, so no cd is needed",
77
- "Seed RBAC: add \"seed\": \"tsx src/lib/auth/src/seed.ts\" to your package.json scripts (adjust the path if you installed elsewhere), then run: npm run seed — it is idempotent, and seeds the permission catalog and the default admin/member roles. Nothing is authorized until it has run at least once.",
77
+ "Seed RBAC: add \"seed\": \"tsx src/lib/auth/seed.ts\" to your package.json scripts (adjust the path if you installed elsewhere), then run: npm run seed — it is idempotent, and seeds the permission catalog and the default admin/member roles. Nothing is authorized until it has run at least once.",
78
78
  "Optional: set SEED_ADMIN_EMAIL and SEED_ADMIN_PASSWORD before seeding to also create an initial admin user. Without them the seeder skips the admin rather than inventing a password.",
79
79
  "Import AuthModule.forRoot({...}) into your root app module",
80
80
  "Optional: wire SwaggerModule.setup('docs', app, document) in your main.ts to serve the OpenAPI spec this combo's controller is already decorated for — see examples/nestjs-drizzle-app/src/main.ts for a working example"
@@ -106,7 +106,7 @@
106
106
  "Add \"@/prisma/*\": [\"./generated/prisma/*\"] to your tsconfig.json's compilerOptions.paths — the copied source imports the generated Prisma client via this alias, since prisma.config.ts/prisma/ (and the client it generates) install at the project root regardless of where --path put everything else",
107
107
  "Run: npx prisma generate",
108
108
  "Run: npx prisma migrate deploy",
109
- "Seed RBAC: add \"seed\": \"tsx src/lib/auth/src/seed.ts\" to your package.json scripts (adjust the path if you installed elsewhere), then run: npm run seed — it is idempotent, and seeds the permission catalog and the default admin/member roles. Nothing is authorized until it has run at least once.",
109
+ "Seed RBAC: add \"seed\": \"tsx src/lib/auth/seed.ts\" to your package.json scripts (adjust the path if you installed elsewhere), then run: npm run seed — it is idempotent, and seeds the permission catalog and the default admin/member roles. Nothing is authorized until it has run at least once.",
110
110
  "Optional: set SEED_ADMIN_EMAIL and SEED_ADMIN_PASSWORD before seeding to also create an initial admin user. Without them the seeder skips the admin rather than inventing a password.",
111
111
  "Call createAuthApp({...}) and mount or listen on the Express app it returns",
112
112
  "The OpenAPI spec this combo hand-authors is already served at /docs and /docs-json — see examples/express-prisma-app/src/main.ts for a working example"
@@ -136,7 +136,7 @@
136
136
  "Set DATABASE_URL in .env (see .env.example)",
137
137
  "Set AUTH_JWT_SECRET in .env — a base64-encoded 256-bit (or longer) secret, e.g.: openssl rand -base64 32. The app refuses to start without it; there is no default and nothing is generated for you. Changing it invalidates every outstanding token.",
138
138
  "Run: npx drizzle-kit migrate — drizzle.config.ts installs at the project root, alongside drizzle/, so no cd is needed",
139
- "Seed RBAC: add \"seed\": \"tsx src/lib/auth/src/seed.ts\" to your package.json scripts (adjust the path if you installed elsewhere), then run: npm run seed — it is idempotent, and seeds the permission catalog and the default admin/member roles. Nothing is authorized until it has run at least once.",
139
+ "Seed RBAC: add \"seed\": \"tsx src/lib/auth/seed.ts\" to your package.json scripts (adjust the path if you installed elsewhere), then run: npm run seed — it is idempotent, and seeds the permission catalog and the default admin/member roles. Nothing is authorized until it has run at least once.",
140
140
  "Optional: set SEED_ADMIN_EMAIL and SEED_ADMIN_PASSWORD before seeding to also create an initial admin user. Without them the seeder skips the admin rather than inventing a password.",
141
141
  "Call createAuthApp({...}) and mount or listen on the Express app it returns",
142
142
  "The OpenAPI spec this combo hand-authors is already served at /docs and /docs-json — see examples/express-drizzle-app/src/main.ts for a working example"
@@ -588,17 +588,44 @@ async function runMergeCopy(
588
588
  plan.destRoot,
589
589
  result,
590
590
  );
591
+
592
+ // shared/src/ and variants/<variant>/src/ are the registry's own internal source layout
593
+ // (parallel to core/, prisma.config.ts, etc.) — copied flat into destRoot rather than
594
+ // preserving that extra "src" level, so a consumer who installed at the default
595
+ // "src/lib/auth" doesn't end up with a redundant "src/lib/auth/src/...". Order matters:
596
+ // shared fully applied (its own src/, then everything else in shared/), then the variant
597
+ // overlaid the same way, so a variant file with the same name still wins.
598
+ const skipShared = new Set([...plan.skipFromShared, "src"]);
599
+ const skipVariant = new Set([...plan.skipFromVariant, "src"]);
600
+ if (await pathExists(join(plan.sharedDir, "src"))) {
601
+ await copyDir(
602
+ join(plan.sharedDir, "src"),
603
+ plan.destRoot,
604
+ { ...copyOpts, neverCopy: plan.skipFromShared },
605
+ plan.destRoot,
606
+ result,
607
+ );
608
+ }
591
609
  await copyDir(
592
610
  plan.sharedDir,
593
611
  plan.destRoot,
594
- { ...copyOpts, neverCopy: plan.skipFromShared },
612
+ { ...copyOpts, neverCopy: skipShared },
595
613
  plan.destRoot,
596
614
  result,
597
615
  );
616
+ if (await pathExists(join(plan.variantDir, "src"))) {
617
+ await copyDir(
618
+ join(plan.variantDir, "src"),
619
+ plan.destRoot,
620
+ { ...copyOpts, neverCopy: plan.skipFromVariant },
621
+ plan.destRoot,
622
+ result,
623
+ );
624
+ }
598
625
  await copyDir(
599
626
  plan.variantDir,
600
627
  plan.destRoot,
601
- { ...copyOpts, neverCopy: plan.skipFromVariant },
628
+ { ...copyOpts, neverCopy: skipVariant },
602
629
  plan.destRoot,
603
630
  result,
604
631
  );
@@ -1,266 +0,0 @@
1
- import type { NextFunction, Request, RequestHandler, Response } from "express";
2
- import type { AuthConfig } from "./auth.config.js";
3
- import { AuthService } from "./auth.service.js";
4
- import { HttpError } from "./http-error.js";
5
- import { authenticated, createTieredRouter, publicRoute, type TierMiddleware } from "./route-tiers.js";
6
- import "./request-context.js";
7
-
8
- function requireString(value: unknown, field: string): string {
9
- if (typeof value !== "string" || value.length === 0) throw new HttpError(400, `${field} is required`);
10
- return value;
11
- }
12
-
13
- const optionalString = (value: unknown): string | undefined => (typeof value === "string" ? value : undefined);
14
-
15
- export interface AuthRouterDeps extends TierMiddleware {
16
- auth: AuthService;
17
- config: AuthConfig;
18
- }
19
-
20
- /**
21
- * Identity endpoints — everything that is about *a user*, never about a group of them.
22
- * Administration lives in admin.router.ts.
23
- *
24
- * Replaces the reference combo's `AuthController` (a Nest `@Controller`). Same endpoints, same
25
- * validation, same status codes — just an Express `Router()` instead of a decorated class, and
26
- * middleware passed in where the controller lists `@UseGuards(...)`. Every handler is wrapped in
27
- * try/catch + `next(err)` so a thrown `HttpError`/`AuthCoreError` reaches the error-handling
28
- * middleware mounted in create-auth-app.ts instead of leaking a bare 500.
29
- */
30
- export function createAuthRouter(deps: AuthRouterDeps): RequestHandler {
31
- const { auth, config, authentication, authorization } = deps;
32
- const router = createTieredRouter({ authentication, authorization });
33
-
34
- router.route("post", "/signup", publicRoute(), async (req: Request, res: Response, next: NextFunction) => {
35
- try {
36
- const body = req.body as Record<string, unknown>;
37
- const tokens = await auth.signup({
38
- email: requireString(body.email, "email"),
39
- password: requireString(body.password, "password"),
40
- firstName: optionalString(body.firstName),
41
- lastName: optionalString(body.lastName),
42
- displayName: optionalString(body.displayName),
43
- phone: optionalString(body.phone),
44
- username: optionalString(body.username),
45
- userAgent: req.headers["user-agent"],
46
- ip: req.ip,
47
- });
48
- res.status(201).json(tokens);
49
- } catch (err) {
50
- next(err);
51
- }
52
- });
53
-
54
- router.route("post", "/login", publicRoute(), async (req: Request, res: Response, next: NextFunction) => {
55
- try {
56
- const body = req.body as Record<string, unknown>;
57
- const result = await auth.login({
58
- identifier: requireString(body.identifier, "identifier"),
59
- password: requireString(body.password, "password"),
60
- userAgent: req.headers["user-agent"],
61
- ip: req.ip,
62
- });
63
- res.status(200).json(result);
64
- } catch (err) {
65
- next(err);
66
- }
67
- });
68
-
69
- router.route("post", "/login/2fa", publicRoute(), async (req: Request, res: Response, next: NextFunction) => {
70
- try {
71
- const body = req.body as Record<string, unknown>;
72
- const tokens = await auth.loginTwoFactor({
73
- challengeToken: requireString(body.challengeToken, "challengeToken"),
74
- code: requireString(body.code, "code"),
75
- userAgent: req.headers["user-agent"],
76
- ip: req.ip,
77
- });
78
- res.status(200).json(tokens);
79
- } catch (err) {
80
- next(err);
81
- }
82
- });
83
-
84
- router.route("post", "/refresh", publicRoute(), async (req: Request, res: Response, next: NextFunction) => {
85
- try {
86
- const body = req.body as Record<string, unknown>;
87
- const tokens = await auth.refresh(requireString(body.refreshToken, "refreshToken"));
88
- res.status(200).json(tokens);
89
- } catch (err) {
90
- next(err);
91
- }
92
- });
93
-
94
- // `roles`/`permissions` are whatever applies to this request — see authz.middleware.ts for how
95
- // they are resolved. A request that names no authorization scope gets empty arrays.
96
- //
97
- // `permissions` is the caller's whole authorization: `defineAbilitiesFor(permissions)` from
98
- // ability.ts rebuilds, in the client, the identical CASL ability the server's own route check
99
- // just answered with, so the UI cannot disagree with what the API will allow. The slugs go on
100
- // the wire rather than a serialised ability, so the payload is something a human can read in a
101
- // log — and it is the same `req.authz` the ability was built from, not a re-resolution.
102
- router.route("get", "/me", authenticated(), async (req: Request, res: Response, next: NextFunction) => {
103
- try {
104
- const { sub, sessionId } = req.auth!;
105
- const [{ twoFactorEnabled }, profile] = await Promise.all([auth.getTwoFactorStatus(sub), auth.getProfile(sub)]);
106
- res.status(200).json({
107
- sub,
108
- sessionId,
109
- roles: req.authz?.roles ?? [],
110
- permissions: req.authz?.permissions ?? [],
111
- twoFactorEnabled,
112
- email: profile.email,
113
- firstName: profile.firstName,
114
- lastName: profile.lastName,
115
- displayName: profile.displayName,
116
- phone: profile.phone,
117
- username: profile.username,
118
- photo: profile.photo,
119
- });
120
- } catch (err) {
121
- next(err);
122
- }
123
- });
124
-
125
- router.route("get", "/sessions", authenticated(), async (req: Request, res: Response, next: NextFunction) => {
126
- try {
127
- const sessions = await auth.listActiveSessions(req.auth!.sub);
128
- res.status(200).json(sessions);
129
- } catch (err) {
130
- next(err);
131
- }
132
- });
133
-
134
- router.route("post", "/logout", authenticated(), async (req: Request, res: Response, next: NextFunction) => {
135
- try {
136
- const { sessionId, jti, sub } = req.auth!;
137
- // Recorded so the row can later be told apart from a session an administrator ended —
138
- // see `sessions.revoked_by`.
139
- await auth.logout(sessionId, jti, config.accessTokenTtlSeconds, { userId: sub, ip: req.ip });
140
- res.status(200).json({ ok: true });
141
- } catch (err) {
142
- next(err);
143
- }
144
- });
145
-
146
- router.route("post", "/logout-all", authenticated(), async (req: Request, res: Response, next: NextFunction) => {
147
- try {
148
- await auth.logoutAll(req.auth!.sub, { userId: req.auth!.sub, ip: req.ip });
149
- res.status(200).json({ ok: true });
150
- } catch (err) {
151
- next(err);
152
- }
153
- });
154
-
155
- router.route("post", "/logout-others", authenticated(), async (req: Request, res: Response, next: NextFunction) => {
156
- try {
157
- await auth.logoutOthers(req.auth!.sub, req.auth!.sessionId, { userId: req.auth!.sub, ip: req.ip });
158
- res.status(200).json({ ok: true });
159
- } catch (err) {
160
- next(err);
161
- }
162
- });
163
-
164
- router.route("post", "/password/change", authenticated(), async (req: Request, res: Response, next: NextFunction) => {
165
- try {
166
- const body = req.body as Record<string, unknown>;
167
- await auth.changePassword(req.auth!.sub, req.auth!.sessionId, {
168
- currentPassword: requireString(body.currentPassword, "currentPassword"),
169
- newPassword: requireString(body.newPassword, "newPassword"),
170
- });
171
- res.status(200).json({ ok: true });
172
- } catch (err) {
173
- next(err);
174
- }
175
- });
176
-
177
- router.route("patch", "/me", authenticated(), async (req: Request, res: Response, next: NextFunction) => {
178
- try {
179
- const body = req.body as Record<string, unknown>;
180
- const profile = await auth.updateProfile(req.auth!.sub, {
181
- firstName: body.firstName === null ? null : optionalString(body.firstName),
182
- lastName: body.lastName === null ? null : optionalString(body.lastName),
183
- displayName: body.displayName === null ? null : optionalString(body.displayName),
184
- phone: body.phone === null ? null : optionalString(body.phone),
185
- username: body.username === null ? null : optionalString(body.username),
186
- photo: body.photo === null ? null : optionalString(body.photo),
187
- });
188
- res.status(200).json(profile);
189
- } catch (err) {
190
- next(err);
191
- }
192
- });
193
-
194
- router.route("post", "/2fa/enroll", authenticated(), async (req: Request, res: Response, next: NextFunction) => {
195
- try {
196
- const result = await auth.enrollTwoFactor(req.auth!.sub);
197
- res.status(200).json(result);
198
- } catch (err) {
199
- next(err);
200
- }
201
- });
202
-
203
- router.route("post", "/2fa/confirm", authenticated(), async (req: Request, res: Response, next: NextFunction) => {
204
- try {
205
- const body = req.body as Record<string, unknown>;
206
- const result = await auth.confirmTwoFactor(req.auth!.sub, requireString(body.code, "code"));
207
- res.status(200).json(result);
208
- } catch (err) {
209
- next(err);
210
- }
211
- });
212
-
213
- router.route("post", "/2fa/disable", authenticated(), async (req: Request, res: Response, next: NextFunction) => {
214
- try {
215
- const body = req.body as Record<string, unknown>;
216
- await auth.disableTwoFactor(req.auth!.sub, requireString(body.code, "code"));
217
- res.status(200).json({ ok: true });
218
- } catch (err) {
219
- next(err);
220
- }
221
- });
222
-
223
- router.route("post", "/password/forgot", publicRoute(), async (req: Request, res: Response, next: NextFunction) => {
224
- try {
225
- const body = req.body as Record<string, unknown>;
226
- await auth.requestPasswordReset(requireString(body.email, "email"));
227
- res.status(200).json({ ok: true }); // always 200 — never reveal whether the email exists
228
- } catch (err) {
229
- next(err);
230
- }
231
- });
232
-
233
- router.route("post", "/password/reset", publicRoute(), async (req: Request, res: Response, next: NextFunction) => {
234
- try {
235
- const body = req.body as Record<string, unknown>;
236
- await auth.resetPassword(requireString(body.token, "token"), requireString(body.newPassword, "newPassword"));
237
- res.status(200).json({ ok: true });
238
- } catch (err) {
239
- next(err);
240
- }
241
- });
242
-
243
- router.route("get", "/oauth/:provider/start", publicRoute(), async (req: Request, res: Response, next: NextFunction) => {
244
- try {
245
- const result = await auth.startOAuth(requireString(req.params.provider, "provider"));
246
- res.status(200).json(result);
247
- } catch (err) {
248
- next(err);
249
- }
250
- });
251
-
252
- router.route("get", "/oauth/:provider/callback", publicRoute(), async (req: Request, res: Response, next: NextFunction) => {
253
- try {
254
- const tokens = await auth.completeOAuthCallback(
255
- requireString(req.params.provider, "provider"),
256
- requireString(req.query.code, "code"),
257
- requireString(req.query.state, "state"),
258
- );
259
- res.status(200).json(tokens);
260
- } catch (err) {
261
- next(err);
262
- }
263
- });
264
-
265
- return router.handler;
266
- }
@@ -1,35 +0,0 @@
1
- import { eq } from "drizzle-orm";
2
- import type { NodePgDatabase } from "drizzle-orm/node-postgres";
3
- import type { PasswordResetStoreDeps } from "@/lib/auth/core/password-reset.js";
4
- import * as schema from "./schema.js";
5
- import { toId } from "./id.helper.js";
6
-
7
- /**
8
- * Plain class, no decorators — Drizzle-specific implementation of `PasswordResetStoreDeps`
9
- * (registry/core/password-reset.js), mirroring the reference combo's PasswordResetRepository.
10
- */
11
- export class PasswordResetRepository implements PasswordResetStoreDeps {
12
- constructor(private readonly db: NodePgDatabase<typeof schema>) {}
13
-
14
- async saveResetToken(input: { userId: string; tokenHash: string; expiresAt: string }): Promise<void> {
15
- await this.db.insert(schema.passwordResetTokens).values({
16
- userId: toId(input.userId),
17
- tokenHash: input.tokenHash,
18
- expiresAt: new Date(input.expiresAt),
19
- });
20
- }
21
-
22
- async findValidResetToken(tokenHash: string): Promise<{ userId: string; expiresAt: string; consumedAt: string | null } | null> {
23
- const [row] = await this.db.select().from(schema.passwordResetTokens).where(eq(schema.passwordResetTokens.tokenHash, tokenHash)).limit(1);
24
- if (!row) return null;
25
- return { userId: row.userId.toString(), expiresAt: row.expiresAt.toISOString(), consumedAt: row.consumedAt?.toISOString() ?? null };
26
- }
27
-
28
- async consumeResetToken(tokenHash: string): Promise<void> {
29
- await this.db.update(schema.passwordResetTokens).set({ consumedAt: new Date() }).where(eq(schema.passwordResetTokens.tokenHash, tokenHash));
30
- }
31
-
32
- async setPasswordHash(userId: string, passwordHash: string): Promise<void> {
33
- await this.db.update(schema.users).set({ passwordHash }).where(eq(schema.users.id, toId(userId)));
34
- }
35
- }