@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.
- package/README.md +23 -5
- package/capability.json +27 -5
- package/dist/auth-middleware.d.ts +1 -0
- package/dist/auth-middleware.d.ts.map +1 -1
- package/dist/auth-middleware.js +66 -2
- package/dist/auth-middleware.js.map +1 -1
- package/dist/auth-service.d.ts +8 -0
- package/dist/auth-service.d.ts.map +1 -1
- package/dist/auth-service.js +161 -45
- package/dist/auth-service.js.map +1 -1
- package/dist/credential-migration.d.ts +76 -0
- package/dist/credential-migration.d.ts.map +1 -0
- package/dist/credential-migration.js +126 -0
- package/dist/credential-migration.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/info-schema-tags.d.ts +21 -0
- package/dist/info-schema-tags.d.ts.map +1 -0
- package/dist/info-schema-tags.js +24 -0
- package/dist/info-schema-tags.js.map +1 -0
- package/dist/team-invitation-service.d.ts.map +1 -1
- package/dist/team-invitation-service.js +42 -8
- package/dist/team-invitation-service.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/scaffold/reference/user-auth.ref.md +49 -7
- package/scaffold/src/controllers/auth.ts +12 -16
- package/scaffold/src/middleware/auth.ts +17 -8
- package/scaffold/src/scripts/migrate-credentials.ts +69 -0
- package/scaffold/src/types/express.ts +77 -0
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 } =
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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 {
|
|
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 {};
|