@endora-commerce/mod-auth 0.0.0-stage → 0.100.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/LICENSE +21 -0
- package/README.md +49 -2
- package/dist/backend/entities/session.entity.d.ts +31 -0
- package/dist/backend/entities/session.entity.d.ts.map +1 -0
- package/dist/backend/entities/session.entity.js +95 -0
- package/dist/backend/entities/session.entity.js.map +1 -0
- package/dist/backend/index.d.ts +120 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +194 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/plugin.d.ts +41 -0
- package/dist/backend/plugin.d.ts.map +1 -0
- package/dist/backend/plugin.js +164 -0
- package/dist/backend/plugin.js.map +1 -0
- package/dist/backend/require-admin.d.ts +35 -0
- package/dist/backend/require-admin.d.ts.map +1 -0
- package/dist/backend/require-admin.js +50 -0
- package/dist/backend/require-admin.js.map +1 -0
- package/dist/backend/require-customer.d.ts +4 -0
- package/dist/backend/require-customer.d.ts.map +1 -0
- package/dist/backend/require-customer.js +16 -0
- package/dist/backend/require-customer.js.map +1 -0
- package/dist/backend/services/session-port.d.ts +30 -0
- package/dist/backend/services/session-port.d.ts.map +1 -0
- package/dist/backend/services/session-port.js +93 -0
- package/dist/backend/services/session-port.js.map +1 -0
- package/dist/backend/services/session-service.d.ts +56 -0
- package/dist/backend/services/session-service.d.ts.map +1 -0
- package/dist/backend/services/session-service.js +200 -0
- package/dist/backend/services/session-service.js.map +1 -0
- package/dist/manifest.d.ts +169 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +71 -0
- package/dist/manifest.js.map +1 -0
- package/docs/auth.md +42 -0
- package/package.json +60 -3
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import fastifyPlugin from 'fastify-plugin';
|
|
2
|
+
import { SESSION_COOKIE_NAME, ADMIN_SESSION_COOKIE_NAME, } from '@endora-commerce/contracts';
|
|
3
|
+
// The `declare module 'fastify'` block that puts `actor` and `adminActor` on
|
|
4
|
+
// `FastifyRequest` is the platform's since T118b
|
|
5
|
+
// (`packages/platform/src/http/request-actor.ts`), and this side-effect import
|
|
6
|
+
// is what puts it in *this* module's program — the writes below are the reason
|
|
7
|
+
// this file needs it, and they are the only writes in the tree.
|
|
8
|
+
import '@endora-commerce/platform/http';
|
|
9
|
+
/*
|
|
10
|
+
* Customer (storefront) sessions live in `b2b_session`; admin (Admin UI)
|
|
11
|
+
* sessions live in a **separate** `b2b_admin_session` cookie. Using two distinct
|
|
12
|
+
* cookie names lets a customer stay signed in on the storefront while an admin is
|
|
13
|
+
* signed in on the Admin UI in the same browser — on a shared host (e.g. all
|
|
14
|
+
* `localhost` ports) a single cookie name would clobber the other on every login.
|
|
15
|
+
*
|
|
16
|
+
* The two spellings moved to `@endora-commerce/contracts` in feature 075's Phase
|
|
17
|
+
* P. This file re-exported them for the consumers Phase C had not reached; that
|
|
18
|
+
* re-export is gone with the move to a package (T040b) because **there were
|
|
19
|
+
* none left** — every one of the five modules that set or clear the cookie
|
|
20
|
+
* already names the contracts package, which is what packaging them forced.
|
|
21
|
+
* Keeping it would also have put a `@endora-commerce/contracts` re-export on
|
|
22
|
+
* this package's `./backend` barrel, which `module-package-entity-surface.test.ts`
|
|
23
|
+
* refuses under D-168: it cannot follow the specifier, so whether a named entity
|
|
24
|
+
* class comes through it is unknown rather than false.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* If the request carries a valid admin session (resolved into `request.adminActor`
|
|
28
|
+
* by the auth plugin) but its ambient `request.actor` is not already an admin,
|
|
29
|
+
* promote the admin candidate. Admin guards call this so an admin route works
|
|
30
|
+
* even when a customer session is simultaneously present on the same request.
|
|
31
|
+
*/
|
|
32
|
+
export function promoteAdminActor(request) {
|
|
33
|
+
const r = request;
|
|
34
|
+
if (request.actor.kind !== 'admin' && r.adminActor) {
|
|
35
|
+
request.actor = r.adminActor;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function authPluginImpl(app, opts) {
|
|
39
|
+
const cookieName = opts.cookieName ?? SESSION_COOKIE_NAME;
|
|
40
|
+
const adminCookieName = opts.adminCookieName ?? ADMIN_SESSION_COOKIE_NAME;
|
|
41
|
+
// Fastify 5 forbids reference-type defaults on decorateRequest (they'd be
|
|
42
|
+
// shared across requests). Use a getter / setter pair backed by a
|
|
43
|
+
// per-request symbol so each request gets its own slot; the onRequest
|
|
44
|
+
// hook below seeds the value.
|
|
45
|
+
const actorSlot = Symbol('b2b-auth.actor');
|
|
46
|
+
app.decorateRequest('actor', {
|
|
47
|
+
getter() {
|
|
48
|
+
const self = this;
|
|
49
|
+
return self[actorSlot] ?? { kind: 'anonymous' };
|
|
50
|
+
},
|
|
51
|
+
setter(value) {
|
|
52
|
+
this[actorSlot] = value;
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
// Admin session candidate, resolved from the admin cookie independently of the
|
|
56
|
+
// customer actor so both can coexist on one request.
|
|
57
|
+
const adminActorSlot = Symbol('b2b-auth.adminActor');
|
|
58
|
+
app.decorateRequest('adminActor', {
|
|
59
|
+
getter() {
|
|
60
|
+
const self = this;
|
|
61
|
+
return self[adminActorSlot] ?? null;
|
|
62
|
+
},
|
|
63
|
+
setter(value) {
|
|
64
|
+
this[adminActorSlot] = value;
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
app.addHook('onRequest', async (request) => {
|
|
68
|
+
// Initialise to anonymous; downstream branches may overwrite with a
|
|
69
|
+
// typed customer / admin / api_key actor.
|
|
70
|
+
request.actor = { kind: 'anonymous' };
|
|
71
|
+
request.adminActor = null;
|
|
72
|
+
// 1. API key (Bearer) beats cookie — integrations pass Authorization: Bearer ...
|
|
73
|
+
const authHeader = request.headers.authorization;
|
|
74
|
+
if (authHeader?.startsWith('Bearer ') && opts.apiKeyResolver) {
|
|
75
|
+
const token = authHeader.slice('Bearer '.length).trim();
|
|
76
|
+
const resolved = await opts.apiKeyResolver(token);
|
|
77
|
+
if (resolved) {
|
|
78
|
+
request.actor = { kind: 'api_key', ...resolved };
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// Unknown bearer token — leave as anonymous; route-level guards will reject.
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const cookies = request.cookies;
|
|
85
|
+
// 2a. Admin cookie → admin session candidate (kept separate so it can
|
|
86
|
+
// coexist with a customer session in the same browser).
|
|
87
|
+
const adminRaw = cookies?.[adminCookieName];
|
|
88
|
+
if (adminRaw) {
|
|
89
|
+
const resolvedAdmin = await opts.sessionService.loadSession(adminRaw);
|
|
90
|
+
if (resolvedAdmin?.kind === 'admin' && resolvedAdmin.session.adminUserId) {
|
|
91
|
+
request.adminActor = {
|
|
92
|
+
kind: 'admin',
|
|
93
|
+
adminUserId: resolvedAdmin.session.adminUserId,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// 2b. Customer cookie → the ambient actor for public + customer routes.
|
|
98
|
+
const raw = cookies?.[cookieName];
|
|
99
|
+
if (raw) {
|
|
100
|
+
const resolved = await opts.sessionService.loadSession(raw);
|
|
101
|
+
if (resolved) {
|
|
102
|
+
const session = resolved.session;
|
|
103
|
+
if (resolved.kind === 'impersonation' && session.customerAccountId) {
|
|
104
|
+
const orgId = opts.customerOrgResolver
|
|
105
|
+
? (await opts.customerOrgResolver(session.customerAccountId)) ?? ''
|
|
106
|
+
: '';
|
|
107
|
+
request.actor = {
|
|
108
|
+
kind: 'customer',
|
|
109
|
+
customerAccountId: session.customerAccountId,
|
|
110
|
+
organizationId: orgId,
|
|
111
|
+
impersonatorAdminUserId: session.impersonatorAdminUserId ?? null,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
else if (resolved.kind === 'customer' && session.customerAccountId) {
|
|
115
|
+
const orgId = opts.customerOrgResolver
|
|
116
|
+
? (await opts.customerOrgResolver(session.customerAccountId)) ?? ''
|
|
117
|
+
: '';
|
|
118
|
+
request.actor = {
|
|
119
|
+
kind: 'customer',
|
|
120
|
+
customerAccountId: session.customerAccountId,
|
|
121
|
+
organizationId: orgId,
|
|
122
|
+
impersonatorAdminUserId: null,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
// An admin-kind session in the customer cookie (legacy) is ignored;
|
|
126
|
+
// admins now authenticate via the dedicated admin cookie.
|
|
127
|
+
// Feature 040 — keep presence fresh: stamp `lastSeenAt` on every active
|
|
128
|
+
// customer request (throttled to once/minute inside the service via a
|
|
129
|
+
// Redis marker). Without this, the admin "online customers" view only
|
|
130
|
+
// reflects login time, so an actively-browsing buyer ages out of the
|
|
131
|
+
// freshness window and disappears. Fire-and-forget: never block or fail
|
|
132
|
+
// the request on a presence write.
|
|
133
|
+
if ((resolved.kind === 'customer' || resolved.kind === 'impersonation') &&
|
|
134
|
+
session.customerAccountId) {
|
|
135
|
+
void opts.sessionService.touchLastSeen(session.id).catch(() => undefined);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// 3. If no customer/anonymous identity was established but an admin session
|
|
140
|
+
// exists, expose it as the ambient actor too, so admin flows that read
|
|
141
|
+
// `request.actor` without first running an admin guard still resolve.
|
|
142
|
+
if (request.actor.kind === 'anonymous' && request.adminActor) {
|
|
143
|
+
request.actor = request.adminActor;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
export const authPlugin = fastifyPlugin(authPluginImpl, {
|
|
148
|
+
name: 'b2b-auth',
|
|
149
|
+
fastify: '5.x',
|
|
150
|
+
});
|
|
151
|
+
/*
|
|
152
|
+
* Three `FastifyInstance` decorators lived here — `requireCustomer`,
|
|
153
|
+
* `requireAdmin`, `requireApiKey` — and were deleted with this module's
|
|
154
|
+
* conversion (feature 072, T078). **Nothing in `src/` or `test/` called any of
|
|
155
|
+
* them.**
|
|
156
|
+
*
|
|
157
|
+
* Worth recording rather than deleting silently, because the `requireAdmin` one
|
|
158
|
+
* read as a live authorisation hole: its body accepted any admin regardless of
|
|
159
|
+
* the permission code, under a comment promising the real check "in T188". It
|
|
160
|
+
* was not a hole — the guard every route actually uses is `createRequireAdmin`
|
|
161
|
+
* in `require-admin.ts`, which checks `permissionService.hasPermission`. The
|
|
162
|
+
* decorator was dead code that looked dangerous, which is its own kind of cost.
|
|
163
|
+
*/
|
|
164
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/backend/plugin.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { AdminPermissionChecker, RequireAdminAnyFactory, RequireAdminFactory } from '@endora-commerce/platform/kernel';
|
|
2
|
+
/**
|
|
3
|
+
* The admin guard (feature 072, D-32). **One implementation**, shared by
|
|
4
|
+
* production and the test harness.
|
|
5
|
+
*
|
|
6
|
+
* Before this file there were two. Production read `request.actor`, called
|
|
7
|
+
* `promoteAdminActor` and checked `permissionService.hasPermission`
|
|
8
|
+
* (`composition.ts`); the harness read `request.testActor` and took
|
|
9
|
+
* `permissionService` as **optional**, so omitting it silently disabled every
|
|
10
|
+
* permission check across 205 call sites in 60 modules. The harness happens to
|
|
11
|
+
* pass a `PermissionService` at all 28 of its wiring sites, so the checks did
|
|
12
|
+
* run — but nothing made them, and the two guards still differed in what they
|
|
13
|
+
* read and in whether an admin session riding alongside a customer session was
|
|
14
|
+
* promoted.
|
|
15
|
+
*
|
|
16
|
+
* `auth` owns this because promotion needs the auth plugin's per-request
|
|
17
|
+
* decorations, and the permission check needs `admin_roles` — which `auth`'s
|
|
18
|
+
* manifest now declares as a dependency. The kernel owns only the type.
|
|
19
|
+
*/
|
|
20
|
+
export interface RequireAdminDeps {
|
|
21
|
+
/** `admin_roles`' `PermissionService`, narrowed to what the guard uses. */
|
|
22
|
+
readonly permissionService: AdminPermissionChecker;
|
|
23
|
+
}
|
|
24
|
+
/** Gate a route on an admin session, optionally holding one permission code. */
|
|
25
|
+
export declare function createRequireAdmin({ permissionService }: RequireAdminDeps): RequireAdminFactory;
|
|
26
|
+
/**
|
|
27
|
+
* Gate a route on an admin session holding **any** of the listed codes.
|
|
28
|
+
*
|
|
29
|
+
* Lives here rather than in `http/` for the same reason as the single-code
|
|
30
|
+
* factory: it needs `promoteAdminActor` and a permission checker, so keeping it
|
|
31
|
+
* under `http/` made the HTTP layer import two domain modules — two of the five
|
|
32
|
+
* infrastructure→module edges this feature removes.
|
|
33
|
+
*/
|
|
34
|
+
export declare function createRequireAdminAny({ permissionService, }: RequireAdminDeps): RequireAdminAnyFactory;
|
|
35
|
+
//# sourceMappingURL=require-admin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"require-admin.d.ts","sourceRoot":"","sources":["../../src/backend/require-admin.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,sBAAsB,EACtB,sBAAsB,EACtB,mBAAmB,EACpB,MAAM,kCAAkC,CAAC;AAG1C;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,2EAA2E;IAC3E,QAAQ,CAAC,iBAAiB,EAAE,sBAAsB,CAAC;CACpD;AAkBD,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,EAAE,iBAAiB,EAAE,EAAE,gBAAgB,GAAG,mBAAmB,CAa/F;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,iBAAiB,GAClB,EAAE,gBAAgB,GAAG,sBAAsB,CAgB3C"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ERROR_CODES } from '@endora-commerce/contracts';
|
|
2
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
3
|
+
import { promoteAdminActor } from './plugin.js';
|
|
4
|
+
function resolveAdminActor(request) {
|
|
5
|
+
// `promoteAdminActor` dereferences `request.actor`. In production it is a
|
|
6
|
+
// decorated getter that always answers; a composition root that mounts a
|
|
7
|
+
// route without an actor resolver would otherwise turn a 401 into a 500.
|
|
8
|
+
const carrier = request;
|
|
9
|
+
if (carrier.actor !== undefined)
|
|
10
|
+
promoteAdminActor(request);
|
|
11
|
+
const actor = carrier.actor;
|
|
12
|
+
return actor?.kind === 'admin' ? actor : null;
|
|
13
|
+
}
|
|
14
|
+
/** Gate a route on an admin session, optionally holding one permission code. */
|
|
15
|
+
export function createRequireAdmin({ permissionService }) {
|
|
16
|
+
return (permission) => async (request) => {
|
|
17
|
+
const actor = resolveAdminActor(request);
|
|
18
|
+
if (!actor) {
|
|
19
|
+
throw new HttpError(401, ERROR_CODES.UNAUTHORIZED, 'Admin session required.');
|
|
20
|
+
}
|
|
21
|
+
if (!permission)
|
|
22
|
+
return;
|
|
23
|
+
const ok = await permissionService.hasPermission(actor.adminUserId, permission);
|
|
24
|
+
if (!ok) {
|
|
25
|
+
throw new HttpError(403, ERROR_CODES.FORBIDDEN, `Missing permission: ${permission}.`);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Gate a route on an admin session holding **any** of the listed codes.
|
|
31
|
+
*
|
|
32
|
+
* Lives here rather than in `http/` for the same reason as the single-code
|
|
33
|
+
* factory: it needs `promoteAdminActor` and a permission checker, so keeping it
|
|
34
|
+
* under `http/` made the HTTP layer import two domain modules — two of the five
|
|
35
|
+
* infrastructure→module edges this feature removes.
|
|
36
|
+
*/
|
|
37
|
+
export function createRequireAdminAny({ permissionService, }) {
|
|
38
|
+
return (codes) => async (request) => {
|
|
39
|
+
const actor = resolveAdminActor(request);
|
|
40
|
+
if (!actor) {
|
|
41
|
+
throw new HttpError(401, ERROR_CODES.UNAUTHORIZED, 'Admin session required.');
|
|
42
|
+
}
|
|
43
|
+
for (const code of codes) {
|
|
44
|
+
if (await permissionService.hasPermission(actor.adminUserId, code))
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
throw new HttpError(403, ERROR_CODES.FORBIDDEN, `Missing permission: one of ${codes.join(', ')}.`);
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=require-admin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"require-admin.js","sourceRoot":"","sources":["../../src/backend/require-admin.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAM3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAgChD,SAAS,iBAAiB,CAAC,OAAuB;IAChD,0EAA0E;IAC1E,yEAAyE;IACzE,yEAAyE;IACzE,MAAM,OAAO,GAAG,OAAuD,CAAC;IACxE,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS;QAAE,iBAAiB,CAAC,OAAO,CAAC,CAAC;IAC5D,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;IAC5B,OAAO,KAAK,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AAChD,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,kBAAkB,CAAC,EAAE,iBAAiB,EAAoB;IACxE,OAAO,CAAC,UAAmB,EAAE,EAAE,CAC7B,KAAK,EAAE,OAAO,EAAiB,EAAE;QAC/B,MAAM,KAAK,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAC;QACzC,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,WAAW,CAAC,YAAY,EAAE,yBAAyB,CAAC,CAAC;QAChF,CAAC;QACD,IAAI,CAAC,UAAU;YAAE,OAAO;QACxB,MAAM,EAAE,GAAG,MAAM,iBAAiB,CAAC,aAAa,CAAC,KAAK,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;QAChF,IAAI,CAAC,EAAE,EAAE,CAAC;YACR,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,WAAW,CAAC,SAAS,EAAE,uBAAuB,UAAU,GAAG,CAAC,CAAC;QACxF,CAAC;IACH,CAAC,CAAC;AACN,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,qBAAqB,CAAC,EACpC,iBAAiB,GACA;IACjB,OAAO,CAAC,KAAK,EAAE,EAAE,CACf,KAAK,EAAE,OAAO,EAAiB,EAAE;QAC/B,MAAM,KAAK,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAC;QACzC,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,WAAW,CAAC,YAAY,EAAE,yBAAyB,CAAC,CAAC;QAChF,CAAC;QACD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,MAAM,iBAAiB,CAAC,aAAa,CAAC,KAAK,CAAC,WAAW,EAAE,IAAI,CAAC;gBAAE,OAAO;QAC7E,CAAC;QACD,MAAM,IAAI,SAAS,CACjB,GAAG,EACH,WAAW,CAAC,SAAS,EACrB,8BAA8B,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAClD,CAAC;IACJ,CAAC,CAAC;AACN,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"require-customer.d.ts","sourceRoot":"","sources":["../../src/backend/require-customer.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AAwC7E,yDAAyD;AACzD,wBAAgB,qBAAqB,IAAI,oBAAoB,CAW5D"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ERROR_CODES } from '@endora-commerce/contracts';
|
|
2
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
3
|
+
/** Gate a route on an authenticated Customer session. */
|
|
4
|
+
export function createRequireCustomer() {
|
|
5
|
+
return async (request) => {
|
|
6
|
+
// In production `actor` is a decorated getter that always answers. A
|
|
7
|
+
// composition root that mounts a route without an actor resolver would
|
|
8
|
+
// otherwise turn a 401 into a 500 — the same guard `createRequireAdmin`
|
|
9
|
+
// puts in front of `promoteAdminActor`.
|
|
10
|
+
const actor = request.actor;
|
|
11
|
+
if (actor?.kind !== 'customer') {
|
|
12
|
+
throw new HttpError(401, ERROR_CODES.UNAUTHORIZED, 'Customer session required.');
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=require-customer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"require-customer.js","sourceRoot":"","sources":["../../src/backend/require-customer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAyC3D,yDAAyD;AACzD,MAAM,UAAU,qBAAqB;IACnC,OAAO,KAAK,EAAE,OAAuB,EAAiB,EAAE;QACtD,qEAAqE;QACrE,uEAAuE;QACvE,wEAAwE;QACxE,wCAAwC;QACxC,MAAM,KAAK,GAAI,OAA2D,CAAC,KAAK,CAAC;QACjF,IAAI,KAAK,EAAE,IAAI,KAAK,UAAU,EAAE,CAAC;YAC/B,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,WAAW,CAAC,YAAY,EAAE,4BAA4B,CAAC,CAAC;QACnF,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { AuthCustomerLastSeen, AuthSessionKind, AuthSessionPort, AuthSessionReadPort, AuthSessionRecord } from '@endora-commerce/contracts';
|
|
3
|
+
import { Session } from '../entities/session.entity.js';
|
|
4
|
+
import type { SessionService } from './session-service.js';
|
|
5
|
+
/**
|
|
6
|
+
* The published face of `auth`'s session machinery (feature 075, Phase P).
|
|
7
|
+
*
|
|
8
|
+
* `SessionService` is not itself the port, and the difference is one field:
|
|
9
|
+
* its `createSession` / `loadSession` hand back the `Session` **entity**, and
|
|
10
|
+
* an entity crossing a module boundary is the problem this feature exists to
|
|
11
|
+
* remove — publishing a port that hands one over would only rename it
|
|
12
|
+
* (contracts/port-publication.md §1.2). So the adapter below maps the row into
|
|
13
|
+
* `AuthSessionRecord`, which drops `tokenHash`: no consumer reads it, and it
|
|
14
|
+
* is the one field that must not travel.
|
|
15
|
+
*
|
|
16
|
+
* The adapter is additive. `sessionService` keeps its registration for the
|
|
17
|
+
* consumers Phase C has not reached yet.
|
|
18
|
+
*/
|
|
19
|
+
export declare function toAuthSessionRecord(session: Session, kind: AuthSessionKind): AuthSessionRecord;
|
|
20
|
+
export declare function createAuthSessionPort(sessionService: SessionService): AuthSessionPort;
|
|
21
|
+
/**
|
|
22
|
+
* The reporting half. `customers`' online-customers view is the only consumer,
|
|
23
|
+
* and it asks exactly one question: when was each of these accounts last seen?
|
|
24
|
+
*/
|
|
25
|
+
export declare class AuthSessionReadService implements AuthSessionReadPort {
|
|
26
|
+
private readonly emFactory;
|
|
27
|
+
constructor(emFactory: () => EntityManager);
|
|
28
|
+
lastSeenByCustomerAccount(customerAccountIds: readonly string[], since: Date): Promise<AuthCustomerLastSeen[]>;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=session-port.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-port.d.ts","sourceRoot":"","sources":["../../../src/backend/services/session-port.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EACV,oBAAoB,EACpB,eAAe,EACf,eAAe,EACf,mBAAmB,EACnB,iBAAiB,EAClB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AACxD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D;;;;;;;;;;;;;GAaG;AAEH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,eAAe,GAAG,iBAAiB,CAc9F;AASD,wBAAgB,qBAAqB,CAAC,cAAc,EAAE,cAAc,GAAG,eAAe,CA0BrF;AAED;;;GAGG;AACH,qBAAa,sBAAuB,YAAW,mBAAmB;IACpD,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,MAAM,aAAa;IAErD,yBAAyB,CAC7B,kBAAkB,EAAE,SAAS,MAAM,EAAE,EACrC,KAAK,EAAE,IAAI,GACV,OAAO,CAAC,oBAAoB,EAAE,CAAC;CAmBnC"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Session } from '../entities/session.entity.js';
|
|
2
|
+
/**
|
|
3
|
+
* The published face of `auth`'s session machinery (feature 075, Phase P).
|
|
4
|
+
*
|
|
5
|
+
* `SessionService` is not itself the port, and the difference is one field:
|
|
6
|
+
* its `createSession` / `loadSession` hand back the `Session` **entity**, and
|
|
7
|
+
* an entity crossing a module boundary is the problem this feature exists to
|
|
8
|
+
* remove — publishing a port that hands one over would only rename it
|
|
9
|
+
* (contracts/port-publication.md §1.2). So the adapter below maps the row into
|
|
10
|
+
* `AuthSessionRecord`, which drops `tokenHash`: no consumer reads it, and it
|
|
11
|
+
* is the one field that must not travel.
|
|
12
|
+
*
|
|
13
|
+
* The adapter is additive. `sessionService` keeps its registration for the
|
|
14
|
+
* consumers Phase C has not reached yet.
|
|
15
|
+
*/
|
|
16
|
+
export function toAuthSessionRecord(session, kind) {
|
|
17
|
+
return {
|
|
18
|
+
id: session.id,
|
|
19
|
+
kind,
|
|
20
|
+
customerAccountId: session.customerAccountId ?? null,
|
|
21
|
+
adminUserId: session.adminUserId ?? null,
|
|
22
|
+
impersonatorAdminUserId: session.impersonatorAdminUserId ?? null,
|
|
23
|
+
expiresAt: session.expiresAt,
|
|
24
|
+
createdAt: session.createdAt,
|
|
25
|
+
updatedAt: session.updatedAt,
|
|
26
|
+
lastSeenAt: session.lastSeenAt ?? null,
|
|
27
|
+
ipAddress: session.ipAddress ?? null,
|
|
28
|
+
userAgent: session.userAgent ?? null,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Same derivation `SessionService` applies internally, over the record's fields. */
|
|
32
|
+
function kindOf(session) {
|
|
33
|
+
if (session.impersonatorAdminUserId)
|
|
34
|
+
return 'impersonation';
|
|
35
|
+
if (session.adminUserId)
|
|
36
|
+
return 'admin';
|
|
37
|
+
return 'customer';
|
|
38
|
+
}
|
|
39
|
+
export function createAuthSessionPort(sessionService) {
|
|
40
|
+
return {
|
|
41
|
+
async createSession(input) {
|
|
42
|
+
const created = await sessionService.createSession(input);
|
|
43
|
+
return {
|
|
44
|
+
cookieValue: created.cookieValue,
|
|
45
|
+
expiresAt: created.expiresAt,
|
|
46
|
+
session: toAuthSessionRecord(created.session, kindOf(created.session)),
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
async loadSession(cookieValue) {
|
|
50
|
+
const resolved = await sessionService.loadSession(cookieValue);
|
|
51
|
+
if (!resolved)
|
|
52
|
+
return null;
|
|
53
|
+
return {
|
|
54
|
+
kind: resolved.kind,
|
|
55
|
+
session: toAuthSessionRecord(resolved.session, resolved.kind),
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
destroySession: (sessionId) => sessionService.destroySession(sessionId),
|
|
59
|
+
destroyAllForCustomer: (customerAccountId) => sessionService.destroyAllForCustomer(customerAccountId),
|
|
60
|
+
destroyAllForAdmin: (adminUserId) => sessionService.destroyAllForAdmin(adminUserId),
|
|
61
|
+
touchLastSeen: (sessionId) => sessionService.touchLastSeen(sessionId),
|
|
62
|
+
listRecentlyActiveCustomers: (windowMinutes) => sessionService.listRecentlyActiveCustomers(windowMinutes),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The reporting half. `customers`' online-customers view is the only consumer,
|
|
67
|
+
* and it asks exactly one question: when was each of these accounts last seen?
|
|
68
|
+
*/
|
|
69
|
+
export class AuthSessionReadService {
|
|
70
|
+
emFactory;
|
|
71
|
+
constructor(emFactory) {
|
|
72
|
+
this.emFactory = emFactory;
|
|
73
|
+
}
|
|
74
|
+
async lastSeenByCustomerAccount(customerAccountIds, since) {
|
|
75
|
+
if (customerAccountIds.length === 0)
|
|
76
|
+
return [];
|
|
77
|
+
const em = this.emFactory();
|
|
78
|
+
const rows = await em.find(Session, { customerAccountId: { $in: [...customerAccountIds] }, lastSeenAt: { $gte: since } }, { fields: ['customerAccountId', 'lastSeenAt'] });
|
|
79
|
+
const newest = new Map();
|
|
80
|
+
for (const row of rows) {
|
|
81
|
+
if (!row.customerAccountId || !row.lastSeenAt)
|
|
82
|
+
continue;
|
|
83
|
+
const previous = newest.get(row.customerAccountId);
|
|
84
|
+
if (!previous || row.lastSeenAt > previous)
|
|
85
|
+
newest.set(row.customerAccountId, row.lastSeenAt);
|
|
86
|
+
}
|
|
87
|
+
return [...newest].map(([customerAccountId, lastSeenAt]) => ({
|
|
88
|
+
customerAccountId,
|
|
89
|
+
lastSeenAt,
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=session-port.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-port.js","sourceRoot":"","sources":["../../../src/backend/services/session-port.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { Redis } from 'ioredis';
|
|
3
|
+
import { Session } from '../entities/session.entity.js';
|
|
4
|
+
export type SessionKind = 'customer' | 'admin' | 'impersonation';
|
|
5
|
+
export interface CreateSessionInput {
|
|
6
|
+
kind: SessionKind;
|
|
7
|
+
customerAccountId?: string;
|
|
8
|
+
adminUserId?: string;
|
|
9
|
+
impersonatorAdminUserId?: string;
|
|
10
|
+
ipAddress?: string;
|
|
11
|
+
userAgent?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface SessionCookiePayload {
|
|
14
|
+
cookieValue: string;
|
|
15
|
+
expiresAt: Date;
|
|
16
|
+
session: Session;
|
|
17
|
+
}
|
|
18
|
+
export interface ResolvedSession {
|
|
19
|
+
session: Session;
|
|
20
|
+
kind: SessionKind;
|
|
21
|
+
}
|
|
22
|
+
export declare class SessionService {
|
|
23
|
+
private readonly emFactory;
|
|
24
|
+
private readonly redis;
|
|
25
|
+
constructor(emFactory: () => EntityManager, redis: Redis);
|
|
26
|
+
createSession(input: CreateSessionInput): Promise<SessionCookiePayload>;
|
|
27
|
+
loadSession(cookieValue: string): Promise<ResolvedSession | null>;
|
|
28
|
+
destroySession(sessionId: string): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Revoke every active session for a Customer — used when an account is
|
|
31
|
+
* blocked or soft-deleted so it loses access promptly (feature 040,
|
|
32
|
+
* FR-016/SC-002). Clears both the Postgres rows and their Redis caches.
|
|
33
|
+
*/
|
|
34
|
+
destroyAllForCustomer(customerAccountId: string): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Revoke every session an admin user holds — both the sessions they signed
|
|
37
|
+
* in with and the impersonations they started, which carry the admin id in
|
|
38
|
+
* `impersonatorAdminUserId` and a customer id in `customerAccountId`. Used
|
|
39
|
+
* when a peer resets their password (issue #252): a session that survives
|
|
40
|
+
* the reset is a credential the reset did not withdraw, which is exactly
|
|
41
|
+
* what the compromised-account case needs withdrawn.
|
|
42
|
+
*/
|
|
43
|
+
destroyAllForAdmin(adminUserId: string): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Feature 040 — stamp activity, throttled to at most once per minute per
|
|
46
|
+
* session (Redis marker) to avoid write amplification on every request.
|
|
47
|
+
*/
|
|
48
|
+
touchLastSeen(sessionId: string): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Feature 040 — distinct customer ids whose session was active within the
|
|
51
|
+
* freshness window, for the admin "online customers" view.
|
|
52
|
+
*/
|
|
53
|
+
listRecentlyActiveCustomers(windowMinutes: number): Promise<string[]>;
|
|
54
|
+
private cache;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=session-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/session-service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAaxD,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,OAAO,GAAG,eAAe,CAAC;AAEjE,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,WAAW,CAAC;IAClB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,IAAI,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,WAAW,CAAC;CACnB;AAED,qBAAa,cAAc;IAEvB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,KAAK;gBADL,SAAS,EAAE,MAAM,aAAa,EAC9B,KAAK,EAAE,KAAK;IAGzB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAgCvE,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IA2BjE,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWtD;;;;OAIG;IACG,qBAAqB,CAAC,iBAAiB,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYrE;;;;;;;OAOG;IACG,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAe5D;;;OAGG;IACG,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUrD;;;OAGG;IACG,2BAA2B,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;YAW7D,KAAK;CASpB"}
|