@boopkit/cap-users 0.5.0 → 0.7.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.
@@ -1,15 +1,11 @@
1
- import type { Response } from 'express';
1
+ import type { Request, Response } from 'express';
2
2
  import type { AuthService } from '@boopkit/cap-users';
3
3
  import { AuthValidationError } from '@boopkit/cap-users';
4
-
5
- /** Extended Express Request with auth fields set by resolveVisitor middleware. */
6
- interface AuthRequest {
7
- body: Record<string, any>;
8
- query: Record<string, any>;
9
- cookies?: Record<string, string>;
10
- user?: { id: number; [key: string]: any };
11
- sessionToken?: string;
12
- }
4
+ // Side-effect import: brings in the Express request augmentation that types
5
+ // req.user / req.sessionToken / req.visitorToken / req.authStrategy. A generated
6
+ // project also picks these up automatically via its tsconfig `include`, so this
7
+ // line is only needed to type-check the controller in isolation.
8
+ import '../types/express';
13
9
 
14
10
  const ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1000;
15
11
  const COOKIE_NAME = 'auth_id';
@@ -97,7 +93,7 @@ export function createAuthController(deps: AuthControllerDeps) {
97
93
 
98
94
  // ---- Login ----
99
95
 
100
- async function showLogin(req: AuthRequest, res: Response): Promise<void> {
96
+ async function showLogin(req: Request, res: Response): Promise<void> {
101
97
  // Already authenticated? Redirect away.
102
98
  if (res.locals.auth?.trustTier === 'authenticated' || res.locals.auth?.trustTier === 'verified') {
103
99
  const next = req.query.next as string | undefined;
@@ -118,7 +114,7 @@ export function createAuthController(deps: AuthControllerDeps) {
118
114
  });
119
115
  }
120
116
 
121
- async function handleLogin(req: AuthRequest, res: Response): Promise<void> {
117
+ async function handleLogin(req: Request, res: Response): Promise<void> {
122
118
  const { email, password } = req.body;
123
119
  const next = req.body.next || req.query.next as string | undefined;
124
120
  const currentSessionToken = req.cookies?.[COOKIE_NAME] as string | undefined;
@@ -149,7 +145,7 @@ export function createAuthController(deps: AuthControllerDeps) {
149
145
 
150
146
  // ---- Signup ----
151
147
 
152
- async function showSignup(req: AuthRequest, res: Response): Promise<void> {
148
+ async function showSignup(req: Request, res: Response): Promise<void> {
153
149
  // Already authenticated? Redirect away.
154
150
  if (res.locals.auth?.trustTier === 'authenticated' || res.locals.auth?.trustTier === 'verified') {
155
151
  const next = req.query.next as string | undefined;
@@ -166,7 +162,7 @@ export function createAuthController(deps: AuthControllerDeps) {
166
162
  });
167
163
  }
168
164
 
169
- async function handleSignup(req: AuthRequest, res: Response): Promise<void> {
165
+ async function handleSignup(req: Request, res: Response): Promise<void> {
170
166
  const { email, password, passwordConfirmation } = req.body;
171
167
  const next = req.body.next || req.query.next as string | undefined;
172
168
 
@@ -209,14 +205,14 @@ export function createAuthController(deps: AuthControllerDeps) {
209
205
 
210
206
  // ---- Logout ----
211
207
 
212
- async function handleLogout(req: AuthRequest, res: Response): Promise<void> {
208
+ async function handleLogout(req: Request, res: Response): Promise<void> {
213
209
  if (req.user && req.sessionToken) {
214
210
  await authService.logout(req.user.id, req.sessionToken);
215
211
  }
216
212
  res.redirect('/');
217
213
  }
218
214
 
219
- async function handleForgetDevice(req: AuthRequest, res: Response): Promise<void> {
215
+ async function handleForgetDevice(req: Request, res: Response): Promise<void> {
220
216
  if (req.user && req.sessionToken) {
221
217
  await authService.forgetDevice(req.user.id, req.sessionToken);
222
218
  }
@@ -6,15 +6,24 @@ import createAuthRouter from '../routes/auth';
6
6
 
7
7
  const { authService, identityService, userService } = UserService;
8
8
 
9
- const { resolveVisitor, resolveBearer, getOrCreateUser, addAuthContext, requireAuth } = createAuthMiddleware(
10
- { authService, identityService, userService },
11
- {
12
- cookieName: 'auth_id',
13
- loginPath: '/login',
14
- },
15
- );
9
+ const { resolveVisitor, resolveBearer, getOrCreateUser, addAuthContext, requireAuth, requireApiKey } =
10
+ createAuthMiddleware(
11
+ { authService, identityService, userService },
12
+ {
13
+ cookieName: 'auth_id',
14
+ loginPath: '/login',
15
+ },
16
+ );
16
17
 
17
18
  /** Pre-built auth routes (signup, login, logout). Ready to mount with app.use(). */
18
19
  const authRoutes = createAuthRouter({ authService });
19
20
 
20
- export { resolveVisitor, resolveBearer, getOrCreateUser, addAuthContext, requireAuth, authRoutes };
21
+ export {
22
+ resolveVisitor,
23
+ resolveBearer,
24
+ getOrCreateUser,
25
+ addAuthContext,
26
+ requireAuth,
27
+ requireApiKey,
28
+ authRoutes,
29
+ };
@@ -0,0 +1,69 @@
1
+ // @boopkit/cap-users template — Rehash cleartext credentials stored in property
2
+ // tables (boop-j8xc credential-hashing epic).
3
+ //
4
+ // Some property payloads once stored a raw, redeemable bearer credential beside
5
+ // its correctly-hashed twin: session tokens and API keys in the `authorization`
6
+ // user-property (and, once cap-users ships it, invitation tokens in the
7
+ // `team_invitations` account-property). The upgraded services store the hash and
8
+ // read tolerantly, but existing rows keep the raw value until they are rewritten.
9
+ // This script rehashes them IN PLACE, so no user is logged out and no pending
10
+ // invitation is voided.
11
+ //
12
+ // Usage:
13
+ // npm run credentials:migrate # REPORT ONLY — counts what would change, writes nothing
14
+ // npm run credentials:migrate -- --run # apply the rehash
15
+ //
16
+ // ⚠️ ONE-WAY, NO ROLLBACK. Hashing is irreversible — once a row is rewritten the
17
+ // raw value is gone for good. TAKE A DATABASE BACKUP before running --run.
18
+ //
19
+ // Safe to re-run: idempotency is decided by value shape (a SHA-256 digest is left
20
+ // untouched), so a value is never double-hashed. Provider access/refresh tokens
21
+ // in the `authentication` property are never touched — they are replayed to
22
+ // Google/Microsoft and must stay raw.
23
+
24
+ import { migrateCredentialStore, authorizationStore, teamInvitationsStore, type CredentialStore, type MigrationDelegate } from '@boopkit/cap-users';
25
+ import db from '../db';
26
+
27
+ const run = process.argv.includes('--run');
28
+
29
+ // The stores this deployment migrates — the same runner drives every one:
30
+ // - `authorization` user-property: raw session tokens and API keys (boop-j8xc.2)
31
+ // - `team_invitations` account-property: raw invitation tokens (boop-j8xc.3)
32
+ const targets: { model: MigrationDelegate; store: CredentialStore }[] = [
33
+ { model: db.userProperty as unknown as MigrationDelegate, store: authorizationStore },
34
+ { model: db.accountProperty as unknown as MigrationDelegate, store: teamInvitationsStore },
35
+ ];
36
+
37
+ async function main() {
38
+ if (!run) {
39
+ console.log('=== REPORT ONLY (no writes) — pass --run to apply ===\n');
40
+ } else {
41
+ console.log('=== APPLYING migration (--run) ===\n');
42
+ }
43
+
44
+ let totalPending = 0;
45
+ for (const { model, store } of targets) {
46
+ const report = await migrateCredentialStore(model, store, { run });
47
+ totalPending += report.needsMigration;
48
+ const action = run ? `rehashed ${report.migrated}` : `${report.needsMigration} would be rehashed`;
49
+ console.log(
50
+ `${store.label}: scanned ${report.scanned}, ${action} ` +
51
+ `(${report.scanned - report.needsMigration} already migrated)`,
52
+ );
53
+ }
54
+
55
+ if (!run && totalPending > 0) {
56
+ console.log(`\n${totalPending} row(s) hold a raw credential. Back up your database, then re-run with --run.`);
57
+ } else if (run) {
58
+ console.log('\nDone. All targeted rows are hashed and tagged.');
59
+ } else {
60
+ console.log('\nNothing to migrate — every targeted row is already hashed.');
61
+ }
62
+ }
63
+
64
+ main()
65
+ .catch((err) => {
66
+ console.error('Migration failed:', err);
67
+ process.exitCode = 1;
68
+ })
69
+ .finally(() => db.$disconnect());
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Express request augmentation for the auth principal (cap-users).
3
+ *
4
+ * cap-users' middleware writes four fields onto the Express request —
5
+ * `req.user`, `req.sessionToken`, `req.visitorToken` and `req.authStrategy` —
6
+ * but the library sets them through `(req as any)` casts and declares none of
7
+ * them to TypeScript. Without this file every consumer has to invent its own
8
+ * typing: the scaffolded auth controller used to hand-roll an `AuthRequest`
9
+ * interface that restated `body`, `query` and `cookies` purely to stand in for
10
+ * `Request`, and typed `user` as `{ id: number; [key: string]: any }`, losing
11
+ * the real `User` shape. Declaring the fields once here types every consumer —
12
+ * this controller, your own route handlers, anything that reads `req.user`.
13
+ *
14
+ * This file is a global ambient augmentation. In a generated project your
15
+ * tsconfig's `include` already compiles everything under `src/`, so these
16
+ * declarations apply project-wide with no import needed; the side-effect
17
+ * `import './types/express'` in the auth controller is only there so the
18
+ * controller still type-checks when compiled outside a full project (for
19
+ * example inside boop's own test suite).
20
+ *
21
+ * WHY THE USER SHAPE HANGS OFF `Express.User`, NOT `Request.user` DIRECTLY.
22
+ * `@types/passport` — pulled in the moment you add oauth-client — augments the
23
+ * global `Express` namespace with an empty `interface User {}` and types
24
+ * `Request['user']` as that `Express.User`. If cap-users declared
25
+ * `Request.user` with a *different* type (say `User<BaseUserInfo>`), the two
26
+ * declarations would merge and collide: TypeScript reports TS2717, "subsequent
27
+ * property declarations must have the same type", and every auth handler stops
28
+ * matching `RequestHandler`. Extending `Express.User` instead means both
29
+ * capabilities describe `Request.user` with the *same* type (`Express.User`),
30
+ * so their declarations merge cleanly and passport's empty interface simply
31
+ * picks up the real `User` members. This is the shape that stays correct
32
+ * whether or not passport is present. (See boop-lrse for the failure this
33
+ * avoids.)
34
+ */
35
+ import type { BaseUserInfo, User } from '@boopkit/cap-users';
36
+
37
+ declare global {
38
+ namespace Express {
39
+ /**
40
+ * The authenticated principal's shape. Extending here rather than typing
41
+ * `Request.user` directly is what lets cap-users and `@types/passport`
42
+ * agree on a single `Request.user` type instead of colliding.
43
+ */
44
+ interface User extends UserPrincipal {}
45
+
46
+ interface Request {
47
+ /** Set by resolveVisitor (session cookie) or resolveBearer (API key). */
48
+ user?: User;
49
+ /**
50
+ * The identity_key for this request's credential — the session-cookie
51
+ * token on the browser path, the raw API key on the bearer path.
52
+ * requireAuth hands it to authService.isAuthorized.
53
+ */
54
+ sessionToken?: string;
55
+ /** Anonymous visitor tracking token, from the visitor_id cookie. */
56
+ visitorToken?: string;
57
+ /**
58
+ * Which door this request's principal came through, set only by the
59
+ * resolver that actually resolved it. Absent when req.user arrived from
60
+ * the session cookie via resolveVisitor. A gate that must accept only one
61
+ * kind of credential (requireApiKey) reads this — req.user alone cannot
62
+ * tell a key principal from a cookie one.
63
+ */
64
+ authStrategy?: 'api-key';
65
+ }
66
+ }
67
+ }
68
+
69
+ /**
70
+ * The concrete user shape, kept out of the `Express` namespace so the
71
+ * `interface User extends …` line above reads cleanly. It is exactly the
72
+ * `User<BaseUserInfo>` the services return, so `req.user` carries the real
73
+ * entity (`id`, `publicId`, `info`, timestamps) rather than a stand-in.
74
+ */
75
+ type UserPrincipal = User<BaseUserInfo>;
76
+
77
+ export {};