@open-mercato/core 0.4.2-canary-19353c5970 → 0.4.2-canary-19703ca707
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/dist/modules/auth/api/login.js +25 -6
- package/dist/modules/auth/api/login.js.map +2 -2
- package/dist/modules/auth/data/validators.js +2 -1
- package/dist/modules/auth/data/validators.js.map +2 -2
- package/dist/modules/auth/frontend/login.js +85 -1
- package/dist/modules/auth/frontend/login.js.map +2 -2
- package/dist/modules/auth/lib/setup-app.js +25 -12
- package/dist/modules/auth/lib/setup-app.js.map +2 -2
- package/dist/modules/auth/services/authService.js +21 -0
- package/dist/modules/auth/services/authService.js.map +2 -2
- package/dist/modules/business_rules/cli.js +2 -1
- package/dist/modules/business_rules/cli.js.map +2 -2
- package/dist/modules/directory/api/get/tenants/lookup.js +68 -0
- package/dist/modules/directory/api/get/tenants/lookup.js.map +7 -0
- package/dist/modules/workflows/cli.js +12 -12
- package/dist/modules/workflows/cli.js.map +2 -2
- package/package.json +2 -2
- package/src/modules/auth/api/__tests__/login.test.ts +2 -0
- package/src/modules/auth/api/login.ts +26 -7
- package/src/modules/auth/data/validators.ts +1 -0
- package/src/modules/auth/frontend/login.tsx +106 -2
- package/src/modules/auth/i18n/de.json +5 -0
- package/src/modules/auth/i18n/en.json +5 -0
- package/src/modules/auth/i18n/es.json +5 -0
- package/src/modules/auth/i18n/pl.json +5 -0
- package/src/modules/auth/lib/setup-app.ts +37 -15
- package/src/modules/auth/services/authService.ts +23 -0
- package/src/modules/business_rules/cli.ts +2 -1
- package/src/modules/directory/api/get/tenants/lookup.ts +73 -0
- package/src/modules/workflows/cli.ts +12 -12
|
@@ -11,15 +11,33 @@ async function POST(req) {
|
|
|
11
11
|
const email = String(form.get("email") ?? "");
|
|
12
12
|
const password = String(form.get("password") ?? "");
|
|
13
13
|
const remember = parseBooleanToken(form.get("remember")?.toString()) === true;
|
|
14
|
+
const tenantIdRaw = String(form.get("tenantId") ?? form.get("tenant") ?? "").trim();
|
|
14
15
|
const requireRoleRaw = String(form.get("requireRole") ?? form.get("role") ?? "").trim();
|
|
15
16
|
const requiredRoles = requireRoleRaw ? requireRoleRaw.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
16
|
-
const parsed = userLoginSchema.pick({ email: true, password: true }).safeParse({
|
|
17
|
+
const parsed = userLoginSchema.pick({ email: true, password: true, tenantId: true }).safeParse({
|
|
18
|
+
email,
|
|
19
|
+
password,
|
|
20
|
+
tenantId: tenantIdRaw || void 0
|
|
21
|
+
});
|
|
17
22
|
if (!parsed.success) {
|
|
18
23
|
return NextResponse.json({ ok: false, error: translate("auth.login.errors.invalidCredentials", "Invalid credentials") }, { status: 400 });
|
|
19
24
|
}
|
|
20
25
|
const container = await createRequestContainer();
|
|
21
26
|
const auth = container.resolve("authService");
|
|
22
|
-
const
|
|
27
|
+
const tenantId = parsed.data.tenantId ?? null;
|
|
28
|
+
let user = null;
|
|
29
|
+
if (tenantId) {
|
|
30
|
+
user = await auth.findUserByEmailAndTenant(parsed.data.email, tenantId);
|
|
31
|
+
} else {
|
|
32
|
+
const users = await auth.findUsersByEmail(parsed.data.email);
|
|
33
|
+
if (users.length > 1) {
|
|
34
|
+
return NextResponse.json({
|
|
35
|
+
ok: false,
|
|
36
|
+
error: translate("auth.login.errors.tenantRequired", "Use the login link provided with your tenant activation to continue.")
|
|
37
|
+
}, { status: 400 });
|
|
38
|
+
}
|
|
39
|
+
user = users[0] ?? null;
|
|
40
|
+
}
|
|
23
41
|
if (!user || !user.passwordHash) {
|
|
24
42
|
return NextResponse.json({ ok: false, error: translate("auth.login.errors.invalidCredentials", "Invalid email or password") }, { status: 401 });
|
|
25
43
|
}
|
|
@@ -28,24 +46,25 @@ async function POST(req) {
|
|
|
28
46
|
return NextResponse.json({ ok: false, error: translate("auth.login.errors.invalidCredentials", "Invalid email or password") }, { status: 401 });
|
|
29
47
|
}
|
|
30
48
|
if (requiredRoles.length) {
|
|
31
|
-
const userRoleNames2 = await auth.getUserRoles(user, user.tenantId ? String(user.tenantId) : null);
|
|
49
|
+
const userRoleNames2 = await auth.getUserRoles(user, tenantId ?? (user.tenantId ? String(user.tenantId) : null));
|
|
32
50
|
const authorized = requiredRoles.some((r) => userRoleNames2.includes(r));
|
|
33
51
|
if (!authorized) {
|
|
34
52
|
return NextResponse.json({ ok: false, error: translate("auth.login.errors.permissionDenied", "Not authorized for this area") }, { status: 403 });
|
|
35
53
|
}
|
|
36
54
|
}
|
|
37
55
|
await auth.updateLastLoginAt(user);
|
|
38
|
-
const
|
|
56
|
+
const resolvedTenantId = tenantId ?? (user.tenantId ? String(user.tenantId) : null);
|
|
57
|
+
const userRoleNames = await auth.getUserRoles(user, resolvedTenantId);
|
|
39
58
|
try {
|
|
40
59
|
const eventBus = container.resolve("eventBus");
|
|
41
60
|
void eventBus.emitEvent("query_index.coverage.warmup", {
|
|
42
|
-
tenantId:
|
|
61
|
+
tenantId: resolvedTenantId
|
|
43
62
|
}).catch(() => void 0);
|
|
44
63
|
} catch {
|
|
45
64
|
}
|
|
46
65
|
const token = signJwt({
|
|
47
66
|
sub: String(user.id),
|
|
48
|
-
tenantId:
|
|
67
|
+
tenantId: resolvedTenantId,
|
|
49
68
|
orgId: user.organizationId ? String(user.organizationId) : null,
|
|
50
69
|
email: user.email,
|
|
51
70
|
roles: userRoleNames
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/auth/api/login.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { userLoginSchema } from '@open-mercato/core/modules/auth/data/validators'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport { signJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { EventBus } from '@open-mercato/events/types'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\n\n// validation comes from userLoginSchema\n\nexport async function POST(req: Request) {\n const { translate } = await resolveTranslations()\n const form = await req.formData()\n const email = String(form.get('email') ?? '')\n const password = String(form.get('password') ?? '')\n const remember = parseBooleanToken(form.get('remember')?.toString()) === true\n const requireRoleRaw = (String(form.get('requireRole') ?? form.get('role') ?? '')).trim()\n const requiredRoles = requireRoleRaw ? requireRoleRaw.split(',').map((s) => s.trim()).filter(Boolean) : []\n const parsed = userLoginSchema.pick({ email: true, password: true }).safeParse({
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,uBAAuB;AAChC,SAAS,8BAA8B;AAEvC,SAAS,eAAe;AACxB,SAAS,2BAA2B;AAEpC,SAAS,yBAAyB;AAIlC,eAAsB,KAAK,KAAc;AACvC,QAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,QAAM,OAAO,MAAM,IAAI,SAAS;AAChC,QAAM,QAAQ,OAAO,KAAK,IAAI,OAAO,KAAK,EAAE;AAC5C,QAAM,WAAW,OAAO,KAAK,IAAI,UAAU,KAAK,EAAE;AAClD,QAAM,WAAW,kBAAkB,KAAK,IAAI,UAAU,GAAG,SAAS,CAAC,MAAM;AACzE,QAAM,iBAAkB,OAAO,KAAK,IAAI,aAAa,KAAK,KAAK,IAAI,MAAM,KAAK,EAAE,EAAG,KAAK;AACxF,QAAM,gBAAgB,iBAAiB,eAAe,MAAM,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,OAAO,IAAI,CAAC;AACzG,QAAM,SAAS,gBAAgB,KAAK,EAAE,OAAO,MAAM,UAAU,KAAK,CAAC,EAAE,UAAU,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { userLoginSchema } from '@open-mercato/core/modules/auth/data/validators'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport { signJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { EventBus } from '@open-mercato/events/types'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\n\n// validation comes from userLoginSchema\n\nexport async function POST(req: Request) {\n const { translate } = await resolveTranslations()\n const form = await req.formData()\n const email = String(form.get('email') ?? '')\n const password = String(form.get('password') ?? '')\n const remember = parseBooleanToken(form.get('remember')?.toString()) === true\n const tenantIdRaw = String(form.get('tenantId') ?? form.get('tenant') ?? '').trim()\n const requireRoleRaw = (String(form.get('requireRole') ?? form.get('role') ?? '')).trim()\n const requiredRoles = requireRoleRaw ? requireRoleRaw.split(',').map((s) => s.trim()).filter(Boolean) : []\n const parsed = userLoginSchema.pick({ email: true, password: true, tenantId: true }).safeParse({\n email,\n password,\n tenantId: tenantIdRaw || undefined,\n })\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: translate('auth.login.errors.invalidCredentials', 'Invalid credentials') }, { status: 400 })\n }\n const container = await createRequestContainer()\n const auth = (container.resolve('authService') as AuthService)\n const tenantId = parsed.data.tenantId ?? null\n let user = null\n if (tenantId) {\n user = await auth.findUserByEmailAndTenant(parsed.data.email, tenantId)\n } else {\n const users = await auth.findUsersByEmail(parsed.data.email)\n if (users.length > 1) {\n return NextResponse.json({\n ok: false,\n error: translate('auth.login.errors.tenantRequired', 'Use the login link provided with your tenant activation to continue.'),\n }, { status: 400 })\n }\n user = users[0] ?? null\n }\n if (!user || !user.passwordHash) {\n return NextResponse.json({ ok: false, error: translate('auth.login.errors.invalidCredentials', 'Invalid email or password') }, { status: 401 })\n }\n const ok = await auth.verifyPassword(user, parsed.data.password)\n if (!ok) {\n return NextResponse.json({ ok: false, error: translate('auth.login.errors.invalidCredentials', 'Invalid email or password') }, { status: 401 })\n }\n // Optional role requirement\n if (requiredRoles.length) {\n const userRoleNames = await auth.getUserRoles(user, tenantId ?? (user.tenantId ? String(user.tenantId) : null))\n const authorized = requiredRoles.some(r => userRoleNames.includes(r))\n if (!authorized) {\n return NextResponse.json({ ok: false, error: translate('auth.login.errors.permissionDenied', 'Not authorized for this area') }, { status: 403 })\n }\n }\n await auth.updateLastLoginAt(user)\n const resolvedTenantId = tenantId ?? (user.tenantId ? String(user.tenantId) : null)\n const userRoleNames = await auth.getUserRoles(user, resolvedTenantId)\n try {\n const eventBus = (container.resolve('eventBus') as EventBus)\n void eventBus.emitEvent('query_index.coverage.warmup', {\n tenantId: resolvedTenantId,\n }).catch(() => undefined)\n } catch {\n // optional warmup\n }\n const token = signJwt({ \n sub: String(user.id), \n tenantId: resolvedTenantId, \n orgId: user.organizationId ? String(user.organizationId) : null,\n email: user.email, \n roles: userRoleNames \n })\n const res = NextResponse.json({ ok: true, token, redirect: '/backend' })\n res.cookies.set('auth_token', token, { httpOnly: true, path: '/', sameSite: 'lax', secure: process.env.NODE_ENV === 'production', maxAge: 60 * 60 * 8 })\n if (remember) {\n const days = Number(process.env.REMEMBER_ME_DAYS || '30')\n const expiresAt = new Date(Date.now() + days * 24 * 60 * 60 * 1000)\n const sess = await auth.createSession(user, expiresAt)\n res.cookies.set('session_token', sess.token, { httpOnly: true, path: '/', sameSite: 'lax', secure: process.env.NODE_ENV === 'production', expires: expiresAt })\n }\n return res\n}\n\nconst loginRequestSchema = userLoginSchema.extend({\n password: z.string().min(6).describe('User password'),\n remember: z.enum(['on', '1', 'true']).optional().describe('Persist the session (submit `on`, `1`, or `true`).'),\n}).describe('Login form payload')\n\nconst loginSuccessSchema = z.object({\n ok: z.literal(true),\n token: z.string().describe('JWT token issued for subsequent API calls'),\n redirect: z.string().nullable().describe('Next location the client should navigate to'),\n})\n\nconst loginErrorSchema = z.object({\n ok: z.literal(false),\n error: z.string(),\n})\n\nconst loginMethodDoc: OpenApiMethodDoc = {\n summary: 'Authenticate user credentials',\n description: 'Validates the submitted credentials and issues a bearer token cookie for subsequent API calls.',\n tags: ['Authentication & Accounts'],\n requestBody: {\n contentType: 'application/x-www-form-urlencoded',\n schema: loginRequestSchema,\n description: 'Form-encoded payload captured from the login form.',\n },\n responses: [\n {\n status: 200,\n description: 'Authentication succeeded',\n schema: loginSuccessSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: loginErrorSchema },\n { status: 401, description: 'Invalid credentials', schema: loginErrorSchema },\n { status: 403, description: 'User lacks required role', schema: loginErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Authenticate user credentials',\n description: 'Accepts login form submissions and manages cookie/session issuance.',\n methods: {\n POST: loginMethodDoc,\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["userRoleNames"]
|
|
7
7
|
}
|
|
@@ -4,7 +4,8 @@ const passwordSchema = buildPasswordSchema();
|
|
|
4
4
|
const userLoginSchema = z.object({
|
|
5
5
|
email: z.string().email(),
|
|
6
6
|
password: z.string().min(6),
|
|
7
|
-
requireRole: z.string().optional()
|
|
7
|
+
requireRole: z.string().optional(),
|
|
8
|
+
tenantId: z.string().uuid().optional()
|
|
8
9
|
});
|
|
9
10
|
const requestPasswordResetSchema = z.object({
|
|
10
11
|
email: z.string().email()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/auth/data/validators.ts"],
|
|
4
|
-
"sourcesContent": ["import { z } from 'zod'\nimport { buildPasswordSchema } from '@open-mercato/shared/lib/auth/passwordPolicy'\n\nconst passwordSchema = buildPasswordSchema()\n\n// Core auth validators\nexport const userLoginSchema = z.object({\n email: z.string().email(),\n password: z.string().min(6),\n requireRole: z.string().optional(),\n})\n\nexport const requestPasswordResetSchema = z.object({\n email: z.string().email(),\n})\n\nexport const confirmPasswordResetSchema = z.object({\n token: z.string().min(10),\n password: passwordSchema,\n})\n\nexport const sidebarPreferencesInputSchema = z.object({\n version: z.number().int().positive().optional(),\n groupOrder: z.array(z.string().min(1)).max(200).optional(),\n groupLabels: z.record(z.string().min(1), z.string().min(1).max(120)).optional(),\n itemLabels: z.record(z.string().min(1), z.string().min(1).max(120)).optional(),\n hiddenItems: z.array(z.string().min(1)).max(500).optional(),\n applyToRoles: z.array(z.string().uuid()).optional(),\n clearRoleIds: z.array(z.string().uuid()).optional(),\n})\n\n// Optional helpers for CLI or admin forms\nexport const userCreateSchema = z.object({\n email: z.string().email(),\n password: passwordSchema,\n tenantId: z.string().uuid().optional(),\n organizationId: z.string().uuid(),\n rolesCsv: z.string().optional(),\n})\n\nexport type UserLoginInput = z.infer<typeof userLoginSchema>\nexport type RequestPasswordResetInput = z.infer<typeof requestPasswordResetSchema>\nexport type ConfirmPasswordResetInput = z.infer<typeof confirmPasswordResetSchema>\nexport type SidebarPreferencesInput = z.infer<typeof sidebarPreferencesInputSchema>\nexport type UserCreateInput = z.infer<typeof userCreateSchema>\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,2BAA2B;AAEpC,MAAM,iBAAiB,oBAAoB;AAGpC,MAAM,kBAAkB,EAAE,OAAO;AAAA,EACtC,OAAO,EAAE,OAAO,EAAE,MAAM;AAAA,EACxB,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC1B,aAAa,EAAE,OAAO,EAAE,SAAS;
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod'\nimport { buildPasswordSchema } from '@open-mercato/shared/lib/auth/passwordPolicy'\n\nconst passwordSchema = buildPasswordSchema()\n\n// Core auth validators\nexport const userLoginSchema = z.object({\n email: z.string().email(),\n password: z.string().min(6),\n requireRole: z.string().optional(),\n tenantId: z.string().uuid().optional(),\n})\n\nexport const requestPasswordResetSchema = z.object({\n email: z.string().email(),\n})\n\nexport const confirmPasswordResetSchema = z.object({\n token: z.string().min(10),\n password: passwordSchema,\n})\n\nexport const sidebarPreferencesInputSchema = z.object({\n version: z.number().int().positive().optional(),\n groupOrder: z.array(z.string().min(1)).max(200).optional(),\n groupLabels: z.record(z.string().min(1), z.string().min(1).max(120)).optional(),\n itemLabels: z.record(z.string().min(1), z.string().min(1).max(120)).optional(),\n hiddenItems: z.array(z.string().min(1)).max(500).optional(),\n applyToRoles: z.array(z.string().uuid()).optional(),\n clearRoleIds: z.array(z.string().uuid()).optional(),\n})\n\n// Optional helpers for CLI or admin forms\nexport const userCreateSchema = z.object({\n email: z.string().email(),\n password: passwordSchema,\n tenantId: z.string().uuid().optional(),\n organizationId: z.string().uuid(),\n rolesCsv: z.string().optional(),\n})\n\nexport type UserLoginInput = z.infer<typeof userLoginSchema>\nexport type RequestPasswordResetInput = z.infer<typeof requestPasswordResetSchema>\nexport type ConfirmPasswordResetInput = z.infer<typeof confirmPasswordResetSchema>\nexport type SidebarPreferencesInput = z.infer<typeof sidebarPreferencesInputSchema>\nexport type UserCreateInput = z.infer<typeof userCreateSchema>\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,2BAA2B;AAEpC,MAAM,iBAAiB,oBAAoB;AAGpC,MAAM,kBAAkB,EAAE,OAAO;AAAA,EACtC,OAAO,EAAE,OAAO,EAAE,MAAM;AAAA,EACxB,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC1B,aAAa,EAAE,OAAO,EAAE,SAAS;AAAA,EACjC,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AACvC,CAAC;AAEM,MAAM,6BAA6B,EAAE,OAAO;AAAA,EACjD,OAAO,EAAE,OAAO,EAAE,MAAM;AAC1B,CAAC;AAEM,MAAM,6BAA6B,EAAE,OAAO;AAAA,EACjD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE;AAAA,EACxB,UAAU;AACZ,CAAC;AAEM,MAAM,gCAAgC,EAAE,OAAO;AAAA,EACpD,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS;AAAA,EAC9C,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EACzD,aAAa,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,CAAC,EAAE,SAAS;AAAA,EAC9E,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,CAAC,EAAE,SAAS;AAAA,EAC7E,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EAC1D,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,SAAS;AAAA,EAClD,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,SAAS;AACpD,CAAC;AAGM,MAAM,mBAAmB,EAAE,OAAO;AAAA,EACvC,OAAO,EAAE,OAAO,EAAE,MAAM;AAAA,EACxB,UAAU;AAAA,EACV,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EACrC,gBAAgB,EAAE,OAAO,EAAE,KAAK;AAAA,EAChC,UAAU,EAAE,OAAO,EAAE,SAAS;AAChC,CAAC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,15 +1,36 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useState } from "react";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
4
|
import Image from "next/image";
|
|
5
5
|
import Link from "next/link";
|
|
6
6
|
import { useRouter, useSearchParams } from "next/navigation";
|
|
7
7
|
import { Card, CardContent, CardHeader, CardDescription } from "@open-mercato/ui/primitives/card";
|
|
8
8
|
import { Input } from "@open-mercato/ui/primitives/input";
|
|
9
9
|
import { Label } from "@open-mercato/ui/primitives/label";
|
|
10
|
+
import { Button } from "@open-mercato/ui/primitives/button";
|
|
10
11
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
11
12
|
import { translateWithFallback } from "@open-mercato/shared/lib/i18n/translate";
|
|
12
13
|
import { clearAllOperations } from "@open-mercato/ui/backend/operations/store";
|
|
14
|
+
import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
|
|
15
|
+
const loginTenantKey = "om_login_tenant";
|
|
16
|
+
const loginTenantCookieMaxAge = 60 * 60 * 24 * 14;
|
|
17
|
+
function readTenantCookie() {
|
|
18
|
+
if (typeof document === "undefined") return null;
|
|
19
|
+
const entries = document.cookie.split(";");
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
const [name, ...rest] = entry.trim().split("=");
|
|
22
|
+
if (name === loginTenantKey) return decodeURIComponent(rest.join("="));
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
function setTenantCookie(value) {
|
|
27
|
+
if (typeof document === "undefined") return;
|
|
28
|
+
document.cookie = `${loginTenantKey}=${encodeURIComponent(value)}; path=/; max-age=${loginTenantCookieMaxAge}; samesite=lax`;
|
|
29
|
+
}
|
|
30
|
+
function clearTenantCookie() {
|
|
31
|
+
if (typeof document === "undefined") return;
|
|
32
|
+
document.cookie = `${loginTenantKey}=; path=/; max-age=0; samesite=lax`;
|
|
33
|
+
}
|
|
13
34
|
function extractErrorMessage(payload) {
|
|
14
35
|
if (!payload) return null;
|
|
15
36
|
if (typeof payload === "string") return payload;
|
|
@@ -53,6 +74,62 @@ function LoginPage() {
|
|
|
53
74
|
const translatedFeatures = requiredFeatures.map((feature) => translate(`features.${feature}`, feature));
|
|
54
75
|
const [error, setError] = useState(null);
|
|
55
76
|
const [submitting, setSubmitting] = useState(false);
|
|
77
|
+
const [tenantId, setTenantId] = useState(null);
|
|
78
|
+
const [tenantName, setTenantName] = useState(null);
|
|
79
|
+
const [tenantLoading, setTenantLoading] = useState(false);
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
const tenantParam = (searchParams.get("tenant") || "").trim();
|
|
82
|
+
if (tenantParam) {
|
|
83
|
+
setTenantId(tenantParam);
|
|
84
|
+
window.localStorage.setItem(loginTenantKey, tenantParam);
|
|
85
|
+
setTenantCookie(tenantParam);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const storedTenant = window.localStorage.getItem(loginTenantKey) || readTenantCookie();
|
|
89
|
+
if (storedTenant) {
|
|
90
|
+
setTenantId(storedTenant);
|
|
91
|
+
}
|
|
92
|
+
}, [searchParams]);
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (!tenantId) {
|
|
95
|
+
setTenantName(null);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
let active = true;
|
|
99
|
+
setTenantLoading(true);
|
|
100
|
+
apiCall(
|
|
101
|
+
`/api/directory/tenants/lookup?tenantId=${encodeURIComponent(tenantId)}`
|
|
102
|
+
).then(({ result }) => {
|
|
103
|
+
if (!active) return;
|
|
104
|
+
if (result?.ok && result.tenant) {
|
|
105
|
+
setTenantName(result.tenant.name);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const message = translate("auth.login.errors.tenantInvalid", "Tenant not found. Clear the tenant selection and try again.");
|
|
109
|
+
setTenantName(null);
|
|
110
|
+
setError(message);
|
|
111
|
+
}).catch(() => {
|
|
112
|
+
if (!active) return;
|
|
113
|
+
setTenantName(null);
|
|
114
|
+
setError(translate("auth.login.errors.tenantInvalid", "Tenant not found. Clear the tenant selection and try again."));
|
|
115
|
+
}).finally(() => {
|
|
116
|
+
if (active) setTenantLoading(false);
|
|
117
|
+
});
|
|
118
|
+
return () => {
|
|
119
|
+
active = false;
|
|
120
|
+
};
|
|
121
|
+
}, [tenantId, translate]);
|
|
122
|
+
function handleClearTenant() {
|
|
123
|
+
window.localStorage.removeItem(loginTenantKey);
|
|
124
|
+
clearTenantCookie();
|
|
125
|
+
setTenantId(null);
|
|
126
|
+
setTenantName(null);
|
|
127
|
+
const params = new URLSearchParams(searchParams);
|
|
128
|
+
params.delete("tenant");
|
|
129
|
+
setError(null);
|
|
130
|
+
const query = params.toString();
|
|
131
|
+
router.replace(query ? `/login?${query}` : "/login");
|
|
132
|
+
}
|
|
56
133
|
async function onSubmit(e) {
|
|
57
134
|
e.preventDefault();
|
|
58
135
|
setError(null);
|
|
@@ -130,6 +207,7 @@ function LoginPage() {
|
|
|
130
207
|
/* @__PURE__ */ jsx(CardDescription, { children: translate("auth.login.subtitle", "Access your workspace") })
|
|
131
208
|
] }),
|
|
132
209
|
/* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsxs("form", { className: "grid gap-3", onSubmit, noValidate: true, children: [
|
|
210
|
+
tenantId ? /* @__PURE__ */ jsx("input", { type: "hidden", name: "tenantId", value: tenantId }) : null,
|
|
133
211
|
!!translatedRoles.length && /* @__PURE__ */ jsx("div", { className: "rounded-md border border-blue-200 bg-blue-50 px-3 py-2 text-center text-xs text-blue-900", children: translate(
|
|
134
212
|
translatedRoles.length > 1 ? "auth.login.requireRolesMessage" : "auth.login.requireRoleMessage",
|
|
135
213
|
translatedRoles.length > 1 ? "Access requires one of the following roles: {roles}" : "Access requires role: {roles}",
|
|
@@ -138,6 +216,12 @@ function LoginPage() {
|
|
|
138
216
|
!!translatedFeatures.length && /* @__PURE__ */ jsx("div", { className: "rounded-md border border-blue-200 bg-blue-50 px-3 py-2 text-center text-xs text-blue-900", children: translate("auth.login.featureDenied", "You don't have access to this feature ({feature}). Please contact your administrator.", {
|
|
139
217
|
feature: translatedFeatures.join(", ")
|
|
140
218
|
}) }),
|
|
219
|
+
tenantId && /* @__PURE__ */ jsxs("div", { className: "rounded-md border border-emerald-200 bg-emerald-50 px-3 py-2 text-center text-xs text-emerald-900", children: [
|
|
220
|
+
/* @__PURE__ */ jsx("div", { className: "font-medium", children: tenantLoading ? translate("auth.login.tenantLoading", "Loading tenant details...") : translate("auth.login.tenantBanner", "You're logging in to {tenant} tenant.", {
|
|
221
|
+
tenant: tenantName || tenantId
|
|
222
|
+
}) }),
|
|
223
|
+
/* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "mt-2 text-emerald-900", onClick: handleClearTenant, children: translate("auth.login.tenantClear", "Clear") })
|
|
224
|
+
] }),
|
|
141
225
|
error && /* @__PURE__ */ jsx("div", { className: "rounded-md border border-red-200 bg-red-50 px-3 py-2 text-center text-sm text-red-700", role: "alert", "aria-live": "polite", children: error }),
|
|
142
226
|
/* @__PURE__ */ jsxs("div", { className: "grid gap-1", children: [
|
|
143
227
|
/* @__PURE__ */ jsx(Label, { htmlFor: "email", children: t("auth.email") }),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/auth/frontend/login.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport { useState } from 'react'\nimport Image from 'next/image'\nimport Link from 'next/link'\nimport { useRouter, useSearchParams } from 'next/navigation'\nimport { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@open-mercato/ui/primitives/card'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'\nimport { clearAllOperations } from '@open-mercato/ui/backend/operations/store'\n\nfunction extractErrorMessage(payload: unknown): string | null {\n if (!payload) return null\n if (typeof payload === 'string') return payload\n if (Array.isArray(payload)) {\n for (const entry of payload) {\n const resolved = extractErrorMessage(entry)\n if (resolved) return resolved\n }\n return null\n }\n if (typeof payload === 'object') {\n const record = payload as Record<string, unknown>\n const candidates: unknown[] = [\n record.error,\n record.message,\n record.detail,\n record.details,\n record.description,\n ]\n for (const candidate of candidates) {\n const resolved = extractErrorMessage(candidate)\n if (resolved) return resolved\n }\n }\n return null\n}\n\nfunction looksLikeJsonString(value: string): boolean {\n const trimmed = value.trim()\n return trimmed.startsWith('{') || trimmed.startsWith('[')\n}\n\nexport default function LoginPage() {\n const t = useT()\n const translate = (key: string, fallback: string, params?: Record<string, string | number>) =>\n translateWithFallback(t, key, fallback, params)\n const router = useRouter()\n const searchParams = useSearchParams()\n const requireRole = (searchParams.get('requireRole') || searchParams.get('role') || '').trim()\n const requireFeature = (searchParams.get('requireFeature') || '').trim()\n const requiredRoles = requireRole ? requireRole.split(',').map((value) => value.trim()).filter(Boolean) : []\n const requiredFeatures = requireFeature ? requireFeature.split(',').map((value) => value.trim()).filter(Boolean) : []\n const translatedRoles = requiredRoles.map((role) => translate(`auth.roles.${role}`, role))\n const translatedFeatures = requiredFeatures.map((feature) => translate(`features.${feature}`, feature))\n const [error, setError] = useState<string | null>(null)\n const [submitting, setSubmitting] = useState(false)\n\n async function onSubmit(e: React.FormEvent<HTMLFormElement>) {\n e.preventDefault()\n setError(null)\n setSubmitting(true)\n try {\n const form = new FormData(e.currentTarget)\n if (requiredRoles.length) form.set('requireRole', requiredRoles.join(','))\n const res = await fetch('/api/auth/login', { method: 'POST', body: form })\n if (res.redirected) {\n clearAllOperations()\n // NextResponse.redirect from API\n router.replace(res.url)\n return\n }\n if (!res.ok) {\n const fallback = (() => {\n if (res.status === 403) {\n return translate(\n 'auth.login.errors.permissionDenied',\n 'You do not have permission to access this area. Please contact your administrator.',\n )\n }\n if (res.status === 401 || res.status === 400) {\n return translate('auth.login.errors.invalidCredentials', 'Invalid email or password')\n }\n return translate('auth.login.errors.generic', 'An error occurred. Please try again.')\n })()\n const cloned = res.clone()\n let errorMessage = ''\n const contentType = res.headers.get('content-type') || ''\n if (contentType.includes('application/json')) {\n try {\n const data = await res.json()\n errorMessage = extractErrorMessage(data) || ''\n } catch {\n try {\n const text = await cloned.text()\n const trimmed = text.trim()\n if (trimmed && !looksLikeJsonString(trimmed)) {\n errorMessage = trimmed\n }\n } catch {\n errorMessage = ''\n }\n }\n } else {\n try {\n const text = await res.text()\n const trimmed = text.trim()\n if (trimmed && !looksLikeJsonString(trimmed)) {\n errorMessage = trimmed\n }\n } catch {\n errorMessage = ''\n }\n }\n setError(errorMessage || fallback)\n return\n }\n // In case API returns 200 with JSON\n const data = await res.json().catch(() => null)\n clearAllOperations()\n if (data && data.redirect) {\n router.replace(data.redirect)\n }\n } catch (err: unknown) {\n // Handle any errors thrown (e.g., network errors or thrown exceptions)\n const message = err instanceof Error ? err.message : ''\n setError(message || translate('auth.login.errors.generic', 'An error occurred. Please try again.'))\n } finally {\n setSubmitting(false)\n }\n }\n\n return (\n <div className=\"min-h-svh flex items-center justify-center p-4\">\n <Card className=\"w-full max-w-sm\">\n <CardHeader className=\"flex flex-col items-center gap-4 text-center p-10\">\n <Image alt={translate('auth.login.logoAlt', 'Open Mercato logo')} src=\"/open-mercato.svg\" width={150} height={150} priority />\n <h1 className=\"text-2xl font-semibold\">{translate('auth.login.brandName', 'Open Mercato')}</h1>\n <CardDescription>{translate('auth.login.subtitle', 'Access your workspace')}</CardDescription>\n </CardHeader>\n <CardContent>\n <form className=\"grid gap-3\" onSubmit={onSubmit} noValidate>\n {!!translatedRoles.length && (\n <div className=\"rounded-md border border-blue-200 bg-blue-50 px-3 py-2 text-center text-xs text-blue-900\">\n {translate(\n translatedRoles.length > 1 ? 'auth.login.requireRolesMessage' : 'auth.login.requireRoleMessage',\n translatedRoles.length > 1\n ? 'Access requires one of the following roles: {roles}'\n : 'Access requires role: {roles}',\n { roles: translatedRoles.join(', ') },\n )}\n </div>\n )}\n {!!translatedFeatures.length && (\n <div className=\"rounded-md border border-blue-200 bg-blue-50 px-3 py-2 text-center text-xs text-blue-900\">\n {translate('auth.login.featureDenied', \"You don't have access to this feature ({feature}). Please contact your administrator.\", {\n feature: translatedFeatures.join(', '),\n })}\n </div>\n )}\n {error && (\n <div className=\"rounded-md border border-red-200 bg-red-50 px-3 py-2 text-center text-sm text-red-700\" role=\"alert\" aria-live=\"polite\">\n {error}\n </div>\n )}\n <div className=\"grid gap-1\">\n <Label htmlFor=\"email\">{t('auth.email')}</Label>\n <Input id=\"email\" name=\"email\" type=\"email\" required aria-invalid={!!error} />\n </div>\n <div className=\"grid gap-1\">\n <Label htmlFor=\"password\">{t('auth.password')}</Label>\n <Input id=\"password\" name=\"password\" type=\"password\" required aria-invalid={!!error} />\n </div>\n <label className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <input type=\"checkbox\" name=\"remember\" className=\"accent-foreground\" />\n <span>{translate('auth.login.rememberMe', 'Remember me')}</span>\n </label>\n <button disabled={submitting} className=\"h-10 rounded-md bg-foreground text-background mt-2 hover:opacity-90 transition disabled:opacity-60\">\n {submitting ? translate('auth.login.loading', 'Loading...') : translate('auth.signIn', 'Sign in')}\n </button>\n <div className=\"text-xs text-muted-foreground mt-2\">\n <Link className=\"underline\" href=\"/reset\">\n {translate('auth.login.forgotPassword', 'Forgot password?')}\n </Link>\n </div>\n </form>\n </CardContent>\n </Card>\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport { useEffect, useState } from 'react'\nimport Image from 'next/image'\nimport Link from 'next/link'\nimport { useRouter, useSearchParams } from 'next/navigation'\nimport { Card, CardContent, CardHeader, CardDescription } from '@open-mercato/ui/primitives/card'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'\nimport { clearAllOperations } from '@open-mercato/ui/backend/operations/store'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\n\nconst loginTenantKey = 'om_login_tenant'\nconst loginTenantCookieMaxAge = 60 * 60 * 24 * 14\n\nfunction readTenantCookie() {\n if (typeof document === 'undefined') return null\n const entries = document.cookie.split(';')\n for (const entry of entries) {\n const [name, ...rest] = entry.trim().split('=')\n if (name === loginTenantKey) return decodeURIComponent(rest.join('='))\n }\n return null\n}\n\nfunction setTenantCookie(value: string) {\n if (typeof document === 'undefined') return\n document.cookie = `${loginTenantKey}=${encodeURIComponent(value)}; path=/; max-age=${loginTenantCookieMaxAge}; samesite=lax`\n}\n\nfunction clearTenantCookie() {\n if (typeof document === 'undefined') return\n document.cookie = `${loginTenantKey}=; path=/; max-age=0; samesite=lax`\n}\n\nfunction extractErrorMessage(payload: unknown): string | null {\n if (!payload) return null\n if (typeof payload === 'string') return payload\n if (Array.isArray(payload)) {\n for (const entry of payload) {\n const resolved = extractErrorMessage(entry)\n if (resolved) return resolved\n }\n return null\n }\n if (typeof payload === 'object') {\n const record = payload as Record<string, unknown>\n const candidates: unknown[] = [\n record.error,\n record.message,\n record.detail,\n record.details,\n record.description,\n ]\n for (const candidate of candidates) {\n const resolved = extractErrorMessage(candidate)\n if (resolved) return resolved\n }\n }\n return null\n}\n\nfunction looksLikeJsonString(value: string): boolean {\n const trimmed = value.trim()\n return trimmed.startsWith('{') || trimmed.startsWith('[')\n}\n\nexport default function LoginPage() {\n const t = useT()\n const translate = (key: string, fallback: string, params?: Record<string, string | number>) =>\n translateWithFallback(t, key, fallback, params)\n const router = useRouter()\n const searchParams = useSearchParams()\n const requireRole = (searchParams.get('requireRole') || searchParams.get('role') || '').trim()\n const requireFeature = (searchParams.get('requireFeature') || '').trim()\n const requiredRoles = requireRole ? requireRole.split(',').map((value) => value.trim()).filter(Boolean) : []\n const requiredFeatures = requireFeature ? requireFeature.split(',').map((value) => value.trim()).filter(Boolean) : []\n const translatedRoles = requiredRoles.map((role) => translate(`auth.roles.${role}`, role))\n const translatedFeatures = requiredFeatures.map((feature) => translate(`features.${feature}`, feature))\n const [error, setError] = useState<string | null>(null)\n const [submitting, setSubmitting] = useState(false)\n const [tenantId, setTenantId] = useState<string | null>(null)\n const [tenantName, setTenantName] = useState<string | null>(null)\n const [tenantLoading, setTenantLoading] = useState(false)\n\n useEffect(() => {\n const tenantParam = (searchParams.get('tenant') || '').trim()\n if (tenantParam) {\n setTenantId(tenantParam)\n window.localStorage.setItem(loginTenantKey, tenantParam)\n setTenantCookie(tenantParam)\n return\n }\n const storedTenant = window.localStorage.getItem(loginTenantKey) || readTenantCookie()\n if (storedTenant) {\n setTenantId(storedTenant)\n }\n }, [searchParams])\n\n useEffect(() => {\n if (!tenantId) {\n setTenantName(null)\n return\n }\n let active = true\n setTenantLoading(true)\n apiCall<{ ok: boolean; tenant?: { id: string; name: string }; error?: string }>(\n `/api/directory/tenants/lookup?tenantId=${encodeURIComponent(tenantId)}`,\n )\n .then(({ result }) => {\n if (!active) return\n if (result?.ok && result.tenant) {\n setTenantName(result.tenant.name)\n return\n }\n const message = translate('auth.login.errors.tenantInvalid', 'Tenant not found. Clear the tenant selection and try again.')\n setTenantName(null)\n setError(message)\n })\n .catch(() => {\n if (!active) return\n setTenantName(null)\n setError(translate('auth.login.errors.tenantInvalid', 'Tenant not found. Clear the tenant selection and try again.'))\n })\n .finally(() => {\n if (active) setTenantLoading(false)\n })\n return () => {\n active = false\n }\n }, [tenantId, translate])\n\n function handleClearTenant() {\n window.localStorage.removeItem(loginTenantKey)\n clearTenantCookie()\n setTenantId(null)\n setTenantName(null)\n const params = new URLSearchParams(searchParams)\n params.delete('tenant')\n setError(null)\n const query = params.toString()\n router.replace(query ? `/login?${query}` : '/login')\n }\n\n async function onSubmit(e: React.FormEvent<HTMLFormElement>) {\n e.preventDefault()\n setError(null)\n setSubmitting(true)\n try {\n const form = new FormData(e.currentTarget)\n if (requiredRoles.length) form.set('requireRole', requiredRoles.join(','))\n const res = await fetch('/api/auth/login', { method: 'POST', body: form })\n if (res.redirected) {\n clearAllOperations()\n // NextResponse.redirect from API\n router.replace(res.url)\n return\n }\n if (!res.ok) {\n const fallback = (() => {\n if (res.status === 403) {\n return translate(\n 'auth.login.errors.permissionDenied',\n 'You do not have permission to access this area. Please contact your administrator.',\n )\n }\n if (res.status === 401 || res.status === 400) {\n return translate('auth.login.errors.invalidCredentials', 'Invalid email or password')\n }\n return translate('auth.login.errors.generic', 'An error occurred. Please try again.')\n })()\n const cloned = res.clone()\n let errorMessage = ''\n const contentType = res.headers.get('content-type') || ''\n if (contentType.includes('application/json')) {\n try {\n const data = await res.json()\n errorMessage = extractErrorMessage(data) || ''\n } catch {\n try {\n const text = await cloned.text()\n const trimmed = text.trim()\n if (trimmed && !looksLikeJsonString(trimmed)) {\n errorMessage = trimmed\n }\n } catch {\n errorMessage = ''\n }\n }\n } else {\n try {\n const text = await res.text()\n const trimmed = text.trim()\n if (trimmed && !looksLikeJsonString(trimmed)) {\n errorMessage = trimmed\n }\n } catch {\n errorMessage = ''\n }\n }\n setError(errorMessage || fallback)\n return\n }\n // In case API returns 200 with JSON\n const data = await res.json().catch(() => null)\n clearAllOperations()\n if (data && data.redirect) {\n router.replace(data.redirect)\n }\n } catch (err: unknown) {\n // Handle any errors thrown (e.g., network errors or thrown exceptions)\n const message = err instanceof Error ? err.message : ''\n setError(message || translate('auth.login.errors.generic', 'An error occurred. Please try again.'))\n } finally {\n setSubmitting(false)\n }\n }\n\n return (\n <div className=\"min-h-svh flex items-center justify-center p-4\">\n <Card className=\"w-full max-w-sm\">\n <CardHeader className=\"flex flex-col items-center gap-4 text-center p-10\">\n <Image alt={translate('auth.login.logoAlt', 'Open Mercato logo')} src=\"/open-mercato.svg\" width={150} height={150} priority />\n <h1 className=\"text-2xl font-semibold\">{translate('auth.login.brandName', 'Open Mercato')}</h1>\n <CardDescription>{translate('auth.login.subtitle', 'Access your workspace')}</CardDescription>\n </CardHeader>\n <CardContent>\n <form className=\"grid gap-3\" onSubmit={onSubmit} noValidate>\n {tenantId ? (\n <input type=\"hidden\" name=\"tenantId\" value={tenantId} />\n ) : null}\n {!!translatedRoles.length && (\n <div className=\"rounded-md border border-blue-200 bg-blue-50 px-3 py-2 text-center text-xs text-blue-900\">\n {translate(\n translatedRoles.length > 1 ? 'auth.login.requireRolesMessage' : 'auth.login.requireRoleMessage',\n translatedRoles.length > 1\n ? 'Access requires one of the following roles: {roles}'\n : 'Access requires role: {roles}',\n { roles: translatedRoles.join(', ') },\n )}\n </div>\n )}\n {!!translatedFeatures.length && (\n <div className=\"rounded-md border border-blue-200 bg-blue-50 px-3 py-2 text-center text-xs text-blue-900\">\n {translate('auth.login.featureDenied', \"You don't have access to this feature ({feature}). Please contact your administrator.\", {\n feature: translatedFeatures.join(', '),\n })}\n </div>\n )}\n {tenantId && (\n <div className=\"rounded-md border border-emerald-200 bg-emerald-50 px-3 py-2 text-center text-xs text-emerald-900\">\n <div className=\"font-medium\">\n {tenantLoading\n ? translate('auth.login.tenantLoading', 'Loading tenant details...')\n : translate('auth.login.tenantBanner', \"You're logging in to {tenant} tenant.\", {\n tenant: tenantName || tenantId,\n })}\n </div>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" className=\"mt-2 text-emerald-900\" onClick={handleClearTenant}>\n {translate('auth.login.tenantClear', 'Clear')}\n </Button>\n </div>\n )}\n {error && (\n <div className=\"rounded-md border border-red-200 bg-red-50 px-3 py-2 text-center text-sm text-red-700\" role=\"alert\" aria-live=\"polite\">\n {error}\n </div>\n )}\n <div className=\"grid gap-1\">\n <Label htmlFor=\"email\">{t('auth.email')}</Label>\n <Input id=\"email\" name=\"email\" type=\"email\" required aria-invalid={!!error} />\n </div>\n <div className=\"grid gap-1\">\n <Label htmlFor=\"password\">{t('auth.password')}</Label>\n <Input id=\"password\" name=\"password\" type=\"password\" required aria-invalid={!!error} />\n </div>\n <label className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <input type=\"checkbox\" name=\"remember\" className=\"accent-foreground\" />\n <span>{translate('auth.login.rememberMe', 'Remember me')}</span>\n </label>\n <button disabled={submitting} className=\"h-10 rounded-md bg-foreground text-background mt-2 hover:opacity-90 transition disabled:opacity-60\">\n {submitting ? translate('auth.login.loading', 'Loading...') : translate('auth.signIn', 'Sign in')}\n </button>\n <div className=\"text-xs text-muted-foreground mt-2\">\n <Link className=\"underline\" href=\"/reset\">\n {translate('auth.login.forgotPassword', 'Forgot password?')}\n </Link>\n </div>\n </form>\n </CardContent>\n </Card>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["data"]
|
|
7
7
|
}
|
|
@@ -15,6 +15,7 @@ import { DEFAULT_ENCRYPTION_MAPS } from "@open-mercato/core/modules/entities/lib
|
|
|
15
15
|
import { createKmsService } from "@open-mercato/shared/lib/encryption/kms";
|
|
16
16
|
import { TenantDataEncryptionService } from "@open-mercato/shared/lib/encryption/tenantDataEncryptionService";
|
|
17
17
|
import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
18
|
+
import { parseBooleanToken } from "@open-mercato/shared/lib/boolean";
|
|
18
19
|
const DEFAULT_ROLE_NAMES = ["employee", "admin", "superadmin"];
|
|
19
20
|
const DEMO_SUPERADMIN_EMAIL = "superadmin@acme.com";
|
|
20
21
|
async function ensureRolesInContext(em, roleNames, tenantId) {
|
|
@@ -114,16 +115,17 @@ async function setupInitialTenant(em, options) {
|
|
|
114
115
|
{ email: primaryUser.email, roles: primaryRoles, name: resolvePrimaryName(primaryUser) }
|
|
115
116
|
];
|
|
116
117
|
if (includeDerivedUsers) {
|
|
117
|
-
const [
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
118
|
+
const [, domain] = String(primaryUser.email).split("@");
|
|
119
|
+
const adminOverride = readEnvValue(DERIVED_EMAIL_ENV.admin);
|
|
120
|
+
const employeeOverride = readEnvValue(DERIVED_EMAIL_ENV.employee);
|
|
121
|
+
const adminEmail = adminOverride ?? (domain ? `admin@${domain}` : "");
|
|
122
|
+
const employeeEmail = employeeOverride ?? (domain ? `employee@${domain}` : "");
|
|
123
|
+
const adminPassword = readEnvValue("OM_INIT_ADMIN_PASSWORD");
|
|
124
|
+
const employeePassword = readEnvValue("OM_INIT_EMPLOYEE_PASSWORD");
|
|
125
|
+
const adminPasswordHash = adminPassword ? await resolvePasswordHash({ email: adminEmail, password: adminPassword }) : null;
|
|
126
|
+
const employeePasswordHash = employeePassword ? await resolvePasswordHash({ email: employeeEmail, password: employeePassword }) : null;
|
|
127
|
+
addUniqueBaseUser(baseUsers, { email: adminEmail, roles: ["admin"], passwordHash: adminPasswordHash });
|
|
128
|
+
addUniqueBaseUser(baseUsers, { email: employeeEmail, roles: ["employee"], passwordHash: employeePasswordHash });
|
|
127
129
|
}
|
|
128
130
|
const passwordHash = await resolvePasswordHash(primaryUser);
|
|
129
131
|
await em.transactional(async (tem) => {
|
|
@@ -205,11 +207,12 @@ async function setupInitialTenant(em, options) {
|
|
|
205
207
|
await encryptionService.invalidateMap("auth:user", String(tenantId), null);
|
|
206
208
|
}
|
|
207
209
|
for (const base of baseUsers) {
|
|
210
|
+
const resolvedPasswordHash = base.passwordHash ?? passwordHash;
|
|
208
211
|
let user = await tem.findOne(User, { email: base.email });
|
|
209
212
|
const confirm = primaryUser.confirm ?? true;
|
|
210
213
|
const encryptedPayload = encryptionService ? await encryptionService.encryptEntityPayload("auth:user", { email: base.email }, tenantId, organizationId) : { email: base.email, emailHash: computeEmailHash(base.email) };
|
|
211
214
|
if (user) {
|
|
212
|
-
user.passwordHash =
|
|
215
|
+
user.passwordHash = resolvedPasswordHash;
|
|
213
216
|
user.organizationId = organizationId;
|
|
214
217
|
user.tenantId = tenantId;
|
|
215
218
|
if (isTenantDataEncryptionEnabled()) {
|
|
@@ -224,7 +227,7 @@ async function setupInitialTenant(em, options) {
|
|
|
224
227
|
user = tem.create(User, {
|
|
225
228
|
email: encryptedPayload.email ?? base.email,
|
|
226
229
|
emailHash: isTenantDataEncryptionEnabled() ? encryptedPayload.emailHash ?? computeEmailHash(base.email) : void 0,
|
|
227
|
-
passwordHash,
|
|
230
|
+
passwordHash: resolvedPasswordHash,
|
|
228
231
|
organizationId,
|
|
229
232
|
tenantId,
|
|
230
233
|
name: base.name ?? void 0,
|
|
@@ -278,6 +281,15 @@ function addUniqueBaseUser(baseUsers, entry) {
|
|
|
278
281
|
if (baseUsers.some((user) => user.email.toLowerCase() === normalized)) return;
|
|
279
282
|
baseUsers.push(entry);
|
|
280
283
|
}
|
|
284
|
+
function isDemoModeEnabled() {
|
|
285
|
+
const parsed = parseBooleanToken(process.env.DEMO_MODE ?? "");
|
|
286
|
+
return parsed === false ? false : true;
|
|
287
|
+
}
|
|
288
|
+
function shouldKeepDemoSuperadminDuringInit() {
|
|
289
|
+
if (process.env.OM_INIT_FLOW !== "true") return false;
|
|
290
|
+
if (!readEnvValue("OM_INIT_SUPERADMIN_EMAIL")) return false;
|
|
291
|
+
return isDemoModeEnabled();
|
|
292
|
+
}
|
|
281
293
|
async function resolvePasswordHash(input) {
|
|
282
294
|
if (typeof input.hashedPassword === "string") return input.hashedPassword;
|
|
283
295
|
if (input.password) return hash(input.password, 10);
|
|
@@ -407,6 +419,7 @@ async function ensureRoleAclFor(em, role, tenantId, features, options = {}) {
|
|
|
407
419
|
}
|
|
408
420
|
async function deactivateDemoSuperAdminIfSelfOnboardingEnabled(em) {
|
|
409
421
|
if (process.env.SELF_SERVICE_ONBOARDING_ENABLED !== "true") return;
|
|
422
|
+
if (shouldKeepDemoSuperadminDuringInit()) return;
|
|
410
423
|
try {
|
|
411
424
|
const user = await em.findOne(User, { email: DEMO_SUPERADMIN_EMAIL });
|
|
412
425
|
if (!user) return;
|