@open-mercato/core 0.6.8-develop.7080.1.9b0d558a3f → 0.6.8-develop.7088.1.3873d81f42
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/auth/api/reset/validate.js +55 -0
- package/dist/modules/auth/api/reset/validate.js.map +7 -0
- package/dist/modules/auth/data/validators.js +5 -1
- package/dist/modules/auth/data/validators.js.map +2 -2
- package/dist/modules/auth/frontend/reset/[token]/page.js +46 -0
- package/dist/modules/auth/frontend/reset/[token]/page.js.map +2 -2
- package/dist/modules/auth/services/authService.js +16 -2
- package/dist/modules/auth/services/authService.js.map +2 -2
- package/dist/modules/customers/backend/hooks/useEmailDuplicateCheck.js +9 -3
- package/dist/modules/customers/backend/hooks/useEmailDuplicateCheck.js.map +2 -2
- package/dist/modules/sales/data/entities.js +8 -2
- package/dist/modules/sales/data/entities.js.map +2 -2
- package/dist/modules/sales/migrations/Migration20260821120000_sales_notes_addresses_context_idx.js +32 -0
- package/dist/modules/sales/migrations/Migration20260821120000_sales_notes_addresses_context_idx.js.map +7 -0
- package/dist/modules/staff/backend/staff/timesheets/page.js +29 -18
- package/dist/modules/staff/backend/staff/timesheets/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/api/reset/validate.ts +57 -0
- package/src/modules/auth/data/validators.ts +5 -0
- package/src/modules/auth/frontend/reset/[token]/page.tsx +74 -0
- package/src/modules/auth/i18n/de.json +4 -0
- package/src/modules/auth/i18n/en.json +4 -0
- package/src/modules/auth/i18n/es.json +4 -0
- package/src/modules/auth/i18n/ko.json +4 -0
- package/src/modules/auth/i18n/pl.json +4 -0
- package/src/modules/auth/services/authService.ts +18 -2
- package/src/modules/customers/backend/hooks/useEmailDuplicateCheck.ts +12 -4
- package/src/modules/sales/data/entities.ts +6 -0
- package/src/modules/sales/migrations/.snapshot-open-mercato.json +60 -0
- package/src/modules/sales/migrations/Migration20260821120000_sales_notes_addresses_context_idx.ts +87 -0
- package/src/modules/staff/backend/staff/timesheets/page.tsx +12 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { validatePasswordResetTokenSchema } from "@open-mercato/core/modules/auth/data/validators";
|
|
2
|
+
import { NextResponse } from "next/server";
|
|
3
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { rateLimitErrorSchema } from "@open-mercato/shared/lib/ratelimit/helpers";
|
|
6
|
+
import { readEndpointRateLimitConfig } from "@open-mercato/shared/lib/ratelimit/config";
|
|
7
|
+
import { checkAuthRateLimit } from "@open-mercato/core/modules/auth/lib/rateLimitCheck";
|
|
8
|
+
const resetValidateRateLimitConfig = readEndpointRateLimitConfig("RESET_VALIDATE", {
|
|
9
|
+
points: 10,
|
|
10
|
+
duration: 300,
|
|
11
|
+
keyPrefix: "reset-validate"
|
|
12
|
+
});
|
|
13
|
+
async function POST(req) {
|
|
14
|
+
const form = await req.formData();
|
|
15
|
+
const token = String(form.get("token") ?? "");
|
|
16
|
+
const { error: rateLimitError } = await checkAuthRateLimit({ req, ipConfig: resetValidateRateLimitConfig });
|
|
17
|
+
if (rateLimitError) return rateLimitError;
|
|
18
|
+
const parsed = validatePasswordResetTokenSchema.safeParse({ token });
|
|
19
|
+
if (!parsed.success) return NextResponse.json({ ok: true, valid: false });
|
|
20
|
+
const container = await createRequestContainer();
|
|
21
|
+
const auth = container.resolve("authService");
|
|
22
|
+
const valid = await auth.isPasswordResetTokenValid(parsed.data.token);
|
|
23
|
+
return NextResponse.json({ ok: true, valid });
|
|
24
|
+
}
|
|
25
|
+
const metadata = { requireAuth: false };
|
|
26
|
+
const passwordResetValidateResponseSchema = z.object({
|
|
27
|
+
ok: z.literal(true),
|
|
28
|
+
valid: z.boolean()
|
|
29
|
+
});
|
|
30
|
+
const openApi = {
|
|
31
|
+
tag: "Authentication & Accounts",
|
|
32
|
+
summary: "Check a password reset token",
|
|
33
|
+
methods: {
|
|
34
|
+
POST: {
|
|
35
|
+
summary: "Check whether a password reset token is still usable",
|
|
36
|
+
description: "Reports whether a reset token exists, is unused, and has not expired, so the reset page can render a terminal state instead of a form the token can never submit. The token is never consumed and the response never distinguishes unknown, used, and expired tokens.",
|
|
37
|
+
requestBody: {
|
|
38
|
+
contentType: "application/x-www-form-urlencoded",
|
|
39
|
+
schema: validatePasswordResetTokenSchema
|
|
40
|
+
},
|
|
41
|
+
responses: [
|
|
42
|
+
{ status: 200, description: "Token state resolved", schema: passwordResetValidateResponseSchema }
|
|
43
|
+
],
|
|
44
|
+
errors: [
|
|
45
|
+
{ status: 429, description: "Too many token validation attempts", schema: rateLimitErrorSchema }
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
export {
|
|
51
|
+
POST,
|
|
52
|
+
metadata,
|
|
53
|
+
openApi
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/auth/api/reset/validate.ts"],
|
|
4
|
+
"sourcesContent": ["import { validatePasswordResetTokenSchema } 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 { 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'\n\nconst resetValidateRateLimitConfig = readEndpointRateLimitConfig('RESET_VALIDATE', {\n points: 10, duration: 300, keyPrefix: 'reset-validate',\n})\n\nexport async function POST(req: Request) {\n const form = await req.formData()\n const token = String(form.get('token') ?? '')\n // Rate limit by IP \u2014 checked before validation and DB work\n const { error: rateLimitError } = await checkAuthRateLimit({ req, ipConfig: resetValidateRateLimitConfig })\n if (rateLimitError) return rateLimitError\n const parsed = validatePasswordResetTokenSchema.safeParse({ token })\n // A malformed token is reported as simply not valid: the caller learns nothing\n // it could not learn by posting the same token to /api/auth/reset/confirm.\n if (!parsed.success) return NextResponse.json({ ok: true, valid: false })\n const container = await createRequestContainer()\n const auth = container.resolve<AuthService>('authService')\n const valid = await auth.isPasswordResetTokenValid(parsed.data.token)\n return NextResponse.json({ ok: true, valid })\n}\n\nexport const metadata = { requireAuth: false }\n\nconst passwordResetValidateResponseSchema = z.object({\n ok: z.literal(true),\n valid: z.boolean(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Check a password reset token',\n methods: {\n POST: {\n summary: 'Check whether a password reset token is still usable',\n description: 'Reports whether a reset token exists, is unused, and has not expired, so the reset page can render a terminal state instead of a form the token can never submit. The token is never consumed and the response never distinguishes unknown, used, and expired tokens.',\n requestBody: {\n contentType: 'application/x-www-form-urlencoded',\n schema: validatePasswordResetTokenSchema,\n },\n responses: [\n { status: 200, description: 'Token state resolved', schema: passwordResetValidateResponseSchema },\n ],\n errors: [\n { status: 429, description: 'Too many token validation attempts', schema: rateLimitErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,wCAAwC;AACjD,SAAS,oBAAoB;AAE7B,SAAS,8BAA8B;AAEvC,SAAS,SAAS;AAClB,SAAS,4BAA4B;AACrC,SAAS,mCAAmC;AAC5C,SAAS,0BAA0B;AAEnC,MAAM,+BAA+B,4BAA4B,kBAAkB;AAAA,EACjF,QAAQ;AAAA,EAAI,UAAU;AAAA,EAAK,WAAW;AACxC,CAAC;AAED,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,EAAE,KAAK,UAAU,6BAA6B,CAAC;AAC1G,MAAI,eAAgB,QAAO;AAC3B,QAAM,SAAS,iCAAiC,UAAU,EAAE,MAAM,CAAC;AAGnE,MAAI,CAAC,OAAO,QAAS,QAAO,aAAa,KAAK,EAAE,IAAI,MAAM,OAAO,MAAM,CAAC;AACxE,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,OAAO,UAAU,QAAqB,aAAa;AACzD,QAAM,QAAQ,MAAM,KAAK,0BAA0B,OAAO,KAAK,KAAK;AACpE,SAAO,aAAa,KAAK,EAAE,IAAI,MAAM,MAAM,CAAC;AAC9C;AAEO,MAAM,WAAW,EAAE,aAAa,MAAM;AAE7C,MAAM,sCAAsC,EAAE,OAAO;AAAA,EACnD,IAAI,EAAE,QAAQ,IAAI;AAAA,EAClB,OAAO,EAAE,QAAQ;AACnB,CAAC;AAEM,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,MACb,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,wBAAwB,QAAQ,oCAAoC;AAAA,MAClG;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,sCAAsC,QAAQ,qBAAqB;AAAA,MACjG;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -14,6 +14,9 @@ const confirmPasswordResetSchema = z.object({
|
|
|
14
14
|
token: z.string().min(10),
|
|
15
15
|
password: passwordSchema
|
|
16
16
|
});
|
|
17
|
+
const validatePasswordResetTokenSchema = z.object({
|
|
18
|
+
token: z.string().min(10)
|
|
19
|
+
});
|
|
17
20
|
const refreshSessionRequestSchema = z.object({
|
|
18
21
|
refreshToken: z.string().min(1)
|
|
19
22
|
});
|
|
@@ -117,6 +120,7 @@ export {
|
|
|
117
120
|
sidebarVariantSettingsSchema,
|
|
118
121
|
updateSidebarVariantInputSchema,
|
|
119
122
|
userCreateSchema,
|
|
120
|
-
userLoginSchema
|
|
123
|
+
userLoginSchema,
|
|
124
|
+
validatePasswordResetTokenSchema
|
|
121
125
|
};
|
|
122
126
|
//# sourceMappingURL=validators.js.map
|
|
@@ -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 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 refreshSessionRequestSchema = z.object({\n refreshToken: z.string().min(1),\n})\n\nexport const sidebarPreferencesScopeSchema = z.union([\n z.object({ type: z.literal('user') }),\n z.object({ type: z.literal('role'), roleId: z.string().uuid() }),\n])\n\n// Sidebar settings shape shared by both `sidebarPreferencesInputSchema` (which\n// adds role-targeting fields) and the variants API (which uses just the\n// settings + name + isActive). Keeping the field constraints in one place\n// prevents drift between the preferences and variants surfaces.\nexport const sidebarVariantSettingsSchema = 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 itemOrder: z.record(z.string().min(1), z.array(z.string().min(1)).max(500)).optional(),\n})\n\nexport const createSidebarVariantInputSchema = z.object({\n name: z.string().trim().min(1).max(120).optional(),\n settings: sidebarVariantSettingsSchema.optional(),\n isActive: z.boolean().optional(),\n})\n\nexport const updateSidebarVariantInputSchema = z.object({\n name: z.string().trim().min(1).max(120).optional(),\n settings: sidebarVariantSettingsSchema.optional(),\n isActive: z.boolean().optional(),\n})\n\nexport const sidebarVariantRecordSchema = z.object({\n id: z.string().uuid(),\n name: z.string(),\n isActive: z.boolean(),\n settings: z.object({\n version: z.number().int().positive(),\n groupOrder: z.array(z.string()),\n groupLabels: z.record(z.string(), z.string()),\n itemLabels: z.record(z.string(), z.string()),\n hiddenItems: z.array(z.string()),\n itemOrder: z.record(z.string(), z.array(z.string())),\n }),\n createdAt: z.string(),\n updatedAt: z.string().nullable(),\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 itemOrder: z.record(z.string().min(1), 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 scope: sidebarPreferencesScopeSchema.optional(),\n}).superRefine((value, ctx) => {\n const scopeType = value.scope?.type ?? 'user'\n if (scopeType === 'role') {\n if ((value.applyToRoles?.length ?? 0) > 0) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['applyToRoles'],\n message: 'applyToRoles is only valid when scope.type === \"user\"',\n })\n }\n if ((value.clearRoleIds?.length ?? 0) > 0) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['clearRoleIds'],\n message: 'clearRoleIds is only valid when scope.type === \"user\"',\n })\n }\n }\n})\n\n// Optional helpers for CLI or admin forms\nexport const userCreateSchema = z.object({\n email: z.string().email(),\n name: z.preprocess(\n (value) => {\n if (typeof value !== 'string') return value\n const trimmed = value.trim()\n return trimmed.length ? trimmed : undefined\n },\n z.string().trim().min(1).max(120).optional(),\n ),\n password: passwordSchema.optional(),\n sendInviteEmail: z.boolean().optional(),\n tenantId: z.string().uuid().optional(),\n organizationId: z.string().uuid(),\n rolesCsv: z.string().optional(),\n}).refine(\n (data) => data.password || data.sendInviteEmail,\n { message: 'Either password or sendInviteEmail is required', path: ['password'] },\n)\n\nexport const featureCheckRequestSchema = z.object({\n features: z.array(z.string().max(128)).max(50).describe('Feature identifiers to check'),\n}).describe('Batch feature check payload')\n\nexport type UserLoginInput = z.infer<typeof userLoginSchema>\nexport type RequestPasswordResetInput = z.infer<typeof requestPasswordResetSchema>\nexport type ConfirmPasswordResetInput = z.infer<typeof confirmPasswordResetSchema>\nexport type RefreshSessionRequestInput = z.infer<typeof refreshSessionRequestSchema>\nexport type SidebarPreferencesInput = z.infer<typeof sidebarPreferencesInputSchema>\nexport type SidebarVariantSettingsInput = z.infer<typeof sidebarVariantSettingsSchema>\nexport type CreateSidebarVariantInput = z.infer<typeof createSidebarVariantInputSchema>\nexport type UpdateSidebarVariantInput = z.infer<typeof updateSidebarVariantInputSchema>\nexport type SidebarVariantRecordResponse = z.infer<typeof sidebarVariantRecordSchema>\nexport type UserCreateInput = z.infer<typeof userCreateSchema>\nexport type FeatureCheckRequestInput = z.infer<typeof featureCheckRequestSchema>\n"],
|
|
5
|
-
"mappings": "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",
|
|
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 validatePasswordResetTokenSchema = z.object({\n token: z.string().min(10),\n})\n\nexport const refreshSessionRequestSchema = z.object({\n refreshToken: z.string().min(1),\n})\n\nexport const sidebarPreferencesScopeSchema = z.union([\n z.object({ type: z.literal('user') }),\n z.object({ type: z.literal('role'), roleId: z.string().uuid() }),\n])\n\n// Sidebar settings shape shared by both `sidebarPreferencesInputSchema` (which\n// adds role-targeting fields) and the variants API (which uses just the\n// settings + name + isActive). Keeping the field constraints in one place\n// prevents drift between the preferences and variants surfaces.\nexport const sidebarVariantSettingsSchema = 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 itemOrder: z.record(z.string().min(1), z.array(z.string().min(1)).max(500)).optional(),\n})\n\nexport const createSidebarVariantInputSchema = z.object({\n name: z.string().trim().min(1).max(120).optional(),\n settings: sidebarVariantSettingsSchema.optional(),\n isActive: z.boolean().optional(),\n})\n\nexport const updateSidebarVariantInputSchema = z.object({\n name: z.string().trim().min(1).max(120).optional(),\n settings: sidebarVariantSettingsSchema.optional(),\n isActive: z.boolean().optional(),\n})\n\nexport const sidebarVariantRecordSchema = z.object({\n id: z.string().uuid(),\n name: z.string(),\n isActive: z.boolean(),\n settings: z.object({\n version: z.number().int().positive(),\n groupOrder: z.array(z.string()),\n groupLabels: z.record(z.string(), z.string()),\n itemLabels: z.record(z.string(), z.string()),\n hiddenItems: z.array(z.string()),\n itemOrder: z.record(z.string(), z.array(z.string())),\n }),\n createdAt: z.string(),\n updatedAt: z.string().nullable(),\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 itemOrder: z.record(z.string().min(1), 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 scope: sidebarPreferencesScopeSchema.optional(),\n}).superRefine((value, ctx) => {\n const scopeType = value.scope?.type ?? 'user'\n if (scopeType === 'role') {\n if ((value.applyToRoles?.length ?? 0) > 0) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['applyToRoles'],\n message: 'applyToRoles is only valid when scope.type === \"user\"',\n })\n }\n if ((value.clearRoleIds?.length ?? 0) > 0) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['clearRoleIds'],\n message: 'clearRoleIds is only valid when scope.type === \"user\"',\n })\n }\n }\n})\n\n// Optional helpers for CLI or admin forms\nexport const userCreateSchema = z.object({\n email: z.string().email(),\n name: z.preprocess(\n (value) => {\n if (typeof value !== 'string') return value\n const trimmed = value.trim()\n return trimmed.length ? trimmed : undefined\n },\n z.string().trim().min(1).max(120).optional(),\n ),\n password: passwordSchema.optional(),\n sendInviteEmail: z.boolean().optional(),\n tenantId: z.string().uuid().optional(),\n organizationId: z.string().uuid(),\n rolesCsv: z.string().optional(),\n}).refine(\n (data) => data.password || data.sendInviteEmail,\n { message: 'Either password or sendInviteEmail is required', path: ['password'] },\n)\n\nexport const featureCheckRequestSchema = z.object({\n features: z.array(z.string().max(128)).max(50).describe('Feature identifiers to check'),\n}).describe('Batch feature check payload')\n\nexport type UserLoginInput = z.infer<typeof userLoginSchema>\nexport type RequestPasswordResetInput = z.infer<typeof requestPasswordResetSchema>\nexport type ConfirmPasswordResetInput = z.infer<typeof confirmPasswordResetSchema>\nexport type ValidatePasswordResetTokenInput = z.infer<typeof validatePasswordResetTokenSchema>\nexport type RefreshSessionRequestInput = z.infer<typeof refreshSessionRequestSchema>\nexport type SidebarPreferencesInput = z.infer<typeof sidebarPreferencesInputSchema>\nexport type SidebarVariantSettingsInput = z.infer<typeof sidebarVariantSettingsSchema>\nexport type CreateSidebarVariantInput = z.infer<typeof createSidebarVariantInputSchema>\nexport type UpdateSidebarVariantInput = z.infer<typeof updateSidebarVariantInputSchema>\nexport type SidebarVariantRecordResponse = z.infer<typeof sidebarVariantRecordSchema>\nexport type UserCreateInput = z.infer<typeof userCreateSchema>\nexport type FeatureCheckRequestInput = z.infer<typeof featureCheckRequestSchema>\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,mCAAmC,EAAE,OAAO;AAAA,EACvD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE;AAC1B,CAAC;AAEM,MAAM,8BAA8B,EAAE,OAAO;AAAA,EAClD,cAAc,EAAE,OAAO,EAAE,IAAI,CAAC;AAChC,CAAC;AAEM,MAAM,gCAAgC,EAAE,MAAM;AAAA,EACnD,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,MAAM,EAAE,CAAC;AAAA,EACpC,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,MAAM,GAAG,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AACjE,CAAC;AAMM,MAAM,+BAA+B,EAAE,OAAO;AAAA,EACnD,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,WAAW,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,EAAE,IAAI,GAAG,CAAC,EAAE,SAAS;AACvF,CAAC;AAEM,MAAM,kCAAkC,EAAE,OAAO;AAAA,EACtD,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EACjD,UAAU,6BAA6B,SAAS;AAAA,EAChD,UAAU,EAAE,QAAQ,EAAE,SAAS;AACjC,CAAC;AAEM,MAAM,kCAAkC,EAAE,OAAO;AAAA,EACtD,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EACjD,UAAU,6BAA6B,SAAS;AAAA,EAChD,UAAU,EAAE,QAAQ,EAAE,SAAS;AACjC,CAAC;AAEM,MAAM,6BAA6B,EAAE,OAAO;AAAA,EACjD,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,EACpB,MAAM,EAAE,OAAO;AAAA,EACf,UAAU,EAAE,QAAQ;AAAA,EACpB,UAAU,EAAE,OAAO;AAAA,IACjB,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS;AAAA,IACnC,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC;AAAA,IAC9B,aAAa,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,OAAO,CAAC;AAAA,IAC5C,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,OAAO,CAAC;AAAA,IAC3C,aAAa,EAAE,MAAM,EAAE,OAAO,CAAC;AAAA,IAC/B,WAAW,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;AAAA,EACrD,CAAC;AAAA,EACD,WAAW,EAAE,OAAO;AAAA,EACpB,WAAW,EAAE,OAAO,EAAE,SAAS;AACjC,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,WAAW,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,EAAE,IAAI,GAAG,CAAC,EAAE,SAAS;AAAA,EACrF,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,SAAS;AAAA,EAClD,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,SAAS;AAAA,EAClD,OAAO,8BAA8B,SAAS;AAChD,CAAC,EAAE,YAAY,CAAC,OAAO,QAAQ;AAC7B,QAAM,YAAY,MAAM,OAAO,QAAQ;AACvC,MAAI,cAAc,QAAQ;AACxB,SAAK,MAAM,cAAc,UAAU,KAAK,GAAG;AACzC,UAAI,SAAS;AAAA,QACX,MAAM,EAAE,aAAa;AAAA,QACrB,MAAM,CAAC,cAAc;AAAA,QACrB,SAAS;AAAA,MACX,CAAC;AAAA,IACH;AACA,SAAK,MAAM,cAAc,UAAU,KAAK,GAAG;AACzC,UAAI,SAAS;AAAA,QACX,MAAM,EAAE,aAAa;AAAA,QACrB,MAAM,CAAC,cAAc;AAAA,QACrB,SAAS;AAAA,MACX,CAAC;AAAA,IACH;AAAA,EACF;AACF,CAAC;AAGM,MAAM,mBAAmB,EAAE,OAAO;AAAA,EACvC,OAAO,EAAE,OAAO,EAAE,MAAM;AAAA,EACxB,MAAM,EAAE;AAAA,IACN,CAAC,UAAU;AACT,UAAI,OAAO,UAAU,SAAU,QAAO;AACtC,YAAM,UAAU,MAAM,KAAK;AAC3B,aAAO,QAAQ,SAAS,UAAU;AAAA,IACpC;AAAA,IACA,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EAC7C;AAAA,EACA,UAAU,eAAe,SAAS;AAAA,EAClC,iBAAiB,EAAE,QAAQ,EAAE,SAAS;AAAA,EACtC,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EACrC,gBAAgB,EAAE,OAAO,EAAE,KAAK;AAAA,EAChC,UAAU,EAAE,OAAO,EAAE,SAAS;AAChC,CAAC,EAAE;AAAA,EACD,CAAC,SAAS,KAAK,YAAY,KAAK;AAAA,EAChC,EAAE,SAAS,kDAAkD,MAAM,CAAC,UAAU,EAAE;AAClF;AAEO,MAAM,4BAA4B,EAAE,OAAO;AAAA,EAChD,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,GAAG,CAAC,EAAE,IAAI,EAAE,EAAE,SAAS,8BAA8B;AACxF,CAAC,EAAE,SAAS,6BAA6B;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -5,8 +5,10 @@ import { Button } from "@open-mercato/ui/primitives/button";
|
|
|
5
5
|
import { PasswordInput } from "@open-mercato/ui/primitives/password-input";
|
|
6
6
|
import { Label } from "@open-mercato/ui/primitives/label";
|
|
7
7
|
import { useEffect, useState } from "react";
|
|
8
|
+
import Link from "next/link";
|
|
8
9
|
import { useRouter } from "next/navigation";
|
|
9
10
|
import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
|
|
11
|
+
import { Spinner } from "@open-mercato/ui/primitives/spinner";
|
|
10
12
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
11
13
|
import { formatPasswordRequirements, getPasswordPolicy, validatePassword } from "@open-mercato/shared/lib/auth/passwordPolicy";
|
|
12
14
|
function ResetWithTokenPage({ params }) {
|
|
@@ -15,12 +17,35 @@ function ResetWithTokenPage({ params }) {
|
|
|
15
17
|
const [error, setError] = useState(null);
|
|
16
18
|
const [submitting, setSubmitting] = useState(false);
|
|
17
19
|
const [clientReady, setClientReady] = useState(false);
|
|
20
|
+
const [tokenState, setTokenState] = useState("checking");
|
|
18
21
|
const passwordPolicy = getPasswordPolicy();
|
|
19
22
|
const passwordRequirements = formatPasswordRequirements(passwordPolicy, t);
|
|
20
23
|
const passwordDescription = passwordRequirements ? t("auth.password.requirements.help", "Password requirements: {requirements}", { requirements: passwordRequirements }) : "";
|
|
21
24
|
useEffect(() => {
|
|
22
25
|
setClientReady(true);
|
|
23
26
|
}, []);
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
let cancelled = false;
|
|
29
|
+
async function checkToken() {
|
|
30
|
+
const body = new FormData();
|
|
31
|
+
body.set("token", params.token);
|
|
32
|
+
try {
|
|
33
|
+
const { ok, result } = await apiCall(
|
|
34
|
+
"/api/auth/reset/validate",
|
|
35
|
+
{ method: "POST", body }
|
|
36
|
+
);
|
|
37
|
+
if (cancelled) return;
|
|
38
|
+
setTokenState(ok && result?.valid === false ? "expired" : "usable");
|
|
39
|
+
} catch {
|
|
40
|
+
if (cancelled) return;
|
|
41
|
+
setTokenState("usable");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
void checkToken();
|
|
45
|
+
return () => {
|
|
46
|
+
cancelled = true;
|
|
47
|
+
};
|
|
48
|
+
}, [params.token]);
|
|
24
49
|
async function onSubmit(e) {
|
|
25
50
|
e.preventDefault();
|
|
26
51
|
setError(null);
|
|
@@ -59,6 +84,27 @@ function ResetWithTokenPage({ params }) {
|
|
|
59
84
|
setSubmitting(false);
|
|
60
85
|
}
|
|
61
86
|
}
|
|
87
|
+
if (tokenState === "checking") {
|
|
88
|
+
return /* @__PURE__ */ jsx("div", { className: "min-h-svh flex items-center justify-center p-4", children: /* @__PURE__ */ jsxs(Card, { className: "w-full max-w-sm", children: [
|
|
89
|
+
/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsx(CardTitle, { children: t("auth.reset.title", "Set a new password") }) }),
|
|
90
|
+
/* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", "data-auth-token-state": "checking", children: [
|
|
91
|
+
/* @__PURE__ */ jsx(Spinner, { size: "sm" }),
|
|
92
|
+
/* @__PURE__ */ jsx("span", { children: t("auth.reset.token.checking", "Checking your reset link...") })
|
|
93
|
+
] }) })
|
|
94
|
+
] }) });
|
|
95
|
+
}
|
|
96
|
+
if (tokenState === "expired") {
|
|
97
|
+
return /* @__PURE__ */ jsx("div", { className: "min-h-svh flex items-center justify-center p-4", children: /* @__PURE__ */ jsxs(Card, { className: "w-full max-w-sm", children: [
|
|
98
|
+
/* @__PURE__ */ jsxs(CardHeader, { children: [
|
|
99
|
+
/* @__PURE__ */ jsx(CardTitle, { children: t("auth.reset.token.expiredTitle", "This reset link is no longer valid") }),
|
|
100
|
+
/* @__PURE__ */ jsx(CardDescription, { children: t("auth.reset.token.expiredSubtitle", "It has already been used or has expired. Request a new link to set your password.") })
|
|
101
|
+
] }),
|
|
102
|
+
/* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsxs("div", { className: "grid gap-3", "data-auth-token-state": "expired", children: [
|
|
103
|
+
/* @__PURE__ */ jsx(Button, { asChild: true, className: "w-full", children: /* @__PURE__ */ jsx(Link, { href: "/reset", children: t("auth.reset.token.requestNew", "Request a new link") }) }),
|
|
104
|
+
/* @__PURE__ */ jsx("div", { className: "text-center", children: /* @__PURE__ */ jsx(Link, { href: "/login", className: "text-xs text-muted-foreground underline", children: t("auth.reset.backToLogin", "Back to login") }) })
|
|
105
|
+
] }) })
|
|
106
|
+
] }) });
|
|
107
|
+
}
|
|
62
108
|
return /* @__PURE__ */ jsx("div", { className: "min-h-svh flex items-center justify-center p-4", children: /* @__PURE__ */ jsxs(Card, { className: "w-full max-w-sm", children: [
|
|
63
109
|
/* @__PURE__ */ jsxs(CardHeader, { children: [
|
|
64
110
|
/* @__PURE__ */ jsx(CardTitle, { children: t("auth.reset.title", "Set a new password") }),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/auth/frontend/reset/%5Btoken%5D/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@open-mercato/ui/primitives/card'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { PasswordInput } from '@open-mercato/ui/primitives/password-input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { useEffect, useState } from 'react'\nimport { useRouter } from 'next/navigation'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { formatPasswordRequirements, getPasswordPolicy, validatePassword } from '@open-mercato/shared/lib/auth/passwordPolicy'\n\nexport default function ResetWithTokenPage({ params }: { params: { token: string } }) {\n const router = useRouter()\n const t = useT()\n const [error, setError] = useState<string | null>(null)\n const [submitting, setSubmitting] = useState(false)\n const [clientReady, setClientReady] = useState(false)\n const passwordPolicy = getPasswordPolicy()\n const passwordRequirements = formatPasswordRequirements(passwordPolicy, t)\n const passwordDescription = passwordRequirements\n ? t('auth.password.requirements.help', 'Password requirements: {requirements}', { requirements: passwordRequirements })\n : ''\n\n useEffect(() => {\n setClientReady(true)\n }, [])\n\n async function onSubmit(e: React.FormEvent<HTMLFormElement>) {\n e.preventDefault()\n setError(null)\n const form = new FormData(e.currentTarget)\n const password = String(form.get('password') ?? '')\n const confirmPassword = String(form.get('confirmPassword') ?? '')\n\n if (!password) {\n setError(t('auth.profile.form.errors.newPasswordRequired', 'New password is required.'))\n return\n }\n if (!confirmPassword) {\n setError(t('auth.profile.form.errors.confirmPasswordRequired', 'Please confirm the new password.'))\n return\n }\n if (password !== confirmPassword) {\n setError(t('auth.profile.form.errors.passwordMismatch', 'Passwords do not match.'))\n return\n }\n if (!validatePassword(password, passwordPolicy).ok) {\n setError(t('auth.profile.form.errors.passwordRequirements', 'Password must meet the requirements.'))\n return\n }\n\n setSubmitting(true)\n try {\n form.set('token', params.token)\n const { ok, result } = await apiCall<{ ok?: boolean; error?: string; redirect?: string }>(\n '/api/auth/reset/confirm',\n { method: 'POST', body: form },\n )\n if (!ok || result?.ok === false) {\n setError(result?.error || t('auth.reset.errors.failed', 'Unable to reset password'))\n return\n }\n router.replace(result?.redirect || '/login')\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>\n <CardTitle>{t('auth.reset.title', 'Set a new password')}</CardTitle>\n <CardDescription>{t('auth.reset.subtitle', 'Choose a strong password for your account.')}</CardDescription>\n </CardHeader>\n <CardContent>\n <form className=\"grid gap-3\" onSubmit={onSubmit} data-auth-ready={clientReady ? '1' : '0'}>\n {error && <div className=\"text-sm text-status-error-text\">{error}</div>}\n <div className=\"grid gap-1\">\n <Label htmlFor=\"password\">{t('auth.reset.form.password', 'New password')}</Label>\n <PasswordInput id=\"password\" name=\"password\" required minLength={passwordPolicy.minLength} autoComplete=\"new-password\" />\n {passwordDescription ? (\n <p className=\"text-xs text-muted-foreground\">{passwordDescription}</p>\n ) : null}\n </div>\n <div className=\"grid gap-1\">\n <Label htmlFor=\"confirmPassword\">{t('auth.profile.form.confirmPassword', 'Confirm new password')}</Label>\n <PasswordInput\n id=\"confirmPassword\"\n name=\"confirmPassword\"\n required\n minLength={passwordPolicy.minLength}\n autoComplete=\"new-password\"\n />\n </div>\n <Button type=\"submit\" className=\"mt-2 w-full\" disabled={submitting}>\n {submitting ? t('auth.reset.form.loading', '...') : t('auth.reset.form.submit', 'Update password')}\n </Button>\n </form>\n </CardContent>\n </Card>\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@open-mercato/ui/primitives/card'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { PasswordInput } from '@open-mercato/ui/primitives/password-input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { useEffect, useState } from 'react'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { formatPasswordRequirements, getPasswordPolicy, validatePassword } from '@open-mercato/shared/lib/auth/passwordPolicy'\n\ntype TokenState = 'checking' | 'usable' | 'expired'\n\nexport default function ResetWithTokenPage({ params }: { params: { token: string } }) {\n const router = useRouter()\n const t = useT()\n const [error, setError] = useState<string | null>(null)\n const [submitting, setSubmitting] = useState(false)\n const [clientReady, setClientReady] = useState(false)\n const [tokenState, setTokenState] = useState<TokenState>('checking')\n const passwordPolicy = getPasswordPolicy()\n const passwordRequirements = formatPasswordRequirements(passwordPolicy, t)\n const passwordDescription = passwordRequirements\n ? t('auth.password.requirements.help', 'Password requirements: {requirements}', { requirements: passwordRequirements })\n : ''\n\n useEffect(() => {\n setClientReady(true)\n }, [])\n\n useEffect(() => {\n let cancelled = false\n async function checkToken() {\n const body = new FormData()\n body.set('token', params.token)\n // Fail open whenever the check cannot answer \u2014 an unreachable or failing\n // endpoint must not strand a user holding a perfectly good link, and the\n // confirm endpoint still rejects a dead token on submit.\n try {\n const { ok, result } = await apiCall<{ ok?: boolean; valid?: boolean }>(\n '/api/auth/reset/validate',\n { method: 'POST', body },\n )\n if (cancelled) return\n setTokenState(ok && result?.valid === false ? 'expired' : 'usable')\n } catch {\n if (cancelled) return\n setTokenState('usable')\n }\n }\n void checkToken()\n return () => { cancelled = true }\n }, [params.token])\n\n async function onSubmit(e: React.FormEvent<HTMLFormElement>) {\n e.preventDefault()\n setError(null)\n const form = new FormData(e.currentTarget)\n const password = String(form.get('password') ?? '')\n const confirmPassword = String(form.get('confirmPassword') ?? '')\n\n if (!password) {\n setError(t('auth.profile.form.errors.newPasswordRequired', 'New password is required.'))\n return\n }\n if (!confirmPassword) {\n setError(t('auth.profile.form.errors.confirmPasswordRequired', 'Please confirm the new password.'))\n return\n }\n if (password !== confirmPassword) {\n setError(t('auth.profile.form.errors.passwordMismatch', 'Passwords do not match.'))\n return\n }\n if (!validatePassword(password, passwordPolicy).ok) {\n setError(t('auth.profile.form.errors.passwordRequirements', 'Password must meet the requirements.'))\n return\n }\n\n setSubmitting(true)\n try {\n form.set('token', params.token)\n const { ok, result } = await apiCall<{ ok?: boolean; error?: string; redirect?: string }>(\n '/api/auth/reset/confirm',\n { method: 'POST', body: form },\n )\n if (!ok || result?.ok === false) {\n setError(result?.error || t('auth.reset.errors.failed', 'Unable to reset password'))\n return\n }\n router.replace(result?.redirect || '/login')\n } finally {\n setSubmitting(false)\n }\n }\n\n if (tokenState === 'checking') {\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>\n <CardTitle>{t('auth.reset.title', 'Set a new password')}</CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground\" data-auth-token-state=\"checking\">\n <Spinner size=\"sm\" />\n <span>{t('auth.reset.token.checking', 'Checking your reset link...')}</span>\n </div>\n </CardContent>\n </Card>\n </div>\n )\n }\n\n if (tokenState === 'expired') {\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>\n <CardTitle>{t('auth.reset.token.expiredTitle', 'This reset link is no longer valid')}</CardTitle>\n <CardDescription>\n {t('auth.reset.token.expiredSubtitle', 'It has already been used or has expired. Request a new link to set your password.')}\n </CardDescription>\n </CardHeader>\n <CardContent>\n <div className=\"grid gap-3\" data-auth-token-state=\"expired\">\n <Button asChild className=\"w-full\">\n <Link href=\"/reset\">{t('auth.reset.token.requestNew', 'Request a new link')}</Link>\n </Button>\n <div className=\"text-center\">\n <Link href=\"/login\" className=\"text-xs text-muted-foreground underline\">\n {t('auth.reset.backToLogin', 'Back to login')}\n </Link>\n </div>\n </div>\n </CardContent>\n </Card>\n </div>\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>\n <CardTitle>{t('auth.reset.title', 'Set a new password')}</CardTitle>\n <CardDescription>{t('auth.reset.subtitle', 'Choose a strong password for your account.')}</CardDescription>\n </CardHeader>\n <CardContent>\n <form className=\"grid gap-3\" onSubmit={onSubmit} data-auth-ready={clientReady ? '1' : '0'}>\n {error && <div className=\"text-sm text-status-error-text\">{error}</div>}\n <div className=\"grid gap-1\">\n <Label htmlFor=\"password\">{t('auth.reset.form.password', 'New password')}</Label>\n <PasswordInput id=\"password\" name=\"password\" required minLength={passwordPolicy.minLength} autoComplete=\"new-password\" />\n {passwordDescription ? (\n <p className=\"text-xs text-muted-foreground\">{passwordDescription}</p>\n ) : null}\n </div>\n <div className=\"grid gap-1\">\n <Label htmlFor=\"confirmPassword\">{t('auth.profile.form.confirmPassword', 'Confirm new password')}</Label>\n <PasswordInput\n id=\"confirmPassword\"\n name=\"confirmPassword\"\n required\n minLength={passwordPolicy.minLength}\n autoComplete=\"new-password\"\n />\n </div>\n <Button type=\"submit\" className=\"mt-2 w-full\" disabled={submitting}>\n {submitting ? t('auth.reset.form.loading', '...') : t('auth.reset.form.submit', 'Update password')}\n </Button>\n </form>\n </CardContent>\n </Card>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -110,11 +110,25 @@ class AuthService {
|
|
|
110
110
|
await this.em.persist(row).flush();
|
|
111
111
|
return { user, token: rawToken };
|
|
112
112
|
}
|
|
113
|
-
async
|
|
113
|
+
async findUnconsumedPasswordReset(token) {
|
|
114
114
|
const now = /* @__PURE__ */ new Date();
|
|
115
115
|
const hashedToken = hashAuthToken(token);
|
|
116
116
|
const row = await this.em.findOne(PasswordReset, { token: hashedToken });
|
|
117
|
-
if (!row
|
|
117
|
+
if (!row) return null;
|
|
118
|
+
if (row.usedAt && row.usedAt <= now) return null;
|
|
119
|
+
if (row.expiresAt <= now) return null;
|
|
120
|
+
return row;
|
|
121
|
+
}
|
|
122
|
+
async isPasswordResetTokenValid(token) {
|
|
123
|
+
const row = await this.findUnconsumedPasswordReset(token);
|
|
124
|
+
if (!row) return false;
|
|
125
|
+
const user = await findOneWithDecryption(this.em, User, { id: row.user.id, deletedAt: null });
|
|
126
|
+
return user != null;
|
|
127
|
+
}
|
|
128
|
+
async confirmPasswordReset(token, newPassword) {
|
|
129
|
+
const now = /* @__PURE__ */ new Date();
|
|
130
|
+
const row = await this.findUnconsumedPasswordReset(token);
|
|
131
|
+
if (!row) return null;
|
|
118
132
|
const affected = await this.em.nativeUpdate(
|
|
119
133
|
PasswordReset,
|
|
120
134
|
{ id: row.id, usedAt: null },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/auth/services/authService.ts"],
|
|
4
|
-
"sourcesContent": ["import { EntityManager } from '@mikro-orm/postgresql'\nimport { compare, hash } from 'bcryptjs'\nimport { User, Role, UserRole, Session, PasswordReset } from '@open-mercato/core/modules/auth/data/entities'\nimport { emailHashLookupValues } from '@open-mercato/core/modules/auth/lib/emailHash'\nimport { generateAuthToken, hashAuthToken } from '@open-mercato/core/modules/auth/lib/tokenHash'\nimport { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\n// A fixed, valid bcrypt hash (cost 10) of a throwaway value no real password\n// can match. verifyPassword compares against it whenever the user is missing or\n// has no password hash, so a failed login spends the same bcrypt CPU time\n// regardless of whether the account exists \u2014 closing the timing side channel\n// for account enumeration (issue #2242).\nconst TIMING_EQUALIZER_PASSWORD_HASH = '$2b$10$OcZrhmZpIzJOjkfwUrk7d.Nl0eHNzOvalBcBlt5Ran.4lj8R3HZg6'\n\nexport class AuthService {\n constructor(private em: EntityManager) {}\n\n async findUserByEmail(email: string) {\n const emailHashes = emailHashLookupValues(email)\n return findOneWithDecryption(this.em, User, {\n deletedAt: null,\n $or: [\n { email },\n { emailHash: { $in: emailHashes } },\n ],\n } as any)\n }\n\n async findUsersByEmail(email: string) {\n const emailHashes = emailHashLookupValues(email)\n return findWithDecryption(this.em, User, {\n deletedAt: null,\n $or: [\n { email },\n { emailHash: { $in: emailHashes } },\n ],\n } as any)\n }\n\n async findUserByEmailAndTenant(email: string, tenantId: string) {\n const emailHashes = emailHashLookupValues(email)\n return findOneWithDecryption(\n this.em,\n User,\n {\n tenantId,\n deletedAt: null,\n $or: [\n { email },\n { emailHash: { $in: emailHashes } },\n ],\n } as any,\n undefined,\n { tenantId },\n )\n }\n\n async verifyPassword(user: User | null, password: string) {\n const storedHash = user?.passwordHash ?? null\n // Always run a bcrypt comparison \u2014 against a fixed dummy hash when the user\n // is absent or has no password \u2014 so login latency does not reveal whether\n // the account exists (timing-based enumeration, issue #2242).\n const matched = await compare(password, storedHash ?? TIMING_EQUALIZER_PASSWORD_HASH)\n return storedHash !== null && matched\n }\n\n async updateLastLoginAt(user: User) {\n const now = new Date()\n // Use native update to avoid flushing unrelated entities that might be pending in this EM\n await this.em.nativeUpdate(User, { id: user.id }, { lastLoginAt: now })\n user.lastLoginAt = now\n }\n\n async getUserRoles(user: User, tenantId?: string | null): Promise<string[]> {\n const resolvedTenantId = tenantId ?? user.tenantId ?? null\n if (!resolvedTenantId) return []\n const links = await findWithDecryption(\n this.em,\n UserRole,\n { user, deletedAt: null, role: { tenantId: resolvedTenantId, deletedAt: null } as any },\n { populate: ['role'] },\n { tenantId: resolvedTenantId, organizationId: user.organizationId ?? null },\n )\n // A populated `role` can still be null when the link points at a soft-deleted\n // role (the Role soft-delete filter suppresses hydration), e.g. an admin link\n // orphaned by a re-seed during interrupted-provisioning recovery. Dropping such\n // links keeps role resolution from throwing on the login / session-refresh hot\n // path, mirroring resolveCanonicalStaffAuthContext in lib/sessionIntegrity.ts.\n return links\n .map((l) => l.role)\n .filter((role): role is Role => !!role)\n .map((role) => role.name)\n .filter((name): name is string => typeof name === 'string' && name.trim().length > 0)\n }\n\n\n async createSession(user: User, expiresAt: Date): Promise<{ session: Session; token: string }> {\n const rawToken = generateAuthToken()\n const tokenHash = hashAuthToken(rawToken)\n const sess = this.em.create(Session as any, { user, token: tokenHash, expiresAt, createdAt: new Date() } as any)\n await this.em.persist(sess).flush()\n return { session: sess as Session, token: rawToken }\n }\n\n async deleteSessionByToken(token: string) {\n const hashedToken = hashAuthToken(token)\n await this.em.nativeDelete(Session, { token: hashedToken })\n }\n\n async deleteSessionById(sessionId: string) {\n await this.em.nativeDelete(Session, { id: sessionId })\n }\n\n async findActiveSessionById(sessionId: string): Promise<Session | null> {\n const session = await this.em.findOne(Session, { id: sessionId, deletedAt: null })\n if (!session) return null\n if (session.expiresAt.getTime() < Date.now()) return null\n return session\n }\n\n async deleteAllUserSessions(userId: string) {\n await this.em.nativeDelete(Session, { user: userId })\n }\n\n async refreshFromSessionToken(token: string) {\n const now = new Date()\n const hashedToken = hashAuthToken(token)\n const sess = await this.em.findOne(Session, { token: hashedToken })\n if (!sess || sess.expiresAt <= now) return null\n const user = await findOneWithDecryption(this.em, User, { id: sess.user.id, deletedAt: null })\n if (!user) return null\n const roles = await this.getUserRoles(user, user.tenantId ?? null)\n return { user, roles, session: sess }\n }\n\n async requestPasswordReset(email: string) {\n const user = await this.findUserByEmail(email)\n if (!user) return null\n const rawToken = generateAuthToken()\n const tokenHash = hashAuthToken(rawToken)\n const expiresAt = new Date(Date.now() + 60 * 60 * 1000)\n const row = this.em.create(PasswordReset as any, { user, token: tokenHash, expiresAt, createdAt: new Date() } as any)\n await this.em.persist(row).flush()\n return { user, token: rawToken }\n }\n\n async
|
|
5
|
-
"mappings": "AACA,SAAS,SAAS,YAAY;AAC9B,SAAS,MAAY,UAAU,SAAS,qBAAqB;AAC7D,SAAS,6BAA6B;AACtC,SAAS,mBAAmB,qBAAqB;AACjD,SAAS,oBAAoB,6BAA6B;AAO1D,MAAM,iCAAiC;AAEhC,MAAM,YAAY;AAAA,EACvB,YAAoB,IAAmB;AAAnB;AAAA,EAAoB;AAAA,EAExC,MAAM,gBAAgB,OAAe;AACnC,UAAM,cAAc,sBAAsB,KAAK;AAC/C,WAAO,sBAAsB,KAAK,IAAI,MAAM;AAAA,MAC1C,WAAW;AAAA,MACX,KAAK;AAAA,QACH,EAAE,MAAM;AAAA,QACR,EAAE,WAAW,EAAE,KAAK,YAAY,EAAE;AAAA,MACpC;AAAA,IACF,CAAQ;AAAA,EACV;AAAA,EAEA,MAAM,iBAAiB,OAAe;AACpC,UAAM,cAAc,sBAAsB,KAAK;AAC/C,WAAO,mBAAmB,KAAK,IAAI,MAAM;AAAA,MACvC,WAAW;AAAA,MACX,KAAK;AAAA,QACH,EAAE,MAAM;AAAA,QACR,EAAE,WAAW,EAAE,KAAK,YAAY,EAAE;AAAA,MACpC;AAAA,IACF,CAAQ;AAAA,EACV;AAAA,EAEA,MAAM,yBAAyB,OAAe,UAAkB;AAC9D,UAAM,cAAc,sBAAsB,KAAK;AAC/C,WAAO;AAAA,MACL,KAAK;AAAA,MACL;AAAA,MACA;AAAA,QACE;AAAA,QACA,WAAW;AAAA,QACX,KAAK;AAAA,UACH,EAAE,MAAM;AAAA,UACR,EAAE,WAAW,EAAE,KAAK,YAAY,EAAE;AAAA,QACpC;AAAA,MACF;AAAA,MACA;AAAA,MACA,EAAE,SAAS;AAAA,IACb;AAAA,EACF;AAAA,EAEA,MAAM,eAAe,MAAmB,UAAkB;AACxD,UAAM,aAAa,MAAM,gBAAgB;AAIzC,UAAM,UAAU,MAAM,QAAQ,UAAU,cAAc,8BAA8B;AACpF,WAAO,eAAe,QAAQ;AAAA,EAChC;AAAA,EAEA,MAAM,kBAAkB,MAAY;AAClC,UAAM,MAAM,oBAAI,KAAK;AAErB,UAAM,KAAK,GAAG,aAAa,MAAM,EAAE,IAAI,KAAK,GAAG,GAAG,EAAE,aAAa,IAAI,CAAC;AACtE,SAAK,cAAc;AAAA,EACrB;AAAA,EAEA,MAAM,aAAa,MAAY,UAA6C;AAC1E,UAAM,mBAAmB,YAAY,KAAK,YAAY;AACtD,QAAI,CAAC,iBAAkB,QAAO,CAAC;AAC/B,UAAM,QAAQ,MAAM;AAAA,MAClB,KAAK;AAAA,MACL;AAAA,MACA,EAAE,MAAM,WAAW,MAAM,MAAM,EAAE,UAAU,kBAAkB,WAAW,KAAK,EAAS;AAAA,MACtF,EAAE,UAAU,CAAC,MAAM,EAAE;AAAA,MACrB,EAAE,UAAU,kBAAkB,gBAAgB,KAAK,kBAAkB,KAAK;AAAA,IAC5E;AAMA,WAAO,MACJ,IAAI,CAAC,MAAM,EAAE,IAAI,EACjB,OAAO,CAAC,SAAuB,CAAC,CAAC,IAAI,EACrC,IAAI,CAAC,SAAS,KAAK,IAAI,EACvB,OAAO,CAAC,SAAyB,OAAO,SAAS,YAAY,KAAK,KAAK,EAAE,SAAS,CAAC;AAAA,EACxF;AAAA,EAGA,MAAM,cAAc,MAAY,WAA+D;AAC7F,UAAM,WAAW,kBAAkB;AACnC,UAAM,YAAY,cAAc,QAAQ;AACxC,UAAM,OAAO,KAAK,GAAG,OAAO,SAAgB,EAAE,MAAM,OAAO,WAAW,WAAW,WAAW,oBAAI,KAAK,EAAE,CAAQ;AAC/G,UAAM,KAAK,GAAG,QAAQ,IAAI,EAAE,MAAM;AAClC,WAAO,EAAE,SAAS,MAAiB,OAAO,SAAS;AAAA,EACrD;AAAA,EAEA,MAAM,qBAAqB,OAAe;AACxC,UAAM,cAAc,cAAc,KAAK;AACvC,UAAM,KAAK,GAAG,aAAa,SAAS,EAAE,OAAO,YAAY,CAAC;AAAA,EAC5D;AAAA,EAEA,MAAM,kBAAkB,WAAmB;AACzC,UAAM,KAAK,GAAG,aAAa,SAAS,EAAE,IAAI,UAAU,CAAC;AAAA,EACvD;AAAA,EAEA,MAAM,sBAAsB,WAA4C;AACtE,UAAM,UAAU,MAAM,KAAK,GAAG,QAAQ,SAAS,EAAE,IAAI,WAAW,WAAW,KAAK,CAAC;AACjF,QAAI,CAAC,QAAS,QAAO;AACrB,QAAI,QAAQ,UAAU,QAAQ,IAAI,KAAK,IAAI,EAAG,QAAO;AACrD,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,sBAAsB,QAAgB;AAC1C,UAAM,KAAK,GAAG,aAAa,SAAS,EAAE,MAAM,OAAO,CAAC;AAAA,EACtD;AAAA,EAEA,MAAM,wBAAwB,OAAe;AAC3C,UAAM,MAAM,oBAAI,KAAK;AACrB,UAAM,cAAc,cAAc,KAAK;AACvC,UAAM,OAAO,MAAM,KAAK,GAAG,QAAQ,SAAS,EAAE,OAAO,YAAY,CAAC;AAClE,QAAI,CAAC,QAAQ,KAAK,aAAa,IAAK,QAAO;AAC3C,UAAM,OAAO,MAAM,sBAAsB,KAAK,IAAI,MAAM,EAAE,IAAI,KAAK,KAAK,IAAI,WAAW,KAAK,CAAC;AAC7F,QAAI,CAAC,KAAM,QAAO;AAClB,UAAM,QAAQ,MAAM,KAAK,aAAa,MAAM,KAAK,YAAY,IAAI;AACjE,WAAO,EAAE,MAAM,OAAO,SAAS,KAAK;AAAA,EACtC;AAAA,EAEA,MAAM,qBAAqB,OAAe;AACxC,UAAM,OAAO,MAAM,KAAK,gBAAgB,KAAK;AAC7C,QAAI,CAAC,KAAM,QAAO;AAClB,UAAM,WAAW,kBAAkB;AACnC,UAAM,YAAY,cAAc,QAAQ;AACxC,UAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,GAAI;AACtD,UAAM,MAAM,KAAK,GAAG,OAAO,eAAsB,EAAE,MAAM,OAAO,WAAW,WAAW,WAAW,oBAAI,KAAK,EAAE,CAAQ;AACpH,UAAM,KAAK,GAAG,QAAQ,GAAG,EAAE,MAAM;AACjC,WAAO,EAAE,MAAM,OAAO,SAAS;AAAA,EACjC;AAAA,EAEA,
|
|
4
|
+
"sourcesContent": ["import { EntityManager } from '@mikro-orm/postgresql'\nimport { compare, hash } from 'bcryptjs'\nimport { User, Role, UserRole, Session, PasswordReset } from '@open-mercato/core/modules/auth/data/entities'\nimport { emailHashLookupValues } from '@open-mercato/core/modules/auth/lib/emailHash'\nimport { generateAuthToken, hashAuthToken } from '@open-mercato/core/modules/auth/lib/tokenHash'\nimport { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\n// A fixed, valid bcrypt hash (cost 10) of a throwaway value no real password\n// can match. verifyPassword compares against it whenever the user is missing or\n// has no password hash, so a failed login spends the same bcrypt CPU time\n// regardless of whether the account exists \u2014 closing the timing side channel\n// for account enumeration (issue #2242).\nconst TIMING_EQUALIZER_PASSWORD_HASH = '$2b$10$OcZrhmZpIzJOjkfwUrk7d.Nl0eHNzOvalBcBlt5Ran.4lj8R3HZg6'\n\nexport class AuthService {\n constructor(private em: EntityManager) {}\n\n async findUserByEmail(email: string) {\n const emailHashes = emailHashLookupValues(email)\n return findOneWithDecryption(this.em, User, {\n deletedAt: null,\n $or: [\n { email },\n { emailHash: { $in: emailHashes } },\n ],\n } as any)\n }\n\n async findUsersByEmail(email: string) {\n const emailHashes = emailHashLookupValues(email)\n return findWithDecryption(this.em, User, {\n deletedAt: null,\n $or: [\n { email },\n { emailHash: { $in: emailHashes } },\n ],\n } as any)\n }\n\n async findUserByEmailAndTenant(email: string, tenantId: string) {\n const emailHashes = emailHashLookupValues(email)\n return findOneWithDecryption(\n this.em,\n User,\n {\n tenantId,\n deletedAt: null,\n $or: [\n { email },\n { emailHash: { $in: emailHashes } },\n ],\n } as any,\n undefined,\n { tenantId },\n )\n }\n\n async verifyPassword(user: User | null, password: string) {\n const storedHash = user?.passwordHash ?? null\n // Always run a bcrypt comparison \u2014 against a fixed dummy hash when the user\n // is absent or has no password \u2014 so login latency does not reveal whether\n // the account exists (timing-based enumeration, issue #2242).\n const matched = await compare(password, storedHash ?? TIMING_EQUALIZER_PASSWORD_HASH)\n return storedHash !== null && matched\n }\n\n async updateLastLoginAt(user: User) {\n const now = new Date()\n // Use native update to avoid flushing unrelated entities that might be pending in this EM\n await this.em.nativeUpdate(User, { id: user.id }, { lastLoginAt: now })\n user.lastLoginAt = now\n }\n\n async getUserRoles(user: User, tenantId?: string | null): Promise<string[]> {\n const resolvedTenantId = tenantId ?? user.tenantId ?? null\n if (!resolvedTenantId) return []\n const links = await findWithDecryption(\n this.em,\n UserRole,\n { user, deletedAt: null, role: { tenantId: resolvedTenantId, deletedAt: null } as any },\n { populate: ['role'] },\n { tenantId: resolvedTenantId, organizationId: user.organizationId ?? null },\n )\n // A populated `role` can still be null when the link points at a soft-deleted\n // role (the Role soft-delete filter suppresses hydration), e.g. an admin link\n // orphaned by a re-seed during interrupted-provisioning recovery. Dropping such\n // links keeps role resolution from throwing on the login / session-refresh hot\n // path, mirroring resolveCanonicalStaffAuthContext in lib/sessionIntegrity.ts.\n return links\n .map((l) => l.role)\n .filter((role): role is Role => !!role)\n .map((role) => role.name)\n .filter((name): name is string => typeof name === 'string' && name.trim().length > 0)\n }\n\n\n async createSession(user: User, expiresAt: Date): Promise<{ session: Session; token: string }> {\n const rawToken = generateAuthToken()\n const tokenHash = hashAuthToken(rawToken)\n const sess = this.em.create(Session as any, { user, token: tokenHash, expiresAt, createdAt: new Date() } as any)\n await this.em.persist(sess).flush()\n return { session: sess as Session, token: rawToken }\n }\n\n async deleteSessionByToken(token: string) {\n const hashedToken = hashAuthToken(token)\n await this.em.nativeDelete(Session, { token: hashedToken })\n }\n\n async deleteSessionById(sessionId: string) {\n await this.em.nativeDelete(Session, { id: sessionId })\n }\n\n async findActiveSessionById(sessionId: string): Promise<Session | null> {\n const session = await this.em.findOne(Session, { id: sessionId, deletedAt: null })\n if (!session) return null\n if (session.expiresAt.getTime() < Date.now()) return null\n return session\n }\n\n async deleteAllUserSessions(userId: string) {\n await this.em.nativeDelete(Session, { user: userId })\n }\n\n async refreshFromSessionToken(token: string) {\n const now = new Date()\n const hashedToken = hashAuthToken(token)\n const sess = await this.em.findOne(Session, { token: hashedToken })\n if (!sess || sess.expiresAt <= now) return null\n const user = await findOneWithDecryption(this.em, User, { id: sess.user.id, deletedAt: null })\n if (!user) return null\n const roles = await this.getUserRoles(user, user.tenantId ?? null)\n return { user, roles, session: sess }\n }\n\n async requestPasswordReset(email: string) {\n const user = await this.findUserByEmail(email)\n if (!user) return null\n const rawToken = generateAuthToken()\n const tokenHash = hashAuthToken(rawToken)\n const expiresAt = new Date(Date.now() + 60 * 60 * 1000)\n const row = this.em.create(PasswordReset as any, { user, token: tokenHash, expiresAt, createdAt: new Date() } as any)\n await this.em.persist(row).flush()\n return { user, token: rawToken }\n }\n\n private async findUnconsumedPasswordReset(token: string): Promise<PasswordReset | null> {\n const now = new Date()\n const hashedToken = hashAuthToken(token)\n const row = await this.em.findOne(PasswordReset, { token: hashedToken })\n if (!row) return null\n if (row.usedAt && row.usedAt <= now) return null\n if (row.expiresAt <= now) return null\n return row\n }\n\n async isPasswordResetTokenValid(token: string): Promise<boolean> {\n const row = await this.findUnconsumedPasswordReset(token)\n if (!row) return false\n const user = await findOneWithDecryption(this.em, User, { id: row.user.id, deletedAt: null })\n return user != null\n }\n\n async confirmPasswordReset(token: string, newPassword: string): Promise<User | null> {\n const now = new Date()\n const row = await this.findUnconsumedPasswordReset(token)\n if (!row) return null\n\n // Atomic compare-and-set: only mark used if still unused \u2014 prevents token replay under concurrency\n const affected = await this.em.nativeUpdate(\n PasswordReset,\n { id: row.id, usedAt: null },\n { usedAt: now },\n )\n if (affected === 0) return null\n\n const user = await findOneWithDecryption(this.em, User, { id: row.user.id, deletedAt: null })\n if (!user) return null\n user.passwordHash = await hash(newPassword, 10)\n await this.em.flush()\n await this.deleteAllUserSessions(String(user.id))\n return user\n }\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,SAAS,YAAY;AAC9B,SAAS,MAAY,UAAU,SAAS,qBAAqB;AAC7D,SAAS,6BAA6B;AACtC,SAAS,mBAAmB,qBAAqB;AACjD,SAAS,oBAAoB,6BAA6B;AAO1D,MAAM,iCAAiC;AAEhC,MAAM,YAAY;AAAA,EACvB,YAAoB,IAAmB;AAAnB;AAAA,EAAoB;AAAA,EAExC,MAAM,gBAAgB,OAAe;AACnC,UAAM,cAAc,sBAAsB,KAAK;AAC/C,WAAO,sBAAsB,KAAK,IAAI,MAAM;AAAA,MAC1C,WAAW;AAAA,MACX,KAAK;AAAA,QACH,EAAE,MAAM;AAAA,QACR,EAAE,WAAW,EAAE,KAAK,YAAY,EAAE;AAAA,MACpC;AAAA,IACF,CAAQ;AAAA,EACV;AAAA,EAEA,MAAM,iBAAiB,OAAe;AACpC,UAAM,cAAc,sBAAsB,KAAK;AAC/C,WAAO,mBAAmB,KAAK,IAAI,MAAM;AAAA,MACvC,WAAW;AAAA,MACX,KAAK;AAAA,QACH,EAAE,MAAM;AAAA,QACR,EAAE,WAAW,EAAE,KAAK,YAAY,EAAE;AAAA,MACpC;AAAA,IACF,CAAQ;AAAA,EACV;AAAA,EAEA,MAAM,yBAAyB,OAAe,UAAkB;AAC9D,UAAM,cAAc,sBAAsB,KAAK;AAC/C,WAAO;AAAA,MACL,KAAK;AAAA,MACL;AAAA,MACA;AAAA,QACE;AAAA,QACA,WAAW;AAAA,QACX,KAAK;AAAA,UACH,EAAE,MAAM;AAAA,UACR,EAAE,WAAW,EAAE,KAAK,YAAY,EAAE;AAAA,QACpC;AAAA,MACF;AAAA,MACA;AAAA,MACA,EAAE,SAAS;AAAA,IACb;AAAA,EACF;AAAA,EAEA,MAAM,eAAe,MAAmB,UAAkB;AACxD,UAAM,aAAa,MAAM,gBAAgB;AAIzC,UAAM,UAAU,MAAM,QAAQ,UAAU,cAAc,8BAA8B;AACpF,WAAO,eAAe,QAAQ;AAAA,EAChC;AAAA,EAEA,MAAM,kBAAkB,MAAY;AAClC,UAAM,MAAM,oBAAI,KAAK;AAErB,UAAM,KAAK,GAAG,aAAa,MAAM,EAAE,IAAI,KAAK,GAAG,GAAG,EAAE,aAAa,IAAI,CAAC;AACtE,SAAK,cAAc;AAAA,EACrB;AAAA,EAEA,MAAM,aAAa,MAAY,UAA6C;AAC1E,UAAM,mBAAmB,YAAY,KAAK,YAAY;AACtD,QAAI,CAAC,iBAAkB,QAAO,CAAC;AAC/B,UAAM,QAAQ,MAAM;AAAA,MAClB,KAAK;AAAA,MACL;AAAA,MACA,EAAE,MAAM,WAAW,MAAM,MAAM,EAAE,UAAU,kBAAkB,WAAW,KAAK,EAAS;AAAA,MACtF,EAAE,UAAU,CAAC,MAAM,EAAE;AAAA,MACrB,EAAE,UAAU,kBAAkB,gBAAgB,KAAK,kBAAkB,KAAK;AAAA,IAC5E;AAMA,WAAO,MACJ,IAAI,CAAC,MAAM,EAAE,IAAI,EACjB,OAAO,CAAC,SAAuB,CAAC,CAAC,IAAI,EACrC,IAAI,CAAC,SAAS,KAAK,IAAI,EACvB,OAAO,CAAC,SAAyB,OAAO,SAAS,YAAY,KAAK,KAAK,EAAE,SAAS,CAAC;AAAA,EACxF;AAAA,EAGA,MAAM,cAAc,MAAY,WAA+D;AAC7F,UAAM,WAAW,kBAAkB;AACnC,UAAM,YAAY,cAAc,QAAQ;AACxC,UAAM,OAAO,KAAK,GAAG,OAAO,SAAgB,EAAE,MAAM,OAAO,WAAW,WAAW,WAAW,oBAAI,KAAK,EAAE,CAAQ;AAC/G,UAAM,KAAK,GAAG,QAAQ,IAAI,EAAE,MAAM;AAClC,WAAO,EAAE,SAAS,MAAiB,OAAO,SAAS;AAAA,EACrD;AAAA,EAEA,MAAM,qBAAqB,OAAe;AACxC,UAAM,cAAc,cAAc,KAAK;AACvC,UAAM,KAAK,GAAG,aAAa,SAAS,EAAE,OAAO,YAAY,CAAC;AAAA,EAC5D;AAAA,EAEA,MAAM,kBAAkB,WAAmB;AACzC,UAAM,KAAK,GAAG,aAAa,SAAS,EAAE,IAAI,UAAU,CAAC;AAAA,EACvD;AAAA,EAEA,MAAM,sBAAsB,WAA4C;AACtE,UAAM,UAAU,MAAM,KAAK,GAAG,QAAQ,SAAS,EAAE,IAAI,WAAW,WAAW,KAAK,CAAC;AACjF,QAAI,CAAC,QAAS,QAAO;AACrB,QAAI,QAAQ,UAAU,QAAQ,IAAI,KAAK,IAAI,EAAG,QAAO;AACrD,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,sBAAsB,QAAgB;AAC1C,UAAM,KAAK,GAAG,aAAa,SAAS,EAAE,MAAM,OAAO,CAAC;AAAA,EACtD;AAAA,EAEA,MAAM,wBAAwB,OAAe;AAC3C,UAAM,MAAM,oBAAI,KAAK;AACrB,UAAM,cAAc,cAAc,KAAK;AACvC,UAAM,OAAO,MAAM,KAAK,GAAG,QAAQ,SAAS,EAAE,OAAO,YAAY,CAAC;AAClE,QAAI,CAAC,QAAQ,KAAK,aAAa,IAAK,QAAO;AAC3C,UAAM,OAAO,MAAM,sBAAsB,KAAK,IAAI,MAAM,EAAE,IAAI,KAAK,KAAK,IAAI,WAAW,KAAK,CAAC;AAC7F,QAAI,CAAC,KAAM,QAAO;AAClB,UAAM,QAAQ,MAAM,KAAK,aAAa,MAAM,KAAK,YAAY,IAAI;AACjE,WAAO,EAAE,MAAM,OAAO,SAAS,KAAK;AAAA,EACtC;AAAA,EAEA,MAAM,qBAAqB,OAAe;AACxC,UAAM,OAAO,MAAM,KAAK,gBAAgB,KAAK;AAC7C,QAAI,CAAC,KAAM,QAAO;AAClB,UAAM,WAAW,kBAAkB;AACnC,UAAM,YAAY,cAAc,QAAQ;AACxC,UAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,GAAI;AACtD,UAAM,MAAM,KAAK,GAAG,OAAO,eAAsB,EAAE,MAAM,OAAO,WAAW,WAAW,WAAW,oBAAI,KAAK,EAAE,CAAQ;AACpH,UAAM,KAAK,GAAG,QAAQ,GAAG,EAAE,MAAM;AACjC,WAAO,EAAE,MAAM,OAAO,SAAS;AAAA,EACjC;AAAA,EAEA,MAAc,4BAA4B,OAA8C;AACtF,UAAM,MAAM,oBAAI,KAAK;AACrB,UAAM,cAAc,cAAc,KAAK;AACvC,UAAM,MAAM,MAAM,KAAK,GAAG,QAAQ,eAAe,EAAE,OAAO,YAAY,CAAC;AACvE,QAAI,CAAC,IAAK,QAAO;AACjB,QAAI,IAAI,UAAU,IAAI,UAAU,IAAK,QAAO;AAC5C,QAAI,IAAI,aAAa,IAAK,QAAO;AACjC,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,0BAA0B,OAAiC;AAC/D,UAAM,MAAM,MAAM,KAAK,4BAA4B,KAAK;AACxD,QAAI,CAAC,IAAK,QAAO;AACjB,UAAM,OAAO,MAAM,sBAAsB,KAAK,IAAI,MAAM,EAAE,IAAI,IAAI,KAAK,IAAI,WAAW,KAAK,CAAC;AAC5F,WAAO,QAAQ;AAAA,EACjB;AAAA,EAEA,MAAM,qBAAqB,OAAe,aAA2C;AACnF,UAAM,MAAM,oBAAI,KAAK;AACrB,UAAM,MAAM,MAAM,KAAK,4BAA4B,KAAK;AACxD,QAAI,CAAC,IAAK,QAAO;AAGjB,UAAM,WAAW,MAAM,KAAK,GAAG;AAAA,MAC7B;AAAA,MACA,EAAE,IAAI,IAAI,IAAI,QAAQ,KAAK;AAAA,MAC3B,EAAE,QAAQ,IAAI;AAAA,IAChB;AACA,QAAI,aAAa,EAAG,QAAO;AAE3B,UAAM,OAAO,MAAM,sBAAsB,KAAK,IAAI,MAAM,EAAE,IAAI,IAAI,KAAK,IAAI,WAAW,KAAK,CAAC;AAC5F,QAAI,CAAC,KAAM,QAAO;AAClB,SAAK,eAAe,MAAM,KAAK,aAAa,EAAE;AAC9C,UAAM,KAAK,GAAG,MAAM;AACpB,UAAM,KAAK,sBAAsB,OAAO,KAAK,EAAE,CAAC;AAChD,WAAO;AAAA,EACT;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -18,13 +18,19 @@ function useEmailDuplicateCheck(email, { recordId, disabled, debounceMs = DEFAUL
|
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
20
|
const normalized = trimmed.toLowerCase();
|
|
21
|
+
const excludedId = typeof recordId === "string" && recordId.trim().length ? recordId.trim() : null;
|
|
21
22
|
let cancelled = false;
|
|
22
23
|
const controller = new AbortController();
|
|
23
24
|
const timer = window.setTimeout(async () => {
|
|
24
25
|
setChecking(true);
|
|
25
26
|
try {
|
|
26
|
-
const
|
|
27
|
-
|
|
27
|
+
const params = [
|
|
28
|
+
matchMode === "prefix" ? `emailStartsWith=${encodeURIComponent(normalized)}` : `email=${encodeURIComponent(normalized)}`,
|
|
29
|
+
"pageSize=5",
|
|
30
|
+
"page=1"
|
|
31
|
+
];
|
|
32
|
+
if (excludedId) params.push(`excludeIds=${encodeURIComponent(excludedId)}`);
|
|
33
|
+
const call = await apiCall(`/api/customers/people?${params.join("&")}`, {
|
|
28
34
|
signal: controller.signal
|
|
29
35
|
});
|
|
30
36
|
if (!call.ok) {
|
|
@@ -41,7 +47,7 @@ function useEmailDuplicateCheck(email, { recordId, disabled, debounceMs = DEFAUL
|
|
|
41
47
|
const emailValue = typeof item.primary_email === "string" ? item.primary_email.toLowerCase() : null;
|
|
42
48
|
return id && displayName && emailValue ? { id, displayName, email: emailValue } : null;
|
|
43
49
|
}).filter((entry) => !!entry).find((entry) => {
|
|
44
|
-
if (entry.id ===
|
|
50
|
+
if (excludedId && entry.id === excludedId) return false;
|
|
45
51
|
return matchMode === "prefix" ? entry.email.startsWith(normalized) : entry.email === normalized;
|
|
46
52
|
}) ?? null;
|
|
47
53
|
if (!cancelled) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customers/backend/hooks/useEmailDuplicateCheck.ts"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\n\nexport type EmailDuplicateMatch = {\n id: string\n displayName: string\n email: string\n}\n\ntype UseEmailDuplicateCheckOptions = {\n recordId?: string | null\n disabled?: boolean\n debounceMs?: number\n matchMode?: 'exact' | 'prefix'\n}\n\nconst DEFAULT_DEBOUNCE_MS = 300\n\nexport function useEmailDuplicateCheck(\n email: string | null | undefined,\n { recordId, disabled, debounceMs = DEFAULT_DEBOUNCE_MS, matchMode = 'exact' }: UseEmailDuplicateCheckOptions = {}\n) {\n const [duplicate, setDuplicate] = React.useState<EmailDuplicateMatch | null>(null)\n const [checking, setChecking] = React.useState(false)\n\n React.useEffect(() => {\n if (disabled) {\n setDuplicate(null)\n setChecking(false)\n return\n }\n\n const trimmed = typeof email === 'string' ? email.trim() : ''\n if (!trimmed.length) {\n setDuplicate(null)\n setChecking(false)\n return\n }\n\n const normalized = trimmed.toLowerCase()\n let cancelled = false\n const controller = new AbortController()\n const timer = window.setTimeout(async () => {\n setChecking(true)\n try {\n const
|
|
5
|
-
"mappings": ";AAEA,YAAY,WAAW;AACvB,SAAS,eAAe;AAexB,MAAM,sBAAsB;AAErB,SAAS,uBACd,OACA,EAAE,UAAU,UAAU,aAAa,qBAAqB,YAAY,QAAQ,IAAmC,CAAC,GAChH;AACA,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAqC,IAAI;AACjF,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAAS,KAAK;AAEpD,QAAM,UAAU,MAAM;AACpB,QAAI,UAAU;AACZ,mBAAa,IAAI;AACjB,kBAAY,KAAK;AACjB;AAAA,IACF;AAEA,UAAM,UAAU,OAAO,UAAU,WAAW,MAAM,KAAK,IAAI;AAC3D,QAAI,CAAC,QAAQ,QAAQ;AACnB,mBAAa,IAAI;AACjB,kBAAY,KAAK;AACjB;AAAA,IACF;AAEA,UAAM,aAAa,QAAQ,YAAY;AACvC,QAAI,YAAY;AAChB,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,QAAQ,OAAO,WAAW,YAAY;AAC1C,kBAAY,IAAI;AAChB,UAAI;AACF,cAAM,
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\n\nexport type EmailDuplicateMatch = {\n id: string\n displayName: string\n email: string\n}\n\ntype UseEmailDuplicateCheckOptions = {\n recordId?: string | null\n disabled?: boolean\n debounceMs?: number\n matchMode?: 'exact' | 'prefix'\n}\n\nconst DEFAULT_DEBOUNCE_MS = 300\n\nexport function useEmailDuplicateCheck(\n email: string | null | undefined,\n { recordId, disabled, debounceMs = DEFAULT_DEBOUNCE_MS, matchMode = 'exact' }: UseEmailDuplicateCheckOptions = {}\n) {\n const [duplicate, setDuplicate] = React.useState<EmailDuplicateMatch | null>(null)\n const [checking, setChecking] = React.useState(false)\n\n React.useEffect(() => {\n if (disabled) {\n setDuplicate(null)\n setChecking(false)\n return\n }\n\n const trimmed = typeof email === 'string' ? email.trim() : ''\n if (!trimmed.length) {\n setDuplicate(null)\n setChecking(false)\n return\n }\n\n const normalized = trimmed.toLowerCase()\n const excludedId = typeof recordId === 'string' && recordId.trim().length ? recordId.trim() : null\n let cancelled = false\n const controller = new AbortController()\n const timer = window.setTimeout(async () => {\n setChecking(true)\n try {\n const params = [\n matchMode === 'prefix'\n ? `emailStartsWith=${encodeURIComponent(normalized)}`\n : `email=${encodeURIComponent(normalized)}`,\n 'pageSize=5',\n 'page=1',\n ]\n // The record being edited is never its own duplicate (#5534). Excluding it in the\n // query \u2014 not only in the client-side scan below \u2014 also stops it from consuming one\n // of the five returned slots and hiding a genuine duplicate.\n if (excludedId) params.push(`excludeIds=${encodeURIComponent(excludedId)}`)\n const call = await apiCall<{ items?: unknown[] }>(`/api/customers/people?${params.join('&')}`, {\n signal: controller.signal,\n })\n if (!call.ok) {\n if (!cancelled) setDuplicate(null)\n return\n }\n const payload = call.result ?? {}\n const items = Array.isArray(payload?.items) ? payload.items : []\n const match =\n items\n .map((raw) => {\n if (!raw || typeof raw !== 'object') return null\n const item = raw as Record<string, unknown>\n const id = typeof item.id === 'string' ? item.id : null\n const displayName = typeof item.display_name === 'string' ? item.display_name : null\n const emailValue = typeof item.primary_email === 'string' ? item.primary_email.toLowerCase() : null\n return id && displayName && emailValue\n ? { id, displayName, email: emailValue }\n : null\n })\n .filter((entry: EmailDuplicateMatch | null): entry is EmailDuplicateMatch => !!entry)\n .find((entry: EmailDuplicateMatch) => {\n if (excludedId && entry.id === excludedId) return false\n return matchMode === 'prefix'\n ? entry.email.startsWith(normalized)\n : entry.email === normalized\n }) ?? null\n if (!cancelled) {\n setDuplicate(match)\n }\n } catch (error) {\n if (cancelled) return\n if ((error as Error)?.name === 'AbortError') return\n setDuplicate(null)\n } finally {\n if (!cancelled) setChecking(false)\n }\n }, debounceMs)\n\n return () => {\n cancelled = true\n controller.abort()\n window.clearTimeout(timer)\n }\n }, [email, recordId, disabled, debounceMs, matchMode])\n\n return { duplicate, checking }\n}\n"],
|
|
5
|
+
"mappings": ";AAEA,YAAY,WAAW;AACvB,SAAS,eAAe;AAexB,MAAM,sBAAsB;AAErB,SAAS,uBACd,OACA,EAAE,UAAU,UAAU,aAAa,qBAAqB,YAAY,QAAQ,IAAmC,CAAC,GAChH;AACA,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAqC,IAAI;AACjF,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAAS,KAAK;AAEpD,QAAM,UAAU,MAAM;AACpB,QAAI,UAAU;AACZ,mBAAa,IAAI;AACjB,kBAAY,KAAK;AACjB;AAAA,IACF;AAEA,UAAM,UAAU,OAAO,UAAU,WAAW,MAAM,KAAK,IAAI;AAC3D,QAAI,CAAC,QAAQ,QAAQ;AACnB,mBAAa,IAAI;AACjB,kBAAY,KAAK;AACjB;AAAA,IACF;AAEA,UAAM,aAAa,QAAQ,YAAY;AACvC,UAAM,aAAa,OAAO,aAAa,YAAY,SAAS,KAAK,EAAE,SAAS,SAAS,KAAK,IAAI;AAC9F,QAAI,YAAY;AAChB,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,QAAQ,OAAO,WAAW,YAAY;AAC1C,kBAAY,IAAI;AAChB,UAAI;AACF,cAAM,SAAS;AAAA,UACb,cAAc,WACV,mBAAmB,mBAAmB,UAAU,CAAC,KACjD,SAAS,mBAAmB,UAAU,CAAC;AAAA,UAC3C;AAAA,UACA;AAAA,QACF;AAIA,YAAI,WAAY,QAAO,KAAK,cAAc,mBAAmB,UAAU,CAAC,EAAE;AAC1E,cAAM,OAAO,MAAM,QAA+B,yBAAyB,OAAO,KAAK,GAAG,CAAC,IAAI;AAAA,UAC7F,QAAQ,WAAW;AAAA,QACrB,CAAC;AACD,YAAI,CAAC,KAAK,IAAI;AACZ,cAAI,CAAC,UAAW,cAAa,IAAI;AACjC;AAAA,QACF;AACA,cAAM,UAAU,KAAK,UAAU,CAAC;AAChC,cAAM,QAAQ,MAAM,QAAQ,SAAS,KAAK,IAAI,QAAQ,QAAQ,CAAC;AAC/D,cAAM,QACJ,MACG,IAAI,CAAC,QAAQ;AACZ,cAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO;AAC5C,gBAAM,OAAO;AACb,gBAAM,KAAK,OAAO,KAAK,OAAO,WAAW,KAAK,KAAK;AACnD,gBAAM,cAAc,OAAO,KAAK,iBAAiB,WAAW,KAAK,eAAe;AAChF,gBAAM,aAAa,OAAO,KAAK,kBAAkB,WAAW,KAAK,cAAc,YAAY,IAAI;AAC/F,iBAAO,MAAM,eAAe,aACxB,EAAE,IAAI,aAAa,OAAO,WAAW,IACrC;AAAA,QACN,CAAC,EACA,OAAO,CAAC,UAAoE,CAAC,CAAC,KAAK,EACnF,KAAK,CAAC,UAA+B;AACpC,cAAI,cAAc,MAAM,OAAO,WAAY,QAAO;AAClD,iBAAO,cAAc,WACjB,MAAM,MAAM,WAAW,UAAU,IACjC,MAAM,UAAU;AAAA,QACtB,CAAC,KAAK;AACV,YAAI,CAAC,WAAW;AACd,uBAAa,KAAK;AAAA,QACpB;AAAA,MACF,SAAS,OAAO;AACd,YAAI,UAAW;AACf,YAAK,OAAiB,SAAS,aAAc;AAC7C,qBAAa,IAAI;AAAA,MACnB,UAAE;AACA,YAAI,CAAC,UAAW,aAAY,KAAK;AAAA,MACnC;AAAA,IACF,GAAG,UAAU;AAEb,WAAO,MAAM;AACX,kBAAY;AACZ,iBAAW,MAAM;AACjB,aAAO,aAAa,KAAK;AAAA,IAC3B;AAAA,EACF,GAAG,CAAC,OAAO,UAAU,UAAU,YAAY,SAAS,CAAC;AAErD,SAAO,EAAE,WAAW,SAAS;AAC/B;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2113,7 +2113,10 @@ __decorateClass([
|
|
|
2113
2113
|
], SalesNote.prototype, "deletedAt", 2);
|
|
2114
2114
|
SalesNote = __decorateClass([
|
|
2115
2115
|
Entity({ tableName: "sales_notes" }),
|
|
2116
|
-
Index({ name: "sales_notes_scope_idx", properties: ["organizationId", "tenantId"] })
|
|
2116
|
+
Index({ name: "sales_notes_scope_idx", properties: ["organizationId", "tenantId"] }),
|
|
2117
|
+
Index({ name: "sales_notes_context_idx", properties: ["contextId"] }),
|
|
2118
|
+
Index({ name: "sales_notes_order_idx", properties: ["order"] }),
|
|
2119
|
+
Index({ name: "sales_notes_quote_idx", properties: ["quote"] })
|
|
2117
2120
|
], SalesNote);
|
|
2118
2121
|
let SalesDocumentAddress = class {
|
|
2119
2122
|
constructor() {
|
|
@@ -2195,7 +2198,10 @@ __decorateClass([
|
|
|
2195
2198
|
], SalesDocumentAddress.prototype, "deletedAt", 2);
|
|
2196
2199
|
SalesDocumentAddress = __decorateClass([
|
|
2197
2200
|
Entity({ tableName: "sales_document_addresses" }),
|
|
2198
|
-
Index({ name: "sales_document_addresses_scope_idx", properties: ["organizationId", "tenantId"] })
|
|
2201
|
+
Index({ name: "sales_document_addresses_scope_idx", properties: ["organizationId", "tenantId"] }),
|
|
2202
|
+
Index({ name: "sales_document_addresses_document_idx", properties: ["documentId"] }),
|
|
2203
|
+
Index({ name: "sales_document_addresses_order_idx", properties: ["order"] }),
|
|
2204
|
+
Index({ name: "sales_document_addresses_quote_idx", properties: ["quote"] })
|
|
2199
2205
|
], SalesDocumentAddress);
|
|
2200
2206
|
let SalesDocumentTag = class {
|
|
2201
2207
|
constructor() {
|