@endora-commerce/mod-admin-users 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 +59 -0
- package/dist/admin/index.d.ts +47 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +44 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/AdminRolesPage.d.ts +3 -0
- package/dist/admin/pages/AdminRolesPage.d.ts.map +1 -0
- package/dist/admin/pages/AdminRolesPage.js +180 -0
- package/dist/admin/pages/AdminRolesPage.js.map +1 -0
- package/dist/admin/pages/AdminUsersPage.d.ts +3 -0
- package/dist/admin/pages/AdminUsersPage.d.ts.map +1 -0
- package/dist/admin/pages/AdminUsersPage.js +151 -0
- package/dist/admin/pages/AdminUsersPage.js.map +1 -0
- package/dist/admin/permission-label.d.ts +26 -0
- package/dist/admin/permission-label.d.ts.map +1 -0
- package/dist/admin/permission-label.js +67 -0
- package/dist/admin/permission-label.js.map +1 -0
- package/dist/backend/cli/create-admin.d.ts +16 -0
- package/dist/backend/cli/create-admin.d.ts.map +1 -0
- package/dist/backend/cli/create-admin.js +101 -0
- package/dist/backend/cli/create-admin.js.map +1 -0
- package/dist/backend/demo/reset.d.ts +15 -0
- package/dist/backend/demo/reset.d.ts.map +1 -0
- package/dist/backend/demo/reset.js +13 -0
- package/dist/backend/demo/reset.js.map +1 -0
- package/dist/backend/demo/rows.d.ts +50 -0
- package/dist/backend/demo/rows.d.ts.map +1 -0
- package/dist/backend/demo/rows.js +73 -0
- package/dist/backend/demo/rows.js.map +1 -0
- package/dist/backend/demo/seed.d.ts +24 -0
- package/dist/backend/demo/seed.d.ts.map +1 -0
- package/dist/backend/demo/seed.js +43 -0
- package/dist/backend/demo/seed.js.map +1 -0
- package/dist/backend/entities/admin-user.entity.d.ts +44 -0
- package/dist/backend/entities/admin-user.entity.d.ts.map +1 -0
- package/dist/backend/entities/admin-user.entity.js +115 -0
- package/dist/backend/entities/admin-user.entity.js.map +1 -0
- package/dist/backend/index.d.ts +77 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +111 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/plugin.d.ts +70 -0
- package/dist/backend/plugin.d.ts.map +1 -0
- package/dist/backend/plugin.js +50 -0
- package/dist/backend/plugin.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +32 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +192 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/routes.impersonation.d.ts +25 -0
- package/dist/backend/routes.impersonation.d.ts.map +1 -0
- package/dist/backend/routes.impersonation.js +108 -0
- package/dist/backend/routes.impersonation.js.map +1 -0
- package/dist/backend/routes.public.d.ts +20 -0
- package/dist/backend/routes.public.d.ts.map +1 -0
- package/dist/backend/routes.public.js +75 -0
- package/dist/backend/routes.public.js.map +1 -0
- package/dist/backend/services/admin-auth-service.d.ts +47 -0
- package/dist/backend/services/admin-auth-service.d.ts.map +1 -0
- package/dist/backend/services/admin-auth-service.js +93 -0
- package/dist/backend/services/admin-auth-service.js.map +1 -0
- package/dist/backend/services/admin-user-ports.d.ts +75 -0
- package/dist/backend/services/admin-user-ports.d.ts.map +1 -0
- package/dist/backend/services/admin-user-ports.js +160 -0
- package/dist/backend/services/admin-user-ports.js.map +1 -0
- package/dist/backend/services/admin-user-service.d.ts +134 -0
- package/dist/backend/services/admin-user-service.d.ts.map +1 -0
- package/dist/backend/services/admin-user-service.js +241 -0
- package/dist/backend/services/admin-user-service.js.map +1 -0
- package/dist/backend/services/impersonation-service.d.ts +65 -0
- package/dist/backend/services/impersonation-service.d.ts.map +1 -0
- package/dist/backend/services/impersonation-service.js +103 -0
- package/dist/backend/services/impersonation-service.js.map +1 -0
- package/dist/backend/services/two-factor-enrolments.d.ts +19 -0
- package/dist/backend/services/two-factor-enrolments.d.ts.map +1 -0
- package/dist/backend/services/two-factor-enrolments.js +2 -0
- package/dist/backend/services/two-factor-enrolments.js.map +1 -0
- package/dist/manifest.d.ts +182 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +208 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260425T053028_admin_users_init.d.ts +11 -0
- package/dist/migrations/20260425T053028_admin_users_init.d.ts.map +1 -0
- package/dist/migrations/20260425T053028_admin_users_init.js +51 -0
- package/dist/migrations/20260425T053028_admin_users_init.js.map +1 -0
- package/dist/migrations/20260819T155150_admin_users_fold_email_case.d.ts +55 -0
- package/dist/migrations/20260819T155150_admin_users_fold_email_case.d.ts.map +1 -0
- package/dist/migrations/20260819T155150_admin_users_fold_email_case.js +106 -0
- package/dist/migrations/20260819T155150_admin_users_fold_email_case.js.map +1 -0
- package/dist/migrations/20260825T124801_admin_users_drop_legacy_two_factor_secret.d.ts +25 -0
- package/dist/migrations/20260825T124801_admin_users_drop_legacy_two_factor_secret.d.ts.map +1 -0
- package/dist/migrations/20260825T124801_admin_users_drop_legacy_two_factor_secret.js +29 -0
- package/dist/migrations/20260825T124801_admin_users_drop_legacy_two_factor_secret.js.map +1 -0
- package/dist/migrations/index.d.ts +29 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +33 -0
- package/dist/migrations/index.js.map +1 -0
- package/docs/admin_users.md +53 -0
- package/i18n/en.json +5 -0
- package/i18n/pl.json +5 -0
- package/package.json +100 -0
- package/tailwind.css +14 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { ADMIN_SESSION_COOKIE_NAME, ERROR_CODES, SESSION_COOKIE_NAME, impersonationRequestSchema, } from '@endora-commerce/contracts';
|
|
2
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
3
|
+
/**
|
|
4
|
+
* Admin impersonation routes (T191).
|
|
5
|
+
*
|
|
6
|
+
* - POST /admin/organizations/:id/impersonate — admin starts impersonation of
|
|
7
|
+
* a customer in the target Organization. Audit row written BEFORE the
|
|
8
|
+
* cookie is set (T179). Two cookies are issued: the new b2b_session for
|
|
9
|
+
* the impersonation, and admin_shadow_session preserving the original
|
|
10
|
+
* admin session value so end can restore it.
|
|
11
|
+
* - POST /admin/impersonation/end — destroys the impersonation session,
|
|
12
|
+
* restores the admin session from admin_shadow_session, writes the
|
|
13
|
+
* impersonation.end audit row.
|
|
14
|
+
*/
|
|
15
|
+
const ADMIN_SHADOW_COOKIE = 'admin_shadow_session';
|
|
16
|
+
export async function registerImpersonationRoutes(app, deps) {
|
|
17
|
+
const { impersonationService, requireAdmin, resolveAdminUserId } = deps;
|
|
18
|
+
app.post('/api/v1/admin/organizations/:id/impersonate', {
|
|
19
|
+
preHandler: requireAdmin('customers:impersonate'),
|
|
20
|
+
schema: { body: impersonationRequestSchema },
|
|
21
|
+
}, async (request, reply) => {
|
|
22
|
+
const body = impersonationRequestSchema.parse(request.body);
|
|
23
|
+
const adminId = resolveAdminUserId(request);
|
|
24
|
+
const cookies = request.cookies;
|
|
25
|
+
// The admin's own session lives in the dedicated admin cookie; fall back to
|
|
26
|
+
// the legacy customer-cookie name so older sessions / test stubs still work.
|
|
27
|
+
const adminCookie = cookies?.[ADMIN_SESSION_COOKIE_NAME] ?? cookies?.[SESSION_COOKIE_NAME] ?? '';
|
|
28
|
+
// For tests, the stub-admin-session cookie isn't a real session value —
|
|
29
|
+
// we treat the entire raw cookie as the shadow regardless. End() will
|
|
30
|
+
// refuse to restore a stub cookie that no longer resolves.
|
|
31
|
+
const result = await impersonationService.start({
|
|
32
|
+
adminUserId: adminId,
|
|
33
|
+
adminSessionCookieValue: adminCookie,
|
|
34
|
+
customerAccountId: body.customerAccountId,
|
|
35
|
+
organizationId: request.params.id,
|
|
36
|
+
...(body.reason !== undefined ? { reason: body.reason } : {}),
|
|
37
|
+
...(request.ip ? { ip: request.ip } : {}),
|
|
38
|
+
...(typeof request.headers['user-agent'] === 'string'
|
|
39
|
+
? { userAgent: request.headers['user-agent'] }
|
|
40
|
+
: {}),
|
|
41
|
+
requestId: request.id,
|
|
42
|
+
});
|
|
43
|
+
setSessionCookie(reply, result.impersonationCookieValue, result.impersonationExpiresAt);
|
|
44
|
+
reply.setCookie(ADMIN_SHADOW_COOKIE, result.adminShadowSessionCookieValue, {
|
|
45
|
+
path: '/',
|
|
46
|
+
httpOnly: true,
|
|
47
|
+
sameSite: 'lax',
|
|
48
|
+
secure: process.env['NODE_ENV'] === 'production',
|
|
49
|
+
expires: result.impersonationExpiresAt,
|
|
50
|
+
signed: false,
|
|
51
|
+
});
|
|
52
|
+
return {
|
|
53
|
+
data: {
|
|
54
|
+
impersonationSessionId: result.impersonationSessionId,
|
|
55
|
+
impersonatedCustomerAccount: {
|
|
56
|
+
id: result.impersonatedCustomerAccount.id,
|
|
57
|
+
email: result.impersonatedCustomerAccount.email,
|
|
58
|
+
firstName: result.impersonatedCustomerAccount.firstName,
|
|
59
|
+
lastName: result.impersonatedCustomerAccount.lastName,
|
|
60
|
+
role: result.impersonatedCustomerAccount.role,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
app.post('/api/v1/admin/impersonation/end', async (request, reply) => {
|
|
66
|
+
const cookies = request.cookies;
|
|
67
|
+
const impersonationCookie = cookies?.[SESSION_COOKIE_NAME];
|
|
68
|
+
const shadow = cookies?.[ADMIN_SHADOW_COOKIE];
|
|
69
|
+
if (!impersonationCookie || !shadow) {
|
|
70
|
+
throw new HttpError(401, ERROR_CODES.UNAUTHORIZED, 'No impersonation session active.');
|
|
71
|
+
}
|
|
72
|
+
const result = await impersonationService.end({
|
|
73
|
+
impersonationSessionCookieValue: impersonationCookie,
|
|
74
|
+
adminShadowCookieValue: shadow,
|
|
75
|
+
...(request.ip ? { ip: request.ip } : {}),
|
|
76
|
+
...(typeof request.headers['user-agent'] === 'string'
|
|
77
|
+
? { userAgent: request.headers['user-agent'] }
|
|
78
|
+
: {}),
|
|
79
|
+
requestId: request.id,
|
|
80
|
+
});
|
|
81
|
+
// Restore the admin session into the admin cookie and drop the impersonation
|
|
82
|
+
// session from the customer cookie, so ending impersonation leaves the admin
|
|
83
|
+
// signed in to the Admin UI but no longer acting as the customer.
|
|
84
|
+
setAdminSessionCookie(reply, result.adminSessionCookieValue, result.adminSessionExpiresAt);
|
|
85
|
+
reply.clearCookie(SESSION_COOKIE_NAME, { path: '/' });
|
|
86
|
+
reply.clearCookie(ADMIN_SHADOW_COOKIE, { path: '/' });
|
|
87
|
+
return { data: { restored: true } };
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/** Set the impersonation (customer-scoped) session in the customer cookie. */
|
|
91
|
+
function setSessionCookie(reply, value, expiresAt) {
|
|
92
|
+
setCookie(reply, SESSION_COOKIE_NAME, value, expiresAt);
|
|
93
|
+
}
|
|
94
|
+
/** Set the admin session in the dedicated admin cookie. */
|
|
95
|
+
function setAdminSessionCookie(reply, value, expiresAt) {
|
|
96
|
+
setCookie(reply, ADMIN_SESSION_COOKIE_NAME, value, expiresAt);
|
|
97
|
+
}
|
|
98
|
+
function setCookie(reply, name, value, expiresAt) {
|
|
99
|
+
reply.setCookie(name, value, {
|
|
100
|
+
path: '/',
|
|
101
|
+
httpOnly: true,
|
|
102
|
+
sameSite: 'lax',
|
|
103
|
+
secure: process.env['NODE_ENV'] === 'production',
|
|
104
|
+
expires: expiresAt,
|
|
105
|
+
signed: false,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=routes.impersonation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.impersonation.js","sourceRoot":"","sources":["../../src/backend/routes.impersonation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
import type { AdminAuthService } from './services/admin-auth-service.js';
|
|
3
|
+
import type { TwoFactorEnrolmentReader } from './services/two-factor-enrolments.js';
|
|
4
|
+
/**
|
|
5
|
+
* Anonymous admin endpoints — login + logout. The admin surface is otherwise
|
|
6
|
+
* gated by `requireAdmin` in each module's admin routes.
|
|
7
|
+
*/
|
|
8
|
+
export interface AdminPublicDeps {
|
|
9
|
+
adminAuthService: AdminAuthService;
|
|
10
|
+
/**
|
|
11
|
+
* `mfa`'s answer to "does this admin hold a second factor". The login
|
|
12
|
+
* response carries `twoFactorEnabled`, and it used to carry
|
|
13
|
+
* `!!a.twoFactorConfirmedAt` — a column nothing writes — so the Admin UI's
|
|
14
|
+
* auth context has always seen `false` for an admin who had just satisfied a
|
|
15
|
+
* TOTP challenge two steps earlier.
|
|
16
|
+
*/
|
|
17
|
+
twoFactorEnrolments: TwoFactorEnrolmentReader;
|
|
18
|
+
}
|
|
19
|
+
export declare function registerAdminPublicRoutes(app: FastifyInstance, deps: AdminPublicDeps): Promise<void>;
|
|
20
|
+
//# sourceMappingURL=routes.public.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.public.d.ts","sourceRoot":"","sources":["../../src/backend/routes.public.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,eAAe,EAAgB,MAAM,SAAS,CAAC;AAE7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAEzE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,qCAAqC,CAAC;AAEpF;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,gBAAgB,EAAE,gBAAgB,CAAC;IACnC;;;;;;OAMG;IACH,mBAAmB,EAAE,wBAAwB,CAAC;CAC/C;AAED,wBAAsB,yBAAyB,CAC7C,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,IAAI,CAAC,CAmDf"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { ADMIN_SESSION_COOKIE_NAME, adminLoginRequestSchema } from '@endora-commerce/contracts';
|
|
2
|
+
export async function registerAdminPublicRoutes(app, deps) {
|
|
3
|
+
const { adminAuthService, twoFactorEnrolments } = deps;
|
|
4
|
+
async function serializeOneAdminUser(user) {
|
|
5
|
+
const enrolled = await twoFactorEnrolments([user.id]);
|
|
6
|
+
return serializeAdminUser(user, enrolled.has(user.id));
|
|
7
|
+
}
|
|
8
|
+
app.post('/api/v1/auth/admin/login', { schema: { body: adminLoginRequestSchema } }, async (request, reply) => {
|
|
9
|
+
const body = adminLoginRequestSchema.parse(request.body);
|
|
10
|
+
const result = await adminAuthService.login({
|
|
11
|
+
email: body.email,
|
|
12
|
+
password: body.password,
|
|
13
|
+
...(request.ip ? { ip: request.ip } : {}),
|
|
14
|
+
...(typeof request.headers['user-agent'] === 'string'
|
|
15
|
+
? { userAgent: request.headers['user-agent'] }
|
|
16
|
+
: {}),
|
|
17
|
+
});
|
|
18
|
+
// Feature 042 — two-step login (no session cookie until the second step).
|
|
19
|
+
if (result.status === 'mfaRequired') {
|
|
20
|
+
return { data: { status: 'mfaRequired', challengeId: result.challengeId } };
|
|
21
|
+
}
|
|
22
|
+
if (result.status === 'mfaSetupRequired') {
|
|
23
|
+
return { data: { status: 'mfaSetupRequired', setupTicket: result.setupTicket } };
|
|
24
|
+
}
|
|
25
|
+
setSessionCookie(reply, result.sessionCookieValue, result.sessionExpiresAt);
|
|
26
|
+
return {
|
|
27
|
+
data: {
|
|
28
|
+
status: 'authenticated',
|
|
29
|
+
adminUser: await serializeOneAdminUser(result.adminUser),
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
app.post('/api/v1/auth/admin/logout', async (request, reply) => {
|
|
34
|
+
const cookies = request.cookies;
|
|
35
|
+
const raw = cookies?.[ADMIN_SESSION_COOKIE_NAME];
|
|
36
|
+
if (raw) {
|
|
37
|
+
const dot = raw.indexOf('.');
|
|
38
|
+
if (dot !== -1) {
|
|
39
|
+
const sessionId = raw.slice(0, dot);
|
|
40
|
+
if (sessionId)
|
|
41
|
+
await adminAuthService.logout(sessionId);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
reply.clearCookie(ADMIN_SESSION_COOKIE_NAME, { path: '/' });
|
|
45
|
+
return reply.status(204).send();
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function setSessionCookie(reply, value, expiresAt) {
|
|
49
|
+
reply.setCookie(ADMIN_SESSION_COOKIE_NAME, value, {
|
|
50
|
+
path: '/',
|
|
51
|
+
httpOnly: true,
|
|
52
|
+
sameSite: 'lax',
|
|
53
|
+
secure: process.env['NODE_ENV'] === 'production',
|
|
54
|
+
expires: expiresAt,
|
|
55
|
+
signed: false,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function serializeAdminUser(a, twoFactorEnabled) {
|
|
59
|
+
return {
|
|
60
|
+
id: a.id,
|
|
61
|
+
email: a.email,
|
|
62
|
+
firstName: a.firstName,
|
|
63
|
+
lastName: a.lastName,
|
|
64
|
+
adminRoleId: a.adminRoleId ?? null,
|
|
65
|
+
twoFactorEnabled,
|
|
66
|
+
status: a.status,
|
|
67
|
+
// Feature 019: surface the per-user Admin UI language preference so the
|
|
68
|
+
// SPA's TranslationProvider can seed itself without a separate fetch.
|
|
69
|
+
preferredLanguage: a.preferredLanguage ?? null,
|
|
70
|
+
lastLoginAt: a.lastLoginAt?.toISOString() ?? null,
|
|
71
|
+
createdAt: a.createdAt.toISOString(),
|
|
72
|
+
updatedAt: a.updatedAt.toISOString(),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=routes.public.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.public.js","sourceRoot":"","sources":["../../src/backend/routes.public.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,yBAAyB,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAqBhG,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,GAAoB,EACpB,IAAqB;IAErB,MAAM,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,GAAG,IAAI,CAAC;IAEvD,KAAK,UAAU,qBAAqB,CAAC,IAAe;QAClD,MAAM,QAAQ,GAAG,MAAM,mBAAmB,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;QACtD,OAAO,kBAAkB,CAAC,IAAI,EAAE,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;IACzD,CAAC;IAED,GAAG,CAAC,IAAI,CACN,0BAA0B,EAC1B,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,uBAAuB,EAAE,EAAE,EAC7C,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QACvB,MAAM,IAAI,GAAG,uBAAuB,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACzD,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,KAAK,CAAC;YAC1C,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACzC,GAAG,CAAC,OAAO,OAAO,CAAC,OAAO,CAAC,YAAY,CAAC,KAAK,QAAQ;gBACnD,CAAC,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE;gBAC9C,CAAC,CAAC,EAAE,CAAC;SACR,CAAC,CAAC;QACH,0EAA0E;QAC1E,IAAI,MAAM,CAAC,MAAM,KAAK,aAAa,EAAE,CAAC;YACpC,OAAO,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE,EAAE,CAAC;QAC9E,CAAC;QACD,IAAI,MAAM,CAAC,MAAM,KAAK,kBAAkB,EAAE,CAAC;YACzC,OAAO,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE,EAAE,CAAC;QACnF,CAAC;QACD,gBAAgB,CAAC,KAAK,EAAE,MAAM,CAAC,kBAAkB,EAAE,MAAM,CAAC,gBAAgB,CAAC,CAAC;QAC5E,OAAO;YACL,IAAI,EAAE;gBACJ,MAAM,EAAE,eAAe;gBACvB,SAAS,EAAE,MAAM,qBAAqB,CAAC,MAAM,CAAC,SAAS,CAAC;aACzD;SACF,CAAC;IACJ,CAAC,CACF,CAAC;IAEF,GAAG,CAAC,IAAI,CAAC,2BAA2B,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QAC7D,MAAM,OAAO,GAAI,OAA4D,CAAC,OAAO,CAAC;QACtF,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC,yBAAyB,CAAC,CAAC;QACjD,IAAI,GAAG,EAAE,CAAC;YACR,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YAC7B,IAAI,GAAG,KAAK,CAAC,CAAC,EAAE,CAAC;gBACf,MAAM,SAAS,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;gBACpC,IAAI,SAAS;oBAAE,MAAM,gBAAgB,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YAC1D,CAAC;QACH,CAAC;QACD,KAAK,CAAC,WAAW,CAAC,yBAAyB,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC;QAC5D,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IAClC,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAmB,EAAE,KAAa,EAAE,SAAe;IAC3E,KAAK,CAAC,SAAS,CAAC,yBAAyB,EAAE,KAAK,EAAE;QAChD,IAAI,EAAE,GAAG;QACT,QAAQ,EAAE,IAAI;QACd,QAAQ,EAAE,KAAK;QACf,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,YAAY;QAChD,OAAO,EAAE,SAAS;QAClB,MAAM,EAAE,KAAK;KACd,CAAC,CAAC;AACL,CAAC;AAED,SAAS,kBAAkB,CAAC,CAAY,EAAE,gBAAyB;IACjE,OAAO;QACL,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,SAAS,EAAE,CAAC,CAAC,SAAS;QACtB,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,WAAW,EAAE,CAAC,CAAC,WAAW,IAAI,IAAI;QAClC,gBAAgB;QAChB,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,wEAAwE;QACxE,sEAAsE;QACtE,iBAAiB,EAAE,CAAC,CAAC,iBAAiB,IAAI,IAAI;QAC9C,WAAW,EAAE,CAAC,CAAC,WAAW,EAAE,WAAW,EAAE,IAAI,IAAI;QACjD,SAAS,EAAE,CAAC,CAAC,SAAS,CAAC,WAAW,EAAE;QACpC,SAAS,EAAE,CAAC,CAAC,SAAS,CAAC,WAAW,EAAE;KACrC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import { type AuthSessionPort, type MfaLoginPort } from '@endora-commerce/contracts';
|
|
3
|
+
import { AdminUser } from '../entities/admin-user.entity.js';
|
|
4
|
+
import type { AuditPort } from '@endora-commerce/platform/kernel';
|
|
5
|
+
/**
|
|
6
|
+
* AdminAuthService (T186; two-step login added in feature 042). login →
|
|
7
|
+
* first factor; when an `MfaLoginPort` is injected and the admin has active
|
|
8
|
+
* 2FA (or 2FA is enforced) it returns `mfaRequired` / `mfaSetupRequired`
|
|
9
|
+
* without a session. Absent port ⇒ password-only (FR-033).
|
|
10
|
+
*/
|
|
11
|
+
export interface AdminLoginResult {
|
|
12
|
+
adminUser: AdminUser;
|
|
13
|
+
sessionCookieValue: string;
|
|
14
|
+
sessionExpiresAt: Date;
|
|
15
|
+
}
|
|
16
|
+
/** Discriminated outcome of the first admin login step (feature 042). */
|
|
17
|
+
export type AdminLoginOutcome = ({
|
|
18
|
+
status: 'authenticated';
|
|
19
|
+
} & AdminLoginResult) | {
|
|
20
|
+
status: 'mfaRequired';
|
|
21
|
+
challengeId: string;
|
|
22
|
+
} | {
|
|
23
|
+
status: 'mfaSetupRequired';
|
|
24
|
+
setupTicket: string;
|
|
25
|
+
};
|
|
26
|
+
export declare class AdminAuthService {
|
|
27
|
+
private readonly emFactory;
|
|
28
|
+
/** `auth`'s published session surface (feature 075, Phase C). */
|
|
29
|
+
private readonly sessionPort;
|
|
30
|
+
/** Lazily resolved so composition can late-bind the MFA module. */
|
|
31
|
+
private readonly getMfaLoginPort?;
|
|
32
|
+
private readonly auditLog?;
|
|
33
|
+
constructor(emFactory: () => EntityManager,
|
|
34
|
+
/** `auth`'s published session surface (feature 075, Phase C). */
|
|
35
|
+
sessionPort: AuthSessionPort,
|
|
36
|
+
/** Lazily resolved so composition can late-bind the MFA module. */
|
|
37
|
+
getMfaLoginPort?: (() => MfaLoginPort | undefined) | undefined, auditLog?: AuditPort | undefined);
|
|
38
|
+
login(input: {
|
|
39
|
+
email: string;
|
|
40
|
+
password: string;
|
|
41
|
+
ip?: string;
|
|
42
|
+
userAgent?: string;
|
|
43
|
+
}): Promise<AdminLoginOutcome>;
|
|
44
|
+
changePassword(adminUserId: string, currentPassword: string, newPassword: string): Promise<void>;
|
|
45
|
+
logout(sessionId: string): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=admin-auth-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-auth-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/admin-auth-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,YAAY,EAClB,MAAM,4BAA4B,CAAC;AAGpC,OAAO,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAE7D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAElE;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,SAAS,CAAC;IACrB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gBAAgB,EAAE,IAAI,CAAC;CACxB;AAED,yEAAyE;AACzE,MAAM,MAAM,iBAAiB,GACzB,CAAC;IAAE,MAAM,EAAE,eAAe,CAAA;CAAE,GAAG,gBAAgB,CAAC,GAChD;IAAE,MAAM,EAAE,aAAa,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAC9C;IAAE,MAAM,EAAE,kBAAkB,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC;AAExD,qBAAa,gBAAgB;IAEzB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,iEAAiE;IACjE,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,mEAAmE;IACnE,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBALT,SAAS,EAAE,MAAM,aAAa;IAC/C,iEAAiE;IAChD,WAAW,EAAE,eAAe;IAC7C,mEAAmE;IAClD,eAAe,CAAC,GAAE,MAAM,YAAY,GAAG,SAAS,aAAA,EAChD,QAAQ,CAAC,EAAE,SAAS,YAAA;IAGjC,KAAK,CAAC,KAAK,EAAE;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,MAAM,CAAC;QACjB,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAoDxB,cAAc,CAClB,WAAW,EAAE,MAAM,EACnB,eAAe,EAAE,MAAM,EACvB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC;IA2BV,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAG/C"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { ERROR_CODES, normalizeEmailAddress, } from '@endora-commerce/contracts';
|
|
2
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
3
|
+
import { hashPassword, verifyPassword } from '@endora-commerce/platform/kernel';
|
|
4
|
+
import { AdminUser } from '../entities/admin-user.entity.js';
|
|
5
|
+
import { recordAuditFromContext } from '@endora-commerce/platform/commands';
|
|
6
|
+
export class AdminAuthService {
|
|
7
|
+
emFactory;
|
|
8
|
+
sessionPort;
|
|
9
|
+
getMfaLoginPort;
|
|
10
|
+
auditLog;
|
|
11
|
+
constructor(emFactory,
|
|
12
|
+
/** `auth`'s published session surface (feature 075, Phase C). */
|
|
13
|
+
sessionPort,
|
|
14
|
+
/** Lazily resolved so composition can late-bind the MFA module. */
|
|
15
|
+
getMfaLoginPort, auditLog) {
|
|
16
|
+
this.emFactory = emFactory;
|
|
17
|
+
this.sessionPort = sessionPort;
|
|
18
|
+
this.getMfaLoginPort = getMfaLoginPort;
|
|
19
|
+
this.auditLog = auditLog;
|
|
20
|
+
}
|
|
21
|
+
async login(input) {
|
|
22
|
+
const em = this.emFactory();
|
|
23
|
+
// The address is folded before it is compared, because it was folded before
|
|
24
|
+
// it was stored: Postgres' `=` on `text` is case-sensitive, so an operator
|
|
25
|
+
// created as `Operator.Mixed@example.com` matched no row when they typed the
|
|
26
|
+
// address they were handed, and the refusal below says nothing about
|
|
27
|
+
// casing. `normalizeEmailAddress` is the same fold the write applies.
|
|
28
|
+
const admin = await em.findOne(AdminUser, {
|
|
29
|
+
email: normalizeEmailAddress(input.email),
|
|
30
|
+
deletedAt: null,
|
|
31
|
+
});
|
|
32
|
+
if (!admin || admin.status !== 'active') {
|
|
33
|
+
throw new HttpError(401, ERROR_CODES.UNAUTHORIZED, 'Invalid email or password.');
|
|
34
|
+
}
|
|
35
|
+
const ok = await verifyPassword(admin.passwordHash, input.password);
|
|
36
|
+
if (!ok) {
|
|
37
|
+
throw new HttpError(401, ERROR_CODES.UNAUTHORIZED, 'Invalid email or password.');
|
|
38
|
+
}
|
|
39
|
+
// Second factor (feature 042). No session until it succeeds.
|
|
40
|
+
const mfaPort = this.getMfaLoginPort?.();
|
|
41
|
+
if (mfaPort) {
|
|
42
|
+
const decision = await mfaPort.beginLogin({ subjectType: 'admin', subjectId: admin.id }, { salesChannelId: null, organizationId: null });
|
|
43
|
+
if (decision.kind === 'challenge') {
|
|
44
|
+
return { status: 'mfaRequired', challengeId: decision.challengeId };
|
|
45
|
+
}
|
|
46
|
+
if (decision.kind === 'setup') {
|
|
47
|
+
return { status: 'mfaSetupRequired', setupTicket: decision.setupTicket };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const session = await this.sessionPort.createSession({
|
|
51
|
+
kind: 'admin',
|
|
52
|
+
adminUserId: admin.id,
|
|
53
|
+
...(input.ip !== undefined ? { ipAddress: input.ip } : {}),
|
|
54
|
+
...(input.userAgent !== undefined ? { userAgent: input.userAgent } : {}),
|
|
55
|
+
});
|
|
56
|
+
// command-coverage-ignore: stamps lastLoginAt — high-volume auth bookkeeping
|
|
57
|
+
// (session lifecycle owned by SessionService), not an audited domain write.
|
|
58
|
+
admin.lastLoginAt = new Date();
|
|
59
|
+
await em.flush();
|
|
60
|
+
return {
|
|
61
|
+
status: 'authenticated',
|
|
62
|
+
adminUser: admin,
|
|
63
|
+
sessionCookieValue: session.cookieValue,
|
|
64
|
+
sessionExpiresAt: session.expiresAt,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
async changePassword(adminUserId, currentPassword, newPassword) {
|
|
68
|
+
const em = this.emFactory();
|
|
69
|
+
const admin = await em.findOne(AdminUser, { id: adminUserId, deletedAt: null });
|
|
70
|
+
if (!admin) {
|
|
71
|
+
throw new HttpError(401, ERROR_CODES.UNAUTHORIZED, 'Admin session required.');
|
|
72
|
+
}
|
|
73
|
+
const ok = await verifyPassword(admin.passwordHash, currentPassword);
|
|
74
|
+
if (!ok) {
|
|
75
|
+
throw new HttpError(401, ERROR_CODES.CURRENT_PASSWORD_INVALID, 'Current password is incorrect.');
|
|
76
|
+
}
|
|
77
|
+
admin.passwordHash = await hashPassword(newPassword);
|
|
78
|
+
if (this.auditLog) {
|
|
79
|
+
recordAuditFromContext(this.auditLog, em, {
|
|
80
|
+
action: 'admin_user.change_password',
|
|
81
|
+
objectType: 'admin_user',
|
|
82
|
+
objectId: admin.id,
|
|
83
|
+
stateBefore: null,
|
|
84
|
+
stateAfter: { via: 'self_service' },
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
await em.flush();
|
|
88
|
+
}
|
|
89
|
+
async logout(sessionId) {
|
|
90
|
+
await this.sessionPort.destroySession(sessionId);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=admin-auth-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-auth-service.js","sourceRoot":"","sources":["../../../src/backend/services/admin-auth-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { AdminPasswordVerificationPort, AdminUserLookupOptions, AdminUserPreferencePort, AdminUserReadPort, AdminUserRecord, ImpersonationPort } from '@endora-commerce/contracts';
|
|
3
|
+
import { AdminUser } from '../entities/admin-user.entity.js';
|
|
4
|
+
import type { AdminUserService } from './admin-user-service.js';
|
|
5
|
+
import type { ImpersonationService } from './impersonation-service.js';
|
|
6
|
+
import type { TwoFactorEnrolmentReader } from './two-factor-enrolments.js';
|
|
7
|
+
/**
|
|
8
|
+
* The published face of `admin_users` (feature 075, Phase P).
|
|
9
|
+
*
|
|
10
|
+
* Seven of the eleven inbound sites read the `AdminUser` entity to put a name
|
|
11
|
+
* beside an id: `admin_roles` resolving a user's role, `quote_requests`
|
|
12
|
+
* listing the admins a notification goes to, `catalog` attributing a bulk
|
|
13
|
+
* operation, `organizations` rendering the sales-rep picker.
|
|
14
|
+
*
|
|
15
|
+
* `passwordHash` does not survive the mapping, for the reason
|
|
16
|
+
* `CustomerAccountRecord` gives: exactly one module reads it.
|
|
17
|
+
*/
|
|
18
|
+
export declare class AdminUserReadService implements AdminUserReadPort {
|
|
19
|
+
private readonly emFactory;
|
|
20
|
+
private readonly twoFactorEnrolments;
|
|
21
|
+
constructor(emFactory: () => EntityManager, twoFactorEnrolments: TwoFactorEnrolmentReader);
|
|
22
|
+
findById(id: string, options?: AdminUserLookupOptions): Promise<AdminUserRecord | null>;
|
|
23
|
+
findByIds(ids: readonly string[], options?: AdminUserLookupOptions): Promise<AdminUserRecord[]>;
|
|
24
|
+
findByEmail(email: string, options?: AdminUserLookupOptions): Promise<AdminUserRecord | null>;
|
|
25
|
+
listAll(options?: AdminUserLookupOptions): Promise<AdminUserRecord[]>;
|
|
26
|
+
listByRoleId(adminRoleId: string): Promise<AdminUserRecord[]>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Step-up re-verification, for `mfa` (feature 080, T052).
|
|
30
|
+
*
|
|
31
|
+
* The read is `admin_users`' own row and the comparison is the platform
|
|
32
|
+
* hasher's, so both halves are on this side of the seam. The composition roots
|
|
33
|
+
* held them instead: each read `passwordHash` off the entity and called
|
|
34
|
+
* `verifyPassword` itself, which is a credential column and a hash comparison
|
|
35
|
+
* living in a file that owns neither.
|
|
36
|
+
*
|
|
37
|
+
* The hash never leaves this function — that is the difference between this
|
|
38
|
+
* port and a `passwordHash` field on `AdminUserRecord`, and it is the whole
|
|
39
|
+
* reason the port exists.
|
|
40
|
+
*/
|
|
41
|
+
export declare function createAdminPasswordVerificationPort(emFactory: () => EntityManager): AdminPasswordVerificationPort;
|
|
42
|
+
/** `_i18n` writes the admin's language choice and reads nothing else. */
|
|
43
|
+
export declare function createAdminUserPreferencePort(getService: () => AdminUserService, twoFactorEnrolments: TwoFactorEnrolmentReader): AdminUserPreferencePort;
|
|
44
|
+
/**
|
|
45
|
+
* `customers` hosts the "view as this customer" control, so the surface and
|
|
46
|
+
* the machinery sit in different modules by design.
|
|
47
|
+
*
|
|
48
|
+
* The only substitution the adapter makes is `impersonatedCustomerAccount`:
|
|
49
|
+
* the service answers with the `CustomerAccount` entity, and the port with the
|
|
50
|
+
* four fields the one consumer renders. Mapping to a full
|
|
51
|
+
* `CustomerAccountRecord` here would mean importing `customer_accounts`'
|
|
52
|
+
* mapper — a new cross-module edge, in the merge request whose job is to
|
|
53
|
+
* remove them.
|
|
54
|
+
*/
|
|
55
|
+
export declare function createImpersonationPort(getService: () => ImpersonationService): ImpersonationPort;
|
|
56
|
+
/**
|
|
57
|
+
* The published record, with `twoFactorEnabled` taken from `mfa` rather than
|
|
58
|
+
* from this module's `two_factor_confirmed_at` column.
|
|
59
|
+
*
|
|
60
|
+
* The second parameter is required and has no default on purpose. The column it
|
|
61
|
+
* replaces had no writer on this table — ever — so the field was a provably
|
|
62
|
+
* constant `false` on `/admin-users`, on `/admin/me` and on every consumer of
|
|
63
|
+
* this record. A default would have let the next call site reintroduce exactly
|
|
64
|
+
* that, silently; making the caller answer means the compiler asks.
|
|
65
|
+
*/
|
|
66
|
+
export declare function toAdminUserRecord(admin: AdminUser, twoFactorEnabled: boolean): AdminUserRecord;
|
|
67
|
+
/**
|
|
68
|
+
* One `mfa` read for a whole page of admins, rather than one per row.
|
|
69
|
+
*
|
|
70
|
+
* Every caller of this mapper is a list or a single entity, so batching here
|
|
71
|
+
* keeps `/admin-users` at two queries instead of one per user, and leaves the
|
|
72
|
+
* single-row callers spelling the same thing.
|
|
73
|
+
*/
|
|
74
|
+
export declare function toAdminUserRecords(twoFactorEnrolments: TwoFactorEnrolmentReader, admins: readonly AdminUser[]): Promise<AdminUserRecord[]>;
|
|
75
|
+
//# sourceMappingURL=admin-user-ports.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-user-ports.d.ts","sourceRoot":"","sources":["../../../src/backend/services/admin-user-ports.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,KAAK,EACV,6BAA6B,EAC7B,sBAAsB,EACtB,uBAAuB,EACvB,iBAAiB,EACjB,eAAe,EACf,iBAAiB,EAClB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAC7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AACvE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AAE3E;;;;;;;;;;GAUG;AACH,qBAAa,oBAAqB,YAAW,iBAAiB;IAE1D,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,mBAAmB;gBADnB,SAAS,EAAE,MAAM,aAAa,EAC9B,mBAAmB,EAAE,wBAAwB;IAG1D,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,sBAAsB,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAKvF,SAAS,CACb,GAAG,EAAE,SAAS,MAAM,EAAE,EACtB,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,eAAe,EAAE,CAAC;IASvB,WAAW,CACf,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAY5B,OAAO,CAAC,OAAO,CAAC,EAAE,sBAAsB,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IASrE,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;CAQpE;AAMD;;;;;;;;;;;;GAYG;AACH,wBAAgB,mCAAmC,CACjD,SAAS,EAAE,MAAM,aAAa,GAC7B,6BAA6B,CAS/B;AAED,yEAAyE;AACzE,wBAAgB,6BAA6B,CAC3C,UAAU,EAAE,MAAM,gBAAgB,EAClC,mBAAmB,EAAE,wBAAwB,GAC5C,uBAAuB,CAOzB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,MAAM,oBAAoB,GACrC,iBAAiB,CAmBnB;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,OAAO,GAAG,eAAe,CAe9F;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,mBAAmB,EAAE,wBAAwB,EAC7C,MAAM,EAAE,SAAS,SAAS,EAAE,GAC3B,OAAO,CAAC,eAAe,EAAE,CAAC,CAI5B"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { normalizeEmailAddress } from '@endora-commerce/contracts';
|
|
2
|
+
import { verifyPassword } from '@endora-commerce/platform/kernel';
|
|
3
|
+
import { AdminUser } from '../entities/admin-user.entity.js';
|
|
4
|
+
/**
|
|
5
|
+
* The published face of `admin_users` (feature 075, Phase P).
|
|
6
|
+
*
|
|
7
|
+
* Seven of the eleven inbound sites read the `AdminUser` entity to put a name
|
|
8
|
+
* beside an id: `admin_roles` resolving a user's role, `quote_requests`
|
|
9
|
+
* listing the admins a notification goes to, `catalog` attributing a bulk
|
|
10
|
+
* operation, `organizations` rendering the sales-rep picker.
|
|
11
|
+
*
|
|
12
|
+
* `passwordHash` does not survive the mapping, for the reason
|
|
13
|
+
* `CustomerAccountRecord` gives: exactly one module reads it.
|
|
14
|
+
*/
|
|
15
|
+
export class AdminUserReadService {
|
|
16
|
+
emFactory;
|
|
17
|
+
twoFactorEnrolments;
|
|
18
|
+
constructor(emFactory, twoFactorEnrolments) {
|
|
19
|
+
this.emFactory = emFactory;
|
|
20
|
+
this.twoFactorEnrolments = twoFactorEnrolments;
|
|
21
|
+
}
|
|
22
|
+
async findById(id, options) {
|
|
23
|
+
const admin = await this.emFactory().findOne(AdminUser, { id, ...activeFilter(options) });
|
|
24
|
+
return admin ? (await toAdminUserRecords(this.twoFactorEnrolments, [admin]))[0] : null;
|
|
25
|
+
}
|
|
26
|
+
async findByIds(ids, options) {
|
|
27
|
+
if (ids.length === 0)
|
|
28
|
+
return [];
|
|
29
|
+
const admins = await this.emFactory().find(AdminUser, {
|
|
30
|
+
id: { $in: [...ids] },
|
|
31
|
+
...activeFilter(options),
|
|
32
|
+
});
|
|
33
|
+
return toAdminUserRecords(this.twoFactorEnrolments, admins);
|
|
34
|
+
}
|
|
35
|
+
async findByEmail(email, options) {
|
|
36
|
+
// Folded before the comparison, like every other read of this column: the
|
|
37
|
+
// callers that reach this method arrive from entrances that go through no
|
|
38
|
+
// request schema — the bootstrap CLI, an identity provider's claim — so the
|
|
39
|
+
// spelling they hand over is whatever their source held.
|
|
40
|
+
const admin = await this.emFactory().findOne(AdminUser, {
|
|
41
|
+
email: normalizeEmailAddress(email),
|
|
42
|
+
...activeFilter(options),
|
|
43
|
+
});
|
|
44
|
+
return admin ? (await toAdminUserRecords(this.twoFactorEnrolments, [admin]))[0] : null;
|
|
45
|
+
}
|
|
46
|
+
async listAll(options) {
|
|
47
|
+
const admins = await this.emFactory().find(AdminUser, activeFilter(options), { orderBy: { email: 'asc' } });
|
|
48
|
+
return toAdminUserRecords(this.twoFactorEnrolments, admins);
|
|
49
|
+
}
|
|
50
|
+
async listByRoleId(adminRoleId) {
|
|
51
|
+
const admins = await this.emFactory().find(AdminUser, { adminRoleId }, { orderBy: { email: 'asc' } });
|
|
52
|
+
return toAdminUserRecords(this.twoFactorEnrolments, admins);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function activeFilter(options) {
|
|
56
|
+
return options?.activeOnly ? { deletedAt: null } : {};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Step-up re-verification, for `mfa` (feature 080, T052).
|
|
60
|
+
*
|
|
61
|
+
* The read is `admin_users`' own row and the comparison is the platform
|
|
62
|
+
* hasher's, so both halves are on this side of the seam. The composition roots
|
|
63
|
+
* held them instead: each read `passwordHash` off the entity and called
|
|
64
|
+
* `verifyPassword` itself, which is a credential column and a hash comparison
|
|
65
|
+
* living in a file that owns neither.
|
|
66
|
+
*
|
|
67
|
+
* The hash never leaves this function — that is the difference between this
|
|
68
|
+
* port and a `passwordHash` field on `AdminUserRecord`, and it is the whole
|
|
69
|
+
* reason the port exists.
|
|
70
|
+
*/
|
|
71
|
+
export function createAdminPasswordVerificationPort(emFactory) {
|
|
72
|
+
return {
|
|
73
|
+
async verifyPassword(adminUserId, password) {
|
|
74
|
+
const admin = await emFactory().findOne(AdminUser, { id: adminUserId });
|
|
75
|
+
// The bare call is the imported hasher: an object method name is not a
|
|
76
|
+
// binding, so it shadows nothing.
|
|
77
|
+
return admin ? verifyPassword(admin.passwordHash, password) : false;
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** `_i18n` writes the admin's language choice and reads nothing else. */
|
|
82
|
+
export function createAdminUserPreferencePort(getService, twoFactorEnrolments) {
|
|
83
|
+
return {
|
|
84
|
+
async setPreferredLanguage(id, preferredLanguage) {
|
|
85
|
+
const admin = await getService().setPreferredLanguage(id, preferredLanguage);
|
|
86
|
+
return (await toAdminUserRecords(twoFactorEnrolments, [admin]))[0];
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* `customers` hosts the "view as this customer" control, so the surface and
|
|
92
|
+
* the machinery sit in different modules by design.
|
|
93
|
+
*
|
|
94
|
+
* The only substitution the adapter makes is `impersonatedCustomerAccount`:
|
|
95
|
+
* the service answers with the `CustomerAccount` entity, and the port with the
|
|
96
|
+
* four fields the one consumer renders. Mapping to a full
|
|
97
|
+
* `CustomerAccountRecord` here would mean importing `customer_accounts`'
|
|
98
|
+
* mapper — a new cross-module edge, in the merge request whose job is to
|
|
99
|
+
* remove them.
|
|
100
|
+
*/
|
|
101
|
+
export function createImpersonationPort(getService) {
|
|
102
|
+
return {
|
|
103
|
+
async start(input) {
|
|
104
|
+
const started = await getService().start(input);
|
|
105
|
+
return {
|
|
106
|
+
impersonationSessionId: started.impersonationSessionId,
|
|
107
|
+
impersonationCookieValue: started.impersonationCookieValue,
|
|
108
|
+
impersonationExpiresAt: started.impersonationExpiresAt,
|
|
109
|
+
adminShadowSessionCookieValue: started.adminShadowSessionCookieValue,
|
|
110
|
+
impersonatedCustomerAccount: {
|
|
111
|
+
id: started.impersonatedCustomerAccount.id,
|
|
112
|
+
email: started.impersonatedCustomerAccount.email,
|
|
113
|
+
firstName: started.impersonatedCustomerAccount.firstName,
|
|
114
|
+
lastName: started.impersonatedCustomerAccount.lastName,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
end: (input) => getService().end(input),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The published record, with `twoFactorEnabled` taken from `mfa` rather than
|
|
123
|
+
* from this module's `two_factor_confirmed_at` column.
|
|
124
|
+
*
|
|
125
|
+
* The second parameter is required and has no default on purpose. The column it
|
|
126
|
+
* replaces had no writer on this table — ever — so the field was a provably
|
|
127
|
+
* constant `false` on `/admin-users`, on `/admin/me` and on every consumer of
|
|
128
|
+
* this record. A default would have let the next call site reintroduce exactly
|
|
129
|
+
* that, silently; making the caller answer means the compiler asks.
|
|
130
|
+
*/
|
|
131
|
+
export function toAdminUserRecord(admin, twoFactorEnabled) {
|
|
132
|
+
return {
|
|
133
|
+
id: admin.id,
|
|
134
|
+
email: admin.email,
|
|
135
|
+
firstName: admin.firstName,
|
|
136
|
+
lastName: admin.lastName,
|
|
137
|
+
adminRoleId: admin.adminRoleId ?? null,
|
|
138
|
+
status: admin.status,
|
|
139
|
+
twoFactorEnabled,
|
|
140
|
+
lastLoginAt: admin.lastLoginAt ?? null,
|
|
141
|
+
preferredLanguage: admin.preferredLanguage ?? null,
|
|
142
|
+
createdAt: admin.createdAt,
|
|
143
|
+
updatedAt: admin.updatedAt,
|
|
144
|
+
deletedAt: admin.deletedAt ?? null,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* One `mfa` read for a whole page of admins, rather than one per row.
|
|
149
|
+
*
|
|
150
|
+
* Every caller of this mapper is a list or a single entity, so batching here
|
|
151
|
+
* keeps `/admin-users` at two queries instead of one per user, and leaves the
|
|
152
|
+
* single-row callers spelling the same thing.
|
|
153
|
+
*/
|
|
154
|
+
export async function toAdminUserRecords(twoFactorEnrolments, admins) {
|
|
155
|
+
if (admins.length === 0)
|
|
156
|
+
return [];
|
|
157
|
+
const enrolled = await twoFactorEnrolments(admins.map((admin) => admin.id));
|
|
158
|
+
return admins.map((admin) => toAdminUserRecord(admin, enrolled.has(admin.id)));
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=admin-user-ports.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-user-ports.js","sourceRoot":"","sources":["../../../src/backend/services/admin-user-ports.ts"],"names":[],"mappings":"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"}
|