@open-mercato/core 0.8.1-develop.7241.1.fad7b2ce2a → 0.8.1-develop.7261.1.5d42353f61
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/.turbo/turbo-build.log +1 -1
- package/dist/modules/attachments/lib/storage.js +15 -1
- package/dist/modules/attachments/lib/storage.js.map +2 -2
- package/dist/modules/auth/api/reset/confirm.js +3 -2
- package/dist/modules/auth/api/reset/confirm.js.map +2 -2
- package/dist/modules/auth/api/reset.js +2 -1
- package/dist/modules/auth/api/reset.js.map +2 -2
- package/dist/modules/auth/di.js +6 -6
- package/dist/modules/auth/di.js.map +2 -2
- package/dist/modules/auth/lib/tenantHint.js +29 -0
- package/dist/modules/auth/lib/tenantHint.js.map +7 -0
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js +11 -3
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/users.js +27 -14
- package/dist/modules/customer_accounts/api/admin/users.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js +9 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/PortalUserDetailPageClient.js +8 -8
- package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/PortalUserDetailPageClient.js.map +2 -2
- package/dist/modules/customer_accounts/di.js +3 -3
- package/dist/modules/customer_accounts/di.js.map +2 -2
- package/dist/modules/customer_accounts/lib/customerAuth.js +18 -7
- package/dist/modules/customer_accounts/lib/customerAuth.js.map +2 -2
- package/dist/modules/customer_accounts/services/customerSessionService.js +23 -0
- package/dist/modules/customer_accounts/services/customerSessionService.js.map +2 -2
- package/dist/modules/customers/ai-tools/activities-tasks-pack.js +28 -24
- package/dist/modules/customers/ai-tools/activities-tasks-pack.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/map/page.js +10 -2
- package/dist/modules/customers/backend/customers/deals/map/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +10 -2
- package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js +41 -66
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js +6 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
- package/dist/modules/customers/commands/companies.js +5 -0
- package/dist/modules/customers/commands/companies.js.map +2 -2
- package/dist/modules/customers/components/formConfig.js +3 -3
- package/dist/modules/customers/components/formConfig.js.map +2 -2
- package/dist/modules/customers/data/validators.js +3 -1
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/data_sync/lib/sync-engine.js +3 -7
- package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
- package/dist/modules/data_sync/lib/sync-schedule-service.js +52 -3
- package/dist/modules/data_sync/lib/sync-schedule-service.js.map +2 -2
- package/dist/modules/progress/lib/progressServiceImpl.js +1 -1
- package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/attachments/lib/storage.ts +24 -1
- package/src/modules/auth/api/reset/confirm.ts +3 -2
- package/src/modules/auth/api/reset.ts +2 -1
- package/src/modules/auth/di.ts +6 -6
- package/src/modules/auth/lib/tenantHint.ts +49 -0
- package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +19 -5
- package/src/modules/catalog/i18n/de.json +1 -0
- package/src/modules/catalog/i18n/en.json +1 -0
- package/src/modules/catalog/i18n/es.json +1 -0
- package/src/modules/catalog/i18n/ko.json +1 -0
- package/src/modules/catalog/i18n/pl.json +1 -0
- package/src/modules/customer_accounts/api/admin/users.ts +30 -14
- package/src/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.tsx +11 -0
- package/src/modules/customer_accounts/backend/customer_accounts/users/[id]/PortalUserDetailPageClient.tsx +11 -11
- package/src/modules/customer_accounts/di.ts +3 -3
- package/src/modules/customer_accounts/i18n/de.json +1 -0
- package/src/modules/customer_accounts/i18n/en.json +1 -0
- package/src/modules/customer_accounts/i18n/es.json +1 -0
- package/src/modules/customer_accounts/i18n/ko.json +1 -0
- package/src/modules/customer_accounts/i18n/pl.json +1 -0
- package/src/modules/customer_accounts/lib/customerAuth.ts +36 -7
- package/src/modules/customer_accounts/services/customerSessionService.ts +28 -0
- package/src/modules/customers/ai-tools/activities-tasks-pack.ts +49 -39
- package/src/modules/customers/backend/customers/deals/map/page.tsx +13 -2
- package/src/modules/customers/backend/customers/deals/page.tsx +13 -2
- package/src/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.tsx +69 -56
- package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +8 -2
- package/src/modules/customers/commands/companies.ts +8 -0
- package/src/modules/customers/components/formConfig.tsx +3 -3
- package/src/modules/customers/data/validators.ts +2 -1
- package/src/modules/customers/i18n/de.json +1 -0
- package/src/modules/customers/i18n/en.json +1 -0
- package/src/modules/customers/i18n/es.json +1 -0
- package/src/modules/customers/i18n/ko.json +1 -0
- package/src/modules/customers/i18n/pl.json +1 -0
- package/src/modules/data_sync/AGENTS.md +1 -1
- package/src/modules/data_sync/lib/sync-engine.ts +23 -12
- package/src/modules/data_sync/lib/sync-schedule-service.ts +54 -3
- package/src/modules/progress/lib/progressServiceImpl.ts +7 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -3,13 +3,25 @@ import path from "path";
|
|
|
3
3
|
import { randomUUID } from "crypto";
|
|
4
4
|
import { resolvePartitionEnvKey } from "./partitionEnv.js";
|
|
5
5
|
import { resolveContainedPath, resolveLegacyPublicRoot } from "./pathContainment.js";
|
|
6
|
+
const STORAGE_ROOT_ENV_KEY = "ATTACHMENTS_STORAGE_ROOT";
|
|
7
|
+
function resolveStorageRoot() {
|
|
8
|
+
const envPath = process.env[STORAGE_ROOT_ENV_KEY];
|
|
9
|
+
if (envPath && envPath.trim().length > 0) {
|
|
10
|
+
const trimmed = envPath.trim();
|
|
11
|
+
if (!path.isAbsolute(trimmed)) {
|
|
12
|
+
throw new Error(`[internal] ${STORAGE_ROOT_ENV_KEY} must be an absolute path, received: ${trimmed}`);
|
|
13
|
+
}
|
|
14
|
+
return path.resolve(trimmed);
|
|
15
|
+
}
|
|
16
|
+
return path.join(process.cwd(), "storage", "attachments");
|
|
17
|
+
}
|
|
6
18
|
function resolvePartitionRoot(code) {
|
|
7
19
|
const envKey = resolvePartitionEnvKey(code);
|
|
8
20
|
const envPath = process.env[envKey];
|
|
9
21
|
if (envPath && envPath.trim().length > 0) {
|
|
10
22
|
return path.resolve(envPath);
|
|
11
23
|
}
|
|
12
|
-
return path.join(
|
|
24
|
+
return path.join(resolveStorageRoot(), code);
|
|
13
25
|
}
|
|
14
26
|
function sanitizeFileName(fileName) {
|
|
15
27
|
if (!fileName) return "file";
|
|
@@ -55,9 +67,11 @@ async function deletePartitionFile(partitionCode, storagePath, storageDriver) {
|
|
|
55
67
|
}
|
|
56
68
|
}
|
|
57
69
|
export {
|
|
70
|
+
STORAGE_ROOT_ENV_KEY,
|
|
58
71
|
deletePartitionFile,
|
|
59
72
|
resolveAttachmentAbsolutePath,
|
|
60
73
|
resolvePartitionRoot,
|
|
74
|
+
resolveStorageRoot,
|
|
61
75
|
storePartitionFile
|
|
62
76
|
};
|
|
63
77
|
//# sourceMappingURL=storage.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/attachments/lib/storage.ts"],
|
|
4
|
-
"sourcesContent": ["import { promises as fs } from 'fs'\nimport path from 'path'\nimport { randomUUID } from 'crypto'\nimport { resolvePartitionEnvKey } from './partitionEnv'\nimport { resolveContainedPath, resolveLegacyPublicRoot } from './pathContainment'\n\nexport function resolvePartitionRoot(code: string): string {\n const envKey = resolvePartitionEnvKey(code)\n const envPath = process.env[envKey]\n if (envPath && envPath.trim().length > 0) {\n return path.resolve(envPath)\n }\n return path.join(
|
|
5
|
-
"mappings": "AAAA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AACjB,SAAS,kBAAkB;AAC3B,SAAS,8BAA8B;AACvC,SAAS,sBAAsB,+BAA+B;AAEvD,SAAS,qBAAqB,MAAsB;AACzD,QAAM,SAAS,uBAAuB,IAAI;AAC1C,QAAM,UAAU,QAAQ,IAAI,MAAM;AAClC,MAAI,WAAW,QAAQ,KAAK,EAAE,SAAS,GAAG;AACxC,WAAO,KAAK,QAAQ,OAAO;AAAA,EAC7B;AACA,SAAO,KAAK,KAAK,
|
|
4
|
+
"sourcesContent": ["import { promises as fs } from 'fs'\nimport path from 'path'\nimport { randomUUID } from 'crypto'\nimport { resolvePartitionEnvKey } from './partitionEnv'\nimport { resolveContainedPath, resolveLegacyPublicRoot } from './pathContainment'\n\nexport const STORAGE_ROOT_ENV_KEY = 'ATTACHMENTS_STORAGE_ROOT'\n\n/**\n * Resolves the base directory that holds every partition directory. Deployment\n * configuration (`ATTACHMENTS_STORAGE_ROOT`) wins over the historical\n * `process.cwd()`-relative default, so the same attachment row resolves to the\n * same file regardless of the directory the process was started from. A\n * relative value is rejected rather than resolved against `process.cwd()`,\n * because `LocalStorageDriver.store()` creates missing directories and a typo\n * would otherwise silently produce a second, empty store.\n */\nexport function resolveStorageRoot(): string {\n const envPath = process.env[STORAGE_ROOT_ENV_KEY]\n if (envPath && envPath.trim().length > 0) {\n const trimmed = envPath.trim()\n if (!path.isAbsolute(trimmed)) {\n throw new Error(`[internal] ${STORAGE_ROOT_ENV_KEY} must be an absolute path, received: ${trimmed}`)\n }\n return path.resolve(trimmed)\n }\n return path.join(process.cwd(), 'storage', 'attachments')\n}\n\nexport function resolvePartitionRoot(code: string): string {\n const envKey = resolvePartitionEnvKey(code)\n const envPath = process.env[envKey]\n if (envPath && envPath.trim().length > 0) {\n return path.resolve(envPath)\n }\n return path.join(resolveStorageRoot(), code)\n}\n\nfunction sanitizeFileName(fileName: string): string {\n if (!fileName) return 'file'\n return fileName.replace(/[^a-zA-Z0-9._-]/g, '_')\n}\n\nfunction resolveOrgSegment(orgId: string | null | undefined): string {\n if (typeof orgId === 'string' && orgId.trim().length > 0) return `org_${orgId}`\n return 'org_shared'\n}\n\nfunction resolveTenantSegment(tenantId: string | null | undefined): string {\n if (typeof tenantId === 'string' && tenantId.trim().length > 0) return `tenant_${tenantId}`\n return 'tenant_shared'\n}\n\n/**\n * @deprecated Use `StorageDriverFactory.resolveForPartition()` + `driver.store()` instead.\n * Kept for backward compatibility with external callers.\n */\nexport type StorePartitionFilePayload = {\n partitionCode: string\n orgId: string | null | undefined\n tenantId: string | null | undefined\n fileName: string\n buffer: Buffer\n}\n\nexport type StoredPartitionFile = {\n storagePath: string\n absolutePath: string\n fileName: string\n}\n\n/**\n * @deprecated Use `StorageDriverFactory.resolveForPartition()` + `driver.store()` instead.\n */\nexport async function storePartitionFile(payload: StorePartitionFilePayload): Promise<StoredPartitionFile> {\n const root = resolvePartitionRoot(payload.partitionCode)\n const orgSegment = resolveOrgSegment(payload.orgId ?? null)\n const tenantSegment = resolveTenantSegment(payload.tenantId ?? null)\n const safeName = sanitizeFileName(payload.fileName || 'file')\n const uniqueSuffix = randomUUID().replace(/-/g, '').slice(0, 12)\n const storedName = `${Date.now()}_${uniqueSuffix}_${safeName}`\n const relativePath = path.join(orgSegment, tenantSegment, storedName)\n const absolutePath = path.join(root, relativePath)\n await fs.mkdir(path.dirname(absolutePath), { recursive: true })\n await fs.writeFile(absolutePath, payload.buffer)\n return {\n storagePath: relativePath.replace(/\\\\/g, '/'),\n absolutePath,\n fileName: storedName,\n }\n}\n\n/**\n * @deprecated Use `StorageDriverFactory.resolveForAttachment()` + `driver.read()` / `driver.toLocalPath()` instead.\n */\nexport function resolveAttachmentAbsolutePath(\n partitionCode: string,\n storagePath: string,\n storageDriver?: string | null\n): string {\n if (storageDriver === 'legacyPublic') {\n return resolveContainedPath(process.cwd(), storagePath, resolveLegacyPublicRoot())\n }\n const root = resolvePartitionRoot(partitionCode)\n return resolveContainedPath(root, storagePath)\n}\n\n/**\n * @deprecated Use `StorageDriverFactory.resolveForAttachment()` + `driver.delete()` instead.\n */\nexport async function deletePartitionFile(\n partitionCode: string,\n storagePath: string,\n storageDriver?: string | null\n): Promise<void> {\n const absolutePath = resolveAttachmentAbsolutePath(partitionCode, storagePath, storageDriver)\n try {\n await fs.unlink(absolutePath)\n } catch {\n // best-effort removal\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AACjB,SAAS,kBAAkB;AAC3B,SAAS,8BAA8B;AACvC,SAAS,sBAAsB,+BAA+B;AAEvD,MAAM,uBAAuB;AAW7B,SAAS,qBAA6B;AAC3C,QAAM,UAAU,QAAQ,IAAI,oBAAoB;AAChD,MAAI,WAAW,QAAQ,KAAK,EAAE,SAAS,GAAG;AACxC,UAAM,UAAU,QAAQ,KAAK;AAC7B,QAAI,CAAC,KAAK,WAAW,OAAO,GAAG;AAC7B,YAAM,IAAI,MAAM,cAAc,oBAAoB,wCAAwC,OAAO,EAAE;AAAA,IACrG;AACA,WAAO,KAAK,QAAQ,OAAO;AAAA,EAC7B;AACA,SAAO,KAAK,KAAK,QAAQ,IAAI,GAAG,WAAW,aAAa;AAC1D;AAEO,SAAS,qBAAqB,MAAsB;AACzD,QAAM,SAAS,uBAAuB,IAAI;AAC1C,QAAM,UAAU,QAAQ,IAAI,MAAM;AAClC,MAAI,WAAW,QAAQ,KAAK,EAAE,SAAS,GAAG;AACxC,WAAO,KAAK,QAAQ,OAAO;AAAA,EAC7B;AACA,SAAO,KAAK,KAAK,mBAAmB,GAAG,IAAI;AAC7C;AAEA,SAAS,iBAAiB,UAA0B;AAClD,MAAI,CAAC,SAAU,QAAO;AACtB,SAAO,SAAS,QAAQ,oBAAoB,GAAG;AACjD;AAEA,SAAS,kBAAkB,OAA0C;AACnE,MAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS,EAAG,QAAO,OAAO,KAAK;AAC7E,SAAO;AACT;AAEA,SAAS,qBAAqB,UAA6C;AACzE,MAAI,OAAO,aAAa,YAAY,SAAS,KAAK,EAAE,SAAS,EAAG,QAAO,UAAU,QAAQ;AACzF,SAAO;AACT;AAuBA,eAAsB,mBAAmB,SAAkE;AACzG,QAAM,OAAO,qBAAqB,QAAQ,aAAa;AACvD,QAAM,aAAa,kBAAkB,QAAQ,SAAS,IAAI;AAC1D,QAAM,gBAAgB,qBAAqB,QAAQ,YAAY,IAAI;AACnE,QAAM,WAAW,iBAAiB,QAAQ,YAAY,MAAM;AAC5D,QAAM,eAAe,WAAW,EAAE,QAAQ,MAAM,EAAE,EAAE,MAAM,GAAG,EAAE;AAC/D,QAAM,aAAa,GAAG,KAAK,IAAI,CAAC,IAAI,YAAY,IAAI,QAAQ;AAC5D,QAAM,eAAe,KAAK,KAAK,YAAY,eAAe,UAAU;AACpE,QAAM,eAAe,KAAK,KAAK,MAAM,YAAY;AACjD,QAAM,GAAG,MAAM,KAAK,QAAQ,YAAY,GAAG,EAAE,WAAW,KAAK,CAAC;AAC9D,QAAM,GAAG,UAAU,cAAc,QAAQ,MAAM;AAC/C,SAAO;AAAA,IACL,aAAa,aAAa,QAAQ,OAAO,GAAG;AAAA,IAC5C;AAAA,IACA,UAAU;AAAA,EACZ;AACF;AAKO,SAAS,8BACd,eACA,aACA,eACQ;AACR,MAAI,kBAAkB,gBAAgB;AACpC,WAAO,qBAAqB,QAAQ,IAAI,GAAG,aAAa,wBAAwB,CAAC;AAAA,EACnF;AACA,QAAM,OAAO,qBAAqB,aAAa;AAC/C,SAAO,qBAAqB,MAAM,WAAW;AAC/C;AAKA,eAAsB,oBACpB,eACA,aACA,eACe;AACf,QAAM,eAAe,8BAA8B,eAAe,aAAa,aAAa;AAC5F,MAAI;AACF,UAAM,GAAG,OAAO,YAAY;AAAA,EAC9B,QAAQ;AAAA,EAER;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -10,6 +10,7 @@ import { readEndpointRateLimitConfig } from "@open-mercato/shared/lib/ratelimit/
|
|
|
10
10
|
import { checkAuthRateLimit } from "@open-mercato/core/modules/auth/lib/rateLimitCheck";
|
|
11
11
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
12
12
|
import { emitAuthEvent } from "@open-mercato/core/modules/auth/events";
|
|
13
|
+
import { withTenantHintPath } from "@open-mercato/core/modules/auth/lib/tenantHint";
|
|
13
14
|
const logger = createLogger("auth").child({ component: "reset-confirm" });
|
|
14
15
|
const resetConfirmRateLimitConfig = readEndpointRateLimitConfig("RESET_CONFIRM", {
|
|
15
16
|
points: 5,
|
|
@@ -54,7 +55,7 @@ async function POST(req) {
|
|
|
54
55
|
} catch (err) {
|
|
55
56
|
logger.error("Failed to create notification", { err });
|
|
56
57
|
}
|
|
57
|
-
return NextResponse.json({ ok: true, redirect: "/login" });
|
|
58
|
+
return NextResponse.json({ ok: true, redirect: withTenantHintPath("/login", user.tenantId) });
|
|
58
59
|
}
|
|
59
60
|
const metadata = { requireAuth: false };
|
|
60
61
|
const passwordResetConfirmResponseSchema = z.object({
|
|
@@ -71,7 +72,7 @@ const openApi = {
|
|
|
71
72
|
methods: {
|
|
72
73
|
POST: {
|
|
73
74
|
summary: "Complete password reset",
|
|
74
|
-
description: "Validates the reset token and updates the user password.",
|
|
75
|
+
description: "Validates the reset token and updates the user password. The returned `redirect` carries the resolved user's tenant as a navigation hint (`/login?tenant=...`) so a tenant-bound user lands on the tenant login entry; tenantless users get `/login`.",
|
|
75
76
|
requestBody: {
|
|
76
77
|
contentType: "application/x-www-form-urlencoded",
|
|
77
78
|
schema: confirmPasswordResetSchema
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/auth/api/reset/confirm.ts"],
|
|
4
|
-
"sourcesContent": ["import { confirmPasswordResetSchema } from '@open-mercato/core/modules/auth/data/validators'\nimport { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport { buildNotificationFromType } from '@open-mercato/core/modules/notifications/lib/notificationBuilder'\nimport { resolveNotificationService } from '@open-mercato/core/modules/notifications/lib/notificationService'\nimport notificationTypes from '@open-mercato/core/modules/auth/notifications'\nimport { z } from 'zod'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'\nimport { checkAuthRateLimit } from '@open-mercato/core/modules/auth/lib/rateLimitCheck'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { emitAuthEvent } from '@open-mercato/core/modules/auth/events'\n\nconst logger = createLogger('auth').child({ component: 'reset-confirm' })\n\nconst resetConfirmRateLimitConfig = readEndpointRateLimitConfig('RESET_CONFIRM', {\n points: 5, duration: 300, keyPrefix: 'reset-confirm',\n})\n\n// validation via confirmPasswordResetSchema\n\nexport async function POST(req: Request) {\n const form = await req.formData()\n const token = String(form.get('token') ?? '')\n const password = String(form.get('password') ?? '')\n // Rate limit by IP \u2014 checked before validation and DB work\n const { error: rateLimitError } = await checkAuthRateLimit({ req, ipConfig: resetConfirmRateLimitConfig })\n if (rateLimitError) return rateLimitError\n const parsed = confirmPasswordResetSchema.safeParse({ token, password })\n if (!parsed.success) return NextResponse.json({ ok: false, error: 'Invalid request' }, { status: 400 })\n const c = await createRequestContainer()\n const auth = c.resolve<AuthService>('authService')\n const user = await auth.confirmPasswordReset(parsed.data.token, parsed.data.password)\n if (!user) return NextResponse.json({ ok: false, error: 'Invalid or expired token' }, { status: 400 })\n void emitAuthEvent('auth.password.reset.completed', {\n id: String(user.id),\n tenantId: user.tenantId ? String(user.tenantId) : null,\n organizationId: user.organizationId ? String(user.organizationId) : null,\n at: new Date().toISOString(),\n }, { persistent: true }).catch(() => undefined)\n try {\n const tenantId = user.tenantId ? String(user.tenantId) : null\n if (tenantId) {\n const notificationService = resolveNotificationService(c)\n const typeDef = notificationTypes.find((type) => type.type === 'auth.password_reset.completed')\n if (typeDef) {\n const notificationInput = buildNotificationFromType(typeDef, {\n recipientUserId: String(user.id),\n sourceEntityType: 'auth:user',\n sourceEntityId: String(user.id),\n })\n await notificationService.create(notificationInput, {\n tenantId,\n organizationId: user.organizationId ? String(user.organizationId) : null,\n })\n }\n }\n } catch (err) {\n logger.error('Failed to create notification', { err })\n }\n return NextResponse.json({ ok: true, redirect: '/login' })\n}\n\nexport const metadata = { requireAuth: false }\n\nconst passwordResetConfirmResponseSchema = z.object({\n ok: z.literal(true),\n redirect: z.string(),\n})\n\nconst passwordResetErrorSchema = z.object({\n ok: z.literal(false),\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Confirm password reset',\n methods: {\n POST: {\n summary: 'Complete password reset',\n description: 'Validates the reset token and updates the user password.',\n requestBody: {\n contentType: 'application/x-www-form-urlencoded',\n schema: confirmPasswordResetSchema,\n },\n responses: [\n { status: 200, description: 'Password reset succeeded', schema: passwordResetConfirmResponseSchema },\n { status: 400, description: 'Invalid token or payload', schema: passwordResetErrorSchema },\n ],\n errors: [\n { status: 429, description: 'Too many reset confirmation attempts', schema: rateLimitErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,kCAAkC;AAC3C,SAAS,oBAAoB;AAE7B,SAAS,8BAA8B;AAEvC,SAAS,iCAAiC;AAC1C,SAAS,kCAAkC;AAC3C,OAAO,uBAAuB;AAC9B,SAAS,SAAS;AAClB,SAAS,4BAA4B;AACrC,SAAS,mCAAmC;AAC5C,SAAS,0BAA0B;AACnC,SAAS,oBAAoB;AAC7B,SAAS,qBAAqB;
|
|
4
|
+
"sourcesContent": ["import { confirmPasswordResetSchema } from '@open-mercato/core/modules/auth/data/validators'\nimport { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport { buildNotificationFromType } from '@open-mercato/core/modules/notifications/lib/notificationBuilder'\nimport { resolveNotificationService } from '@open-mercato/core/modules/notifications/lib/notificationService'\nimport notificationTypes from '@open-mercato/core/modules/auth/notifications'\nimport { z } from 'zod'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'\nimport { checkAuthRateLimit } from '@open-mercato/core/modules/auth/lib/rateLimitCheck'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { emitAuthEvent } from '@open-mercato/core/modules/auth/events'\nimport { withTenantHintPath } from '@open-mercato/core/modules/auth/lib/tenantHint'\n\nconst logger = createLogger('auth').child({ component: 'reset-confirm' })\n\nconst resetConfirmRateLimitConfig = readEndpointRateLimitConfig('RESET_CONFIRM', {\n points: 5, duration: 300, keyPrefix: 'reset-confirm',\n})\n\n// validation via confirmPasswordResetSchema\n\nexport async function POST(req: Request) {\n const form = await req.formData()\n const token = String(form.get('token') ?? '')\n const password = String(form.get('password') ?? '')\n // Rate limit by IP \u2014 checked before validation and DB work\n const { error: rateLimitError } = await checkAuthRateLimit({ req, ipConfig: resetConfirmRateLimitConfig })\n if (rateLimitError) return rateLimitError\n const parsed = confirmPasswordResetSchema.safeParse({ token, password })\n if (!parsed.success) return NextResponse.json({ ok: false, error: 'Invalid request' }, { status: 400 })\n const c = await createRequestContainer()\n const auth = c.resolve<AuthService>('authService')\n const user = await auth.confirmPasswordReset(parsed.data.token, parsed.data.password)\n if (!user) return NextResponse.json({ ok: false, error: 'Invalid or expired token' }, { status: 400 })\n void emitAuthEvent('auth.password.reset.completed', {\n id: String(user.id),\n tenantId: user.tenantId ? String(user.tenantId) : null,\n organizationId: user.organizationId ? String(user.organizationId) : null,\n at: new Date().toISOString(),\n }, { persistent: true }).catch(() => undefined)\n try {\n const tenantId = user.tenantId ? String(user.tenantId) : null\n if (tenantId) {\n const notificationService = resolveNotificationService(c)\n const typeDef = notificationTypes.find((type) => type.type === 'auth.password_reset.completed')\n if (typeDef) {\n const notificationInput = buildNotificationFromType(typeDef, {\n recipientUserId: String(user.id),\n sourceEntityType: 'auth:user',\n sourceEntityId: String(user.id),\n })\n await notificationService.create(notificationInput, {\n tenantId,\n organizationId: user.organizationId ? String(user.organizationId) : null,\n })\n }\n }\n } catch (err) {\n logger.error('Failed to create notification', { err })\n }\n return NextResponse.json({ ok: true, redirect: withTenantHintPath('/login', user.tenantId) })\n}\n\nexport const metadata = { requireAuth: false }\n\nconst passwordResetConfirmResponseSchema = z.object({\n ok: z.literal(true),\n redirect: z.string(),\n})\n\nconst passwordResetErrorSchema = z.object({\n ok: z.literal(false),\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Confirm password reset',\n methods: {\n POST: {\n summary: 'Complete password reset',\n description: 'Validates the reset token and updates the user password. The returned `redirect` carries the resolved user\\'s tenant as a navigation hint (`/login?tenant=...`) so a tenant-bound user lands on the tenant login entry; tenantless users get `/login`.',\n requestBody: {\n contentType: 'application/x-www-form-urlencoded',\n schema: confirmPasswordResetSchema,\n },\n responses: [\n { status: 200, description: 'Password reset succeeded', schema: passwordResetConfirmResponseSchema },\n { status: 400, description: 'Invalid token or payload', schema: passwordResetErrorSchema },\n ],\n errors: [\n { status: 429, description: 'Too many reset confirmation attempts', schema: rateLimitErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -12,6 +12,7 @@ import { rateLimitErrorSchema } from "@open-mercato/shared/lib/ratelimit/helpers
|
|
|
12
12
|
import { readEndpointRateLimitConfig } from "@open-mercato/shared/lib/ratelimit/config";
|
|
13
13
|
import { checkAuthRateLimit } from "@open-mercato/core/modules/auth/lib/rateLimitCheck";
|
|
14
14
|
import { mapSecurityEmailUrlError, toSecurityEmailUrl } from "@open-mercato/shared/lib/url";
|
|
15
|
+
import { withTenantHintUrl } from "@open-mercato/core/modules/auth/lib/tenantHint";
|
|
15
16
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
16
17
|
import { emitAuthEvent } from "@open-mercato/core/modules/auth/events";
|
|
17
18
|
const logger = createLogger("auth").child({ component: "reset" });
|
|
@@ -58,7 +59,7 @@ async function POST(req) {
|
|
|
58
59
|
const resReq = await auth.requestPasswordReset(parsed.data.email);
|
|
59
60
|
if (!resReq) return NextResponse.json({ ok: true });
|
|
60
61
|
const { user, token } = resReq;
|
|
61
|
-
const resetUrl = resetUrlTemplate.replace("__token__", token);
|
|
62
|
+
const resetUrl = withTenantHintUrl(resetUrlTemplate.replace("__token__", token), user.tenantId);
|
|
62
63
|
void emitAuthEvent("auth.password.reset.requested", {
|
|
63
64
|
id: String(user.id),
|
|
64
65
|
tenantId: user.tenantId ? String(user.tenantId) : null,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/auth/api/reset.ts"],
|
|
4
|
-
"sourcesContent": ["import { requestPasswordResetSchema } from '@open-mercato/core/modules/auth/data/validators'\nimport { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport ResetPasswordEmail from '@open-mercato/core/modules/auth/emails/ResetPasswordEmail'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { buildNotificationFromType } from '@open-mercato/core/modules/notifications/lib/notificationBuilder'\nimport { resolveNotificationService } from '@open-mercato/core/modules/notifications/lib/notificationService'\nimport notificationTypes from '@open-mercato/core/modules/auth/notifications'\nimport { z } from 'zod'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'\nimport { checkAuthRateLimit } from '@open-mercato/core/modules/auth/lib/rateLimitCheck'\nimport { mapSecurityEmailUrlError, toSecurityEmailUrl } from '@open-mercato/shared/lib/url'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { emitAuthEvent } from '@open-mercato/core/modules/auth/events'\n\nconst logger = createLogger('auth').child({ component: 'reset' })\n\nconst resetRateLimitConfig = readEndpointRateLimitConfig('RESET', {\n points: 3, duration: 60, blockDuration: 60, keyPrefix: 'reset',\n})\nconst resetIpRateLimitConfig = readEndpointRateLimitConfig('RESET_IP', {\n points: 10, duration: 60, blockDuration: 60, keyPrefix: 'reset-ip',\n})\n\n// validation via requestPasswordResetSchema\n\nexport async function POST(req: Request) {\n const form = await req.formData()\n const email = String(form.get('email') ?? '')\n // Rate limit \u2014 two layers, both checked before validation and DB work\n const { error: rateLimitError } = await checkAuthRateLimit({\n req, ipConfig: resetIpRateLimitConfig, compoundConfig: resetRateLimitConfig, compoundIdentifier: email,\n })\n if (rateLimitError) return rateLimitError\n const parsed = requestPasswordResetSchema.safeParse({ email })\n if (!parsed.success) {\n const fieldErrors = parsed.error.flatten().fieldErrors\n return NextResponse.json({ error: 'Validation failed', fieldErrors }, { status: 422 })\n }\n let resetUrlTemplate: string\n try {\n resetUrlTemplate = toSecurityEmailUrl(req, '/reset/__token__')\n } catch (error) {\n const mapped = mapSecurityEmailUrlError(error, {\n scope: 'auth.reset',\n configMessage: 'Password reset is not configured',\n })\n if (mapped) return NextResponse.json(mapped.body, { status: mapped.status })\n throw error\n }\n const c = await createRequestContainer()\n const auth = c.resolve<AuthService>('authService')\n const resReq = await auth.requestPasswordReset(parsed.data.email)\n if (!resReq) return NextResponse.json({ ok: true })\n const { user, token } = resReq\n const resetUrl = resetUrlTemplate.replace('__token__', token)\n void emitAuthEvent('auth.password.reset.requested', {\n id: String(user.id),\n tenantId: user.tenantId ? String(user.tenantId) : null,\n organizationId: user.organizationId ? String(user.organizationId) : null,\n at: new Date().toISOString(),\n }, { persistent: true }).catch(() => undefined)\n\n const { translate } = await resolveTranslations()\n const subject = translate('auth.email.resetPassword.subject', 'Reset your password')\n const copy = {\n preview: translate('auth.email.resetPassword.preview', 'Reset your password'),\n title: translate('auth.email.resetPassword.title', 'Reset your password'),\n body: translate('auth.email.resetPassword.body', 'Click the link below to set a new password. This link will expire in 60 minutes.'),\n cta: translate('auth.email.resetPassword.cta', 'Set a new password'),\n hint: translate('auth.email.resetPassword.hint', \"If you didn't request this, you can safely ignore this email.\"),\n }\n\n try {\n await sendEmail({\n to: user.email,\n subject,\n react: ResetPasswordEmail({ resetUrl, copy }),\n tenantId: user.tenantId ? String(user.tenantId) : undefined,\n organizationId: user.organizationId ? String(user.organizationId) : null,\n })\n } catch (err) {\n logger.error('Failed to send reset email', { err })\n }\n try {\n const tenantId = user.tenantId ? String(user.tenantId) : null\n if (tenantId) {\n const notificationService = resolveNotificationService(c)\n const typeDef = notificationTypes.find((type) => type.type === 'auth.password_reset.requested')\n if (typeDef) {\n const notificationInput = buildNotificationFromType(typeDef, {\n recipientUserId: String(user.id),\n sourceEntityType: 'auth:user',\n sourceEntityId: String(user.id),\n })\n await notificationService.create(notificationInput, {\n tenantId,\n organizationId: user.organizationId ? String(user.organizationId) : null,\n })\n }\n }\n } catch (err) {\n logger.error('Failed to create notification', { err })\n }\n return NextResponse.json({ ok: true })\n}\n\nexport const metadata = { requireAuth: false }\n\nconst passwordResetRequestSchema = z.object({\n email: z.string().email(),\n})\n\nconst passwordResetResponseSchema = z.object({\n ok: z.literal(true),\n})\n\nconst passwordResetErrorSchema = z.object({\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Request password reset',\n methods: {\n POST: {\n summary: 'Send reset email',\n description: 'Requests a password reset email for the given account. The endpoint always returns `ok: true` to avoid leaking account existence.',\n requestBody: {\n contentType: 'application/x-www-form-urlencoded',\n schema: passwordResetRequestSchema,\n },\n responses: [\n { status: 200, description: 'Reset email dispatched (or ignored for unknown accounts)', schema: passwordResetResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid request origin', schema: passwordResetErrorSchema },\n { status: 429, description: 'Too many password reset requests', schema: rateLimitErrorSchema },\n { status: 500, description: 'Password reset email origin is not configured', schema: passwordResetErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,kCAAkC;AAC3C,SAAS,oBAAoB;AAE7B,SAAS,8BAA8B;AAEvC,SAAS,iBAAiB;AAC1B,OAAO,wBAAwB;AAC/B,SAAS,2BAA2B;AACpC,SAAS,iCAAiC;AAC1C,SAAS,kCAAkC;AAC3C,OAAO,uBAAuB;AAC9B,SAAS,SAAS;AAClB,SAAS,4BAA4B;AACrC,SAAS,mCAAmC;AAC5C,SAAS,0BAA0B;AACnC,SAAS,0BAA0B,0BAA0B;AAC7D,SAAS,oBAAoB;AAC7B,SAAS,qBAAqB;AAE9B,MAAM,SAAS,aAAa,MAAM,EAAE,MAAM,EAAE,WAAW,QAAQ,CAAC;AAEhE,MAAM,uBAAuB,4BAA4B,SAAS;AAAA,EAChE,QAAQ;AAAA,EAAG,UAAU;AAAA,EAAI,eAAe;AAAA,EAAI,WAAW;AACzD,CAAC;AACD,MAAM,yBAAyB,4BAA4B,YAAY;AAAA,EACrE,QAAQ;AAAA,EAAI,UAAU;AAAA,EAAI,eAAe;AAAA,EAAI,WAAW;AAC1D,CAAC;AAID,eAAsB,KAAK,KAAc;AACvC,QAAM,OAAO,MAAM,IAAI,SAAS;AAChC,QAAM,QAAQ,OAAO,KAAK,IAAI,OAAO,KAAK,EAAE;AAE5C,QAAM,EAAE,OAAO,eAAe,IAAI,MAAM,mBAAmB;AAAA,IACzD;AAAA,IAAK,UAAU;AAAA,IAAwB,gBAAgB;AAAA,IAAsB,oBAAoB;AAAA,EACnG,CAAC;AACD,MAAI,eAAgB,QAAO;AAC3B,QAAM,SAAS,2BAA2B,UAAU,EAAE,MAAM,CAAC;AAC7D,MAAI,CAAC,OAAO,SAAS;AACnB,UAAM,cAAc,OAAO,MAAM,QAAQ,EAAE;AAC3C,WAAO,aAAa,KAAK,EAAE,OAAO,qBAAqB,YAAY,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACvF;AACA,MAAI;AACJ,MAAI;AACF,uBAAmB,mBAAmB,KAAK,kBAAkB;AAAA,EAC/D,SAAS,OAAO;AACd,UAAM,SAAS,yBAAyB,OAAO;AAAA,MAC7C,OAAO;AAAA,MACP,eAAe;AAAA,IACjB,CAAC;AACD,QAAI,OAAQ,QAAO,aAAa,KAAK,OAAO,MAAM,EAAE,QAAQ,OAAO,OAAO,CAAC;AAC3E,UAAM;AAAA,EACR;AACA,QAAM,IAAI,MAAM,uBAAuB;AACvC,QAAM,OAAO,EAAE,QAAqB,aAAa;AACjD,QAAM,SAAS,MAAM,KAAK,qBAAqB,OAAO,KAAK,KAAK;AAChE,MAAI,CAAC,OAAQ,QAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AAClD,QAAM,EAAE,MAAM,MAAM,IAAI;AACxB,QAAM,WAAW,iBAAiB,QAAQ,aAAa,KAAK;
|
|
4
|
+
"sourcesContent": ["import { requestPasswordResetSchema } from '@open-mercato/core/modules/auth/data/validators'\nimport { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport ResetPasswordEmail from '@open-mercato/core/modules/auth/emails/ResetPasswordEmail'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { buildNotificationFromType } from '@open-mercato/core/modules/notifications/lib/notificationBuilder'\nimport { resolveNotificationService } from '@open-mercato/core/modules/notifications/lib/notificationService'\nimport notificationTypes from '@open-mercato/core/modules/auth/notifications'\nimport { z } from 'zod'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'\nimport { checkAuthRateLimit } from '@open-mercato/core/modules/auth/lib/rateLimitCheck'\nimport { mapSecurityEmailUrlError, toSecurityEmailUrl } from '@open-mercato/shared/lib/url'\nimport { withTenantHintUrl } from '@open-mercato/core/modules/auth/lib/tenantHint'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { emitAuthEvent } from '@open-mercato/core/modules/auth/events'\n\nconst logger = createLogger('auth').child({ component: 'reset' })\n\nconst resetRateLimitConfig = readEndpointRateLimitConfig('RESET', {\n points: 3, duration: 60, blockDuration: 60, keyPrefix: 'reset',\n})\nconst resetIpRateLimitConfig = readEndpointRateLimitConfig('RESET_IP', {\n points: 10, duration: 60, blockDuration: 60, keyPrefix: 'reset-ip',\n})\n\n// validation via requestPasswordResetSchema\n\nexport async function POST(req: Request) {\n const form = await req.formData()\n const email = String(form.get('email') ?? '')\n // Rate limit \u2014 two layers, both checked before validation and DB work\n const { error: rateLimitError } = await checkAuthRateLimit({\n req, ipConfig: resetIpRateLimitConfig, compoundConfig: resetRateLimitConfig, compoundIdentifier: email,\n })\n if (rateLimitError) return rateLimitError\n const parsed = requestPasswordResetSchema.safeParse({ email })\n if (!parsed.success) {\n const fieldErrors = parsed.error.flatten().fieldErrors\n return NextResponse.json({ error: 'Validation failed', fieldErrors }, { status: 422 })\n }\n let resetUrlTemplate: string\n try {\n resetUrlTemplate = toSecurityEmailUrl(req, '/reset/__token__')\n } catch (error) {\n const mapped = mapSecurityEmailUrlError(error, {\n scope: 'auth.reset',\n configMessage: 'Password reset is not configured',\n })\n if (mapped) return NextResponse.json(mapped.body, { status: mapped.status })\n throw error\n }\n const c = await createRequestContainer()\n const auth = c.resolve<AuthService>('authService')\n const resReq = await auth.requestPasswordReset(parsed.data.email)\n if (!resReq) return NextResponse.json({ ok: true })\n const { user, token } = resReq\n const resetUrl = withTenantHintUrl(resetUrlTemplate.replace('__token__', token), user.tenantId)\n void emitAuthEvent('auth.password.reset.requested', {\n id: String(user.id),\n tenantId: user.tenantId ? String(user.tenantId) : null,\n organizationId: user.organizationId ? String(user.organizationId) : null,\n at: new Date().toISOString(),\n }, { persistent: true }).catch(() => undefined)\n\n const { translate } = await resolveTranslations()\n const subject = translate('auth.email.resetPassword.subject', 'Reset your password')\n const copy = {\n preview: translate('auth.email.resetPassword.preview', 'Reset your password'),\n title: translate('auth.email.resetPassword.title', 'Reset your password'),\n body: translate('auth.email.resetPassword.body', 'Click the link below to set a new password. This link will expire in 60 minutes.'),\n cta: translate('auth.email.resetPassword.cta', 'Set a new password'),\n hint: translate('auth.email.resetPassword.hint', \"If you didn't request this, you can safely ignore this email.\"),\n }\n\n try {\n await sendEmail({\n to: user.email,\n subject,\n react: ResetPasswordEmail({ resetUrl, copy }),\n tenantId: user.tenantId ? String(user.tenantId) : undefined,\n organizationId: user.organizationId ? String(user.organizationId) : null,\n })\n } catch (err) {\n logger.error('Failed to send reset email', { err })\n }\n try {\n const tenantId = user.tenantId ? String(user.tenantId) : null\n if (tenantId) {\n const notificationService = resolveNotificationService(c)\n const typeDef = notificationTypes.find((type) => type.type === 'auth.password_reset.requested')\n if (typeDef) {\n const notificationInput = buildNotificationFromType(typeDef, {\n recipientUserId: String(user.id),\n sourceEntityType: 'auth:user',\n sourceEntityId: String(user.id),\n })\n await notificationService.create(notificationInput, {\n tenantId,\n organizationId: user.organizationId ? String(user.organizationId) : null,\n })\n }\n }\n } catch (err) {\n logger.error('Failed to create notification', { err })\n }\n return NextResponse.json({ ok: true })\n}\n\nexport const metadata = { requireAuth: false }\n\nconst passwordResetRequestSchema = z.object({\n email: z.string().email(),\n})\n\nconst passwordResetResponseSchema = z.object({\n ok: z.literal(true),\n})\n\nconst passwordResetErrorSchema = z.object({\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Request password reset',\n methods: {\n POST: {\n summary: 'Send reset email',\n description: 'Requests a password reset email for the given account. The endpoint always returns `ok: true` to avoid leaking account existence.',\n requestBody: {\n contentType: 'application/x-www-form-urlencoded',\n schema: passwordResetRequestSchema,\n },\n responses: [\n { status: 200, description: 'Reset email dispatched (or ignored for unknown accounts)', schema: passwordResetResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid request origin', schema: passwordResetErrorSchema },\n { status: 429, description: 'Too many password reset requests', schema: rateLimitErrorSchema },\n { status: 500, description: 'Password reset email origin is not configured', schema: passwordResetErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/modules/auth/di.js
CHANGED
|
@@ -26,22 +26,22 @@ function resolveOptionalOrganizationHierarchyService(container) {
|
|
|
26
26
|
function register(container) {
|
|
27
27
|
container.register({ authService: asClass(AuthService).scoped() });
|
|
28
28
|
container.register({
|
|
29
|
-
authPrincipalService: asFunction(function authPrincipalServiceFactory(
|
|
29
|
+
authPrincipalService: asFunction(function authPrincipalServiceFactory(cradle) {
|
|
30
30
|
return new DefaultAuthPrincipalService(
|
|
31
|
-
em,
|
|
31
|
+
cradle.em,
|
|
32
32
|
resolveOptionalOrganizationHierarchyService(container)
|
|
33
33
|
);
|
|
34
|
-
}).scoped()
|
|
34
|
+
}).scoped().proxy()
|
|
35
35
|
});
|
|
36
36
|
container.register({
|
|
37
|
-
rbacService: asFunction(function rbacServiceFactory(
|
|
37
|
+
rbacService: asFunction(function rbacServiceFactory(cradle) {
|
|
38
38
|
const configuredCache = resolveOptionalCache(container);
|
|
39
39
|
return new RbacService(
|
|
40
|
-
em,
|
|
40
|
+
cradle.em,
|
|
41
41
|
configuredCache ?? (isRbacDefaultCacheEnabled() ? createRbacFallbackCache() : void 0),
|
|
42
42
|
resolveOptionalOrganizationHierarchyService(container)
|
|
43
43
|
);
|
|
44
|
-
}).scoped()
|
|
44
|
+
}).scoped().proxy()
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
47
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/auth/di.ts"],
|
|
4
|
-
"sourcesContent": ["import { asClass, asFunction } from 'awilix'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport type { OrganizationHierarchyService } from '@open-mercato/shared/lib/auth/principal-service'\nimport { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport {\n createRbacFallbackCache,\n isRbacDefaultCacheEnabled,\n resetRbacFallbackCache,\n} from '@open-mercato/core/modules/auth/services/rbacDefaultCache'\nimport { DefaultAuthPrincipalService } from './services/principalService'\n\nexport { resetRbacFallbackCache }\n\nfunction resolveOptionalCache(container: AppContainer): CacheStrategy | undefined {\n try {\n return container.hasRegistration('cache')\n ? container.resolve<CacheStrategy>('cache')\n : undefined\n } catch {\n return undefined\n }\n}\n\nfunction resolveOptionalOrganizationHierarchyService(\n container: AppContainer,\n): OrganizationHierarchyService | undefined {\n try {\n if (!container.hasRegistration('organizationHierarchyService')) return undefined\n const candidate = container.resolve<OrganizationHierarchyService>('organizationHierarchyService')\n return typeof candidate?.resolveAncestorIds === 'function' ? candidate : undefined\n } catch {\n return undefined\n }\n}\n\nexport function register(container: AppContainer) {\n // Register or override core auth service\n container.register({ authService: asClass(AuthService).scoped() })\n container.register({\n authPrincipalService: asFunction(function authPrincipalServiceFactory(em: EntityManager) {\n return new DefaultAuthPrincipalService(\n em,\n resolveOptionalOrganizationHierarchyService(container),\n )\n }).scoped(),\n })\n // Resolve optional infrastructure lazily so Auth still works when Directory\n // is disabled and in lean CLI/test containers without a CacheStrategy.\n // Setting `OM_RBAC_DEFAULT_CACHE=on` opts into the in-process fallback;\n // an explicitly registered cache always wins.\n container.register({\n rbacService: asFunction(function rbacServiceFactory(em: EntityManager) {\n const configuredCache = resolveOptionalCache(container)\n return new RbacService(\n em,\n configuredCache ?? (isRbacDefaultCacheEnabled() ? createRbacFallbackCache() : undefined),\n resolveOptionalOrganizationHierarchyService(container),\n )\n }).scoped(),\n })\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,SAAS,kBAAkB;AAKpC,SAAS,mBAAmB;AAC5B,SAAS,mBAAmB;AAC5B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,mCAAmC;AAI5C,SAAS,qBAAqB,WAAoD;AAChF,MAAI;AACF,WAAO,UAAU,gBAAgB,OAAO,IACpC,UAAU,QAAuB,OAAO,IACxC;AAAA,EACN,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,4CACP,WAC0C;AAC1C,MAAI;AACF,QAAI,CAAC,UAAU,gBAAgB,8BAA8B,EAAG,QAAO;AACvE,UAAM,YAAY,UAAU,QAAsC,8BAA8B;AAChG,WAAO,OAAO,WAAW,uBAAuB,aAAa,YAAY;AAAA,EAC3E,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,SAAS,WAAyB;AAEhD,YAAU,SAAS,EAAE,aAAa,QAAQ,WAAW,EAAE,OAAO,EAAE,CAAC;AACjE,YAAU,SAAS;AAAA,IACjB,sBAAsB,WAAW,SAAS,4BAA4B,
|
|
4
|
+
"sourcesContent": ["import { asClass, asFunction } from 'awilix'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport type { OrganizationHierarchyService } from '@open-mercato/shared/lib/auth/principal-service'\nimport { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport {\n createRbacFallbackCache,\n isRbacDefaultCacheEnabled,\n resetRbacFallbackCache,\n} from '@open-mercato/core/modules/auth/services/rbacDefaultCache'\nimport { DefaultAuthPrincipalService } from './services/principalService'\n\nexport { resetRbacFallbackCache }\n\nfunction resolveOptionalCache(container: AppContainer): CacheStrategy | undefined {\n try {\n return container.hasRegistration('cache')\n ? container.resolve<CacheStrategy>('cache')\n : undefined\n } catch {\n return undefined\n }\n}\n\nfunction resolveOptionalOrganizationHierarchyService(\n container: AppContainer,\n): OrganizationHierarchyService | undefined {\n try {\n if (!container.hasRegistration('organizationHierarchyService')) return undefined\n const candidate = container.resolve<OrganizationHierarchyService>('organizationHierarchyService')\n return typeof candidate?.resolveAncestorIds === 'function' ? candidate : undefined\n } catch {\n return undefined\n }\n}\n\nexport function register(container: AppContainer) {\n // Register or override core auth service\n container.register({ authService: asClass(AuthService).scoped() })\n container.register({\n authPrincipalService: asFunction(function authPrincipalServiceFactory(cradle: { em: EntityManager }) {\n return new DefaultAuthPrincipalService(\n cradle.em,\n resolveOptionalOrganizationHierarchyService(container),\n )\n }).scoped().proxy(),\n })\n // Resolve optional infrastructure lazily so Auth still works when Directory\n // is disabled and in lean CLI/test containers without a CacheStrategy.\n // Setting `OM_RBAC_DEFAULT_CACHE=on` opts into the in-process fallback;\n // an explicitly registered cache always wins.\n container.register({\n rbacService: asFunction(function rbacServiceFactory(cradle: { em: EntityManager }) {\n const configuredCache = resolveOptionalCache(container)\n return new RbacService(\n cradle.em,\n configuredCache ?? (isRbacDefaultCacheEnabled() ? createRbacFallbackCache() : undefined),\n resolveOptionalOrganizationHierarchyService(container),\n )\n }).scoped().proxy(),\n })\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,SAAS,kBAAkB;AAKpC,SAAS,mBAAmB;AAC5B,SAAS,mBAAmB;AAC5B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,mCAAmC;AAI5C,SAAS,qBAAqB,WAAoD;AAChF,MAAI;AACF,WAAO,UAAU,gBAAgB,OAAO,IACpC,UAAU,QAAuB,OAAO,IACxC;AAAA,EACN,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,4CACP,WAC0C;AAC1C,MAAI;AACF,QAAI,CAAC,UAAU,gBAAgB,8BAA8B,EAAG,QAAO;AACvE,UAAM,YAAY,UAAU,QAAsC,8BAA8B;AAChG,WAAO,OAAO,WAAW,uBAAuB,aAAa,YAAY;AAAA,EAC3E,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,SAAS,WAAyB;AAEhD,YAAU,SAAS,EAAE,aAAa,QAAQ,WAAW,EAAE,OAAO,EAAE,CAAC;AACjE,YAAU,SAAS;AAAA,IACjB,sBAAsB,WAAW,SAAS,4BAA4B,QAA+B;AACnG,aAAO,IAAI;AAAA,QACT,OAAO;AAAA,QACP,4CAA4C,SAAS;AAAA,MACvD;AAAA,IACF,CAAC,EAAE,OAAO,EAAE,MAAM;AAAA,EACpB,CAAC;AAKD,YAAU,SAAS;AAAA,IACjB,aAAa,WAAW,SAAS,mBAAmB,QAA+B;AACjF,YAAM,kBAAkB,qBAAqB,SAAS;AACtD,aAAO,IAAI;AAAA,QACT,OAAO;AAAA,QACP,oBAAoB,0BAA0B,IAAI,wBAAwB,IAAI;AAAA,QAC9E,4CAA4C,SAAS;AAAA,MACvD;AAAA,IACF,CAAC,EAAE,OAAO,EAAE,MAAM;AAAA,EACpB,CAAC;AACH;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const TENANT_HINT_PARAM = "tenant";
|
|
2
|
+
function normalizeTenantHint(tenantId) {
|
|
3
|
+
const value = tenantId?.trim();
|
|
4
|
+
return value ? value : null;
|
|
5
|
+
}
|
|
6
|
+
function withTenantHintUrl(url, tenantId) {
|
|
7
|
+
const tenant = normalizeTenantHint(tenantId);
|
|
8
|
+
if (!tenant) return url;
|
|
9
|
+
try {
|
|
10
|
+
const parsed = new URL(url);
|
|
11
|
+
parsed.searchParams.set(TENANT_HINT_PARAM, tenant);
|
|
12
|
+
return parsed.toString();
|
|
13
|
+
} catch {
|
|
14
|
+
return url;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function withTenantHintPath(path, tenantId) {
|
|
18
|
+
const tenant = normalizeTenantHint(tenantId);
|
|
19
|
+
if (!tenant) return path;
|
|
20
|
+
const [pathname, existingQuery = ""] = path.split("?");
|
|
21
|
+
const params = new URLSearchParams(existingQuery);
|
|
22
|
+
params.set(TENANT_HINT_PARAM, tenant);
|
|
23
|
+
return `${pathname}?${params.toString()}`;
|
|
24
|
+
}
|
|
25
|
+
export {
|
|
26
|
+
withTenantHintPath,
|
|
27
|
+
withTenantHintUrl
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=tenantHint.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/auth/lib/tenantHint.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Tenant continuity hints for unauthenticated auth navigation.\n *\n * The `tenant` query parameter is a navigation hint only \u2014 it tells the login\n * entry which tenant the user was already resolved to so it can show tenant\n * branding instead of the generic form. It is never authorization, and it must\n * only ever be derived from a server-resolved user, never from request input.\n */\n\nconst TENANT_HINT_PARAM = 'tenant'\n\nexport type TenantHintSource = string | null | undefined\n\nfunction normalizeTenantHint(tenantId: TenantHintSource): string | null {\n const value = tenantId?.trim()\n return value ? value : null\n}\n\n/**\n * Adds the tenant hint to an absolute URL (password reset and invitation email\n * links). Returns the URL unchanged for tenantless users or an unparsable URL,\n * so a hint can never cost a user their working link.\n */\nexport function withTenantHintUrl(url: string, tenantId: TenantHintSource): string {\n const tenant = normalizeTenantHint(tenantId)\n if (!tenant) return url\n try {\n const parsed = new URL(url)\n parsed.searchParams.set(TENANT_HINT_PARAM, tenant)\n return parsed.toString()\n } catch {\n return url\n }\n}\n\n/**\n * Adds the tenant hint to an app-relative path (the redirect handed back to the\n * browser after a successful reset). Returns the path unchanged for tenantless\n * users. Encoding and replace-don't-duplicate semantics match\n * {@link withTenantHintUrl}.\n */\nexport function withTenantHintPath(path: string, tenantId: TenantHintSource): string {\n const tenant = normalizeTenantHint(tenantId)\n if (!tenant) return path\n const [pathname, existingQuery = ''] = path.split('?')\n const params = new URLSearchParams(existingQuery)\n params.set(TENANT_HINT_PARAM, tenant)\n return `${pathname}?${params.toString()}`\n}\n"],
|
|
5
|
+
"mappings": "AASA,MAAM,oBAAoB;AAI1B,SAAS,oBAAoB,UAA2C;AACtE,QAAM,QAAQ,UAAU,KAAK;AAC7B,SAAO,QAAQ,QAAQ;AACzB;AAOO,SAAS,kBAAkB,KAAa,UAAoC;AACjF,QAAM,SAAS,oBAAoB,QAAQ;AAC3C,MAAI,CAAC,OAAQ,QAAO;AACpB,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,WAAO,aAAa,IAAI,mBAAmB,MAAM;AACjD,WAAO,OAAO,SAAS;AAAA,EACzB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAQO,SAAS,mBAAmB,MAAc,UAAoC;AACnF,QAAM,SAAS,oBAAoB,QAAQ;AAC3C,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,CAAC,UAAU,gBAAgB,EAAE,IAAI,KAAK,MAAM,GAAG;AACrD,QAAM,SAAS,IAAI,gBAAgB,aAAa;AAChD,SAAO,IAAI,mBAAmB,MAAM;AACpC,SAAO,GAAG,QAAQ,IAAI,OAAO,SAAS,CAAC;AACzC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -651,7 +651,8 @@ function EditCatalogProductPage({
|
|
|
651
651
|
{
|
|
652
652
|
values,
|
|
653
653
|
setValue,
|
|
654
|
-
errors
|
|
654
|
+
errors,
|
|
655
|
+
variantCount: variants.length
|
|
655
656
|
}
|
|
656
657
|
)
|
|
657
658
|
},
|
|
@@ -1487,7 +1488,11 @@ function ProductMetadataSection({ values, setValue }) {
|
|
|
1487
1488
|
);
|
|
1488
1489
|
return /* @__PURE__ */ jsx(MetadataEditor, { value: metadata, onChange: handleMetadataChange, embedded: true });
|
|
1489
1490
|
}
|
|
1490
|
-
function ProductOptionsSection({
|
|
1491
|
+
function ProductOptionsSection({
|
|
1492
|
+
values,
|
|
1493
|
+
setValue,
|
|
1494
|
+
variantCount = 0
|
|
1495
|
+
}) {
|
|
1491
1496
|
const t = useT();
|
|
1492
1497
|
const [schemaDialogOpen, setSchemaDialogOpen] = React.useState(false);
|
|
1493
1498
|
const [schemaTemplates, setSchemaTemplates] = React.useState([]);
|
|
@@ -1743,7 +1748,10 @@ function ProductOptionsSection({ values, setValue }) {
|
|
|
1743
1748
|
)
|
|
1744
1749
|
] })
|
|
1745
1750
|
] }, option.id)),
|
|
1746
|
-
!values.options?.length ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t(
|
|
1751
|
+
!values.options?.length ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: variantCount > 0 ? t(
|
|
1752
|
+
"catalog.products.edit.optionsBuilder.emptyWithVariants",
|
|
1753
|
+
"This product has variants without an option schema. Options are optional."
|
|
1754
|
+
) : t(
|
|
1747
1755
|
"catalog.products.create.optionsBuilder.empty",
|
|
1748
1756
|
"No options yet. Add your first option to generate variants."
|
|
1749
1757
|
) }) : null
|