@open-mercato/core 0.6.6-develop.6460.1.f94c74174c → 0.6.6-develop.6465.1.019f0fb26f
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/helpers/integration/auth.js +148 -82
- package/dist/helpers/integration/auth.js.map +2 -2
- package/dist/modules/sales/api/document-addresses/route.js +96 -6
- package/dist/modules/sales/api/document-addresses/route.js.map +2 -2
- package/dist/modules/sales/commands/documentAddresses.js +7 -0
- package/dist/modules/sales/commands/documentAddresses.js.map +2 -2
- package/dist/modules/staff/commands/activities.js +23 -12
- package/dist/modules/staff/commands/activities.js.map +2 -2
- package/dist/modules/staff/commands/addresses.js +20 -12
- package/dist/modules/staff/commands/addresses.js.map +2 -2
- package/dist/modules/staff/commands/comments.js +18 -11
- package/dist/modules/staff/commands/comments.js.map +2 -2
- package/dist/modules/staff/commands/job-histories.js +23 -12
- package/dist/modules/staff/commands/job-histories.js.map +2 -2
- package/dist/modules/staff/commands/leave-requests.js +37 -19
- package/dist/modules/staff/commands/leave-requests.js.map +2 -2
- package/dist/modules/staff/commands/shared.js +28 -1
- package/dist/modules/staff/commands/shared.js.map +2 -2
- package/dist/modules/staff/commands/tag-assignments.js +20 -3
- package/dist/modules/staff/commands/tag-assignments.js.map +2 -2
- package/dist/modules/staff/commands/team-members.js +21 -11
- package/dist/modules/staff/commands/team-members.js.map +2 -2
- package/dist/modules/staff/commands/team-roles.js +21 -11
- package/dist/modules/staff/commands/team-roles.js.map +2 -2
- package/dist/modules/staff/commands/teams.js +21 -11
- package/dist/modules/staff/commands/teams.js.map +2 -2
- package/dist/modules/staff/commands/timesheets-entries.js +24 -14
- package/dist/modules/staff/commands/timesheets-entries.js.map +2 -2
- package/dist/modules/staff/commands/timesheets-projects.js +34 -18
- package/dist/modules/staff/commands/timesheets-projects.js.map +2 -2
- package/dist/modules/workflows/api/instances/route.js +1 -1
- package/dist/modules/workflows/api/instances/route.js.map +2 -2
- package/package.json +7 -7
- package/src/helpers/integration/auth.ts +178 -86
- package/src/modules/sales/api/document-addresses/route.ts +140 -7
- package/src/modules/sales/commands/documentAddresses.ts +8 -0
- package/src/modules/staff/commands/activities.ts +23 -11
- package/src/modules/staff/commands/addresses.ts +20 -11
- package/src/modules/staff/commands/comments.ts +18 -10
- package/src/modules/staff/commands/job-histories.ts +27 -11
- package/src/modules/staff/commands/leave-requests.ts +38 -19
- package/src/modules/staff/commands/shared.ts +38 -0
- package/src/modules/staff/commands/tag-assignments.ts +19 -2
- package/src/modules/staff/commands/team-members.ts +21 -10
- package/src/modules/staff/commands/team-roles.ts +21 -10
- package/src/modules/staff/commands/teams.ts +21 -10
- package/src/modules/staff/commands/timesheets-entries.ts +24 -13
- package/src/modules/staff/commands/timesheets-projects.ts +34 -17
- package/src/modules/workflows/api/instances/route.ts +2 -2
package/.turbo/turbo-build.log
CHANGED
|
@@ -120,96 +120,162 @@ async function recoverGenericErrorPageIfPresent(page) {
|
|
|
120
120
|
}
|
|
121
121
|
await dismissGlobalNoticesIfPresent(page);
|
|
122
122
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
apiLoginForm.set("password", creds.password);
|
|
139
|
-
let apiLoginResponse = null;
|
|
140
|
-
for (let retry = 0; retry < 4; retry += 1) {
|
|
141
|
-
apiLoginResponse = await page.request.post("/api/auth/login", {
|
|
142
|
-
headers: {
|
|
143
|
-
"content-type": "application/x-www-form-urlencoded"
|
|
144
|
-
},
|
|
145
|
-
data: apiLoginForm.toString()
|
|
146
|
-
}).catch(() => null);
|
|
147
|
-
if (!apiLoginResponse || apiLoginResponse.status() !== 429) break;
|
|
148
|
-
await new Promise((resolve2) => setTimeout(resolve2, 1e3 * 2 ** retry));
|
|
123
|
+
function toPathOnly(rawUrl) {
|
|
124
|
+
try {
|
|
125
|
+
const url = new URL(rawUrl);
|
|
126
|
+
return `${url.pathname}${url.search}`;
|
|
127
|
+
} catch {
|
|
128
|
+
return "[unparseable-url]";
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function toRedactedLocation(rawLocation) {
|
|
132
|
+
if (!rawLocation) return null;
|
|
133
|
+
try {
|
|
134
|
+
const url = new URL(rawLocation, "http://integration.local");
|
|
135
|
+
return `${url.pathname}${url.search}`;
|
|
136
|
+
} catch {
|
|
137
|
+
return "[unparseable-location]";
|
|
149
138
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
sameSite: "Lax"
|
|
139
|
+
}
|
|
140
|
+
function createAuthNavigationTrace(page) {
|
|
141
|
+
const entries = [];
|
|
142
|
+
const onResponse = (response) => {
|
|
143
|
+
const path = toPathOnly(response.url());
|
|
144
|
+
if (path === "/backend" || path.startsWith("/api/auth/session/refresh")) {
|
|
145
|
+
entries.push({
|
|
146
|
+
path,
|
|
147
|
+
status: response.status(),
|
|
148
|
+
location: toRedactedLocation(response.headers().location)
|
|
161
149
|
});
|
|
162
150
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
151
|
+
};
|
|
152
|
+
page.on("response", onResponse);
|
|
153
|
+
return {
|
|
154
|
+
entries,
|
|
155
|
+
stop: () => page.off("response", onResponse)
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
async function formatLoginDiagnostics(page, role, entries) {
|
|
159
|
+
const cookieNames = Array.from(
|
|
160
|
+
new Set((await page.context().cookies()).map((cookie) => cookie.name))
|
|
161
|
+
).sort((left, right) => left.localeCompare(right));
|
|
162
|
+
const traceLines = entries.length ? entries.map((entry) => {
|
|
163
|
+
const location = entry.location ? ` -> ${entry.location}` : "";
|
|
164
|
+
return `${entry.status} ${entry.path}${location}`;
|
|
165
|
+
}) : ["none"];
|
|
166
|
+
return [
|
|
167
|
+
`Login diagnostics for role "${role}":`,
|
|
168
|
+
`current URL: ${toPathOnly(page.url())}`,
|
|
169
|
+
`browser cookie names: ${cookieNames.length ? cookieNames.join(", ") : "none"}`,
|
|
170
|
+
"backend/refresh trace:",
|
|
171
|
+
...traceLines
|
|
172
|
+
].join("\n");
|
|
173
|
+
}
|
|
174
|
+
async function throwLoginErrorWithDiagnostics(page, role, entries, error) {
|
|
175
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
176
|
+
throw new Error([
|
|
177
|
+
`Login did not reach backend for role: ${role}.`,
|
|
178
|
+
await formatLoginDiagnostics(page, role, entries),
|
|
179
|
+
`Original error: ${message}`
|
|
180
|
+
].join("\n"));
|
|
181
|
+
}
|
|
182
|
+
async function login(page, role = "admin") {
|
|
183
|
+
const authTrace = createAuthNavigationTrace(page);
|
|
184
|
+
try {
|
|
185
|
+
const creds = DEFAULT_CREDENTIALS[role];
|
|
186
|
+
const loginReadySelector = 'form[data-auth-ready="1"]';
|
|
187
|
+
const hasBackendUrl = () => /\/backend(?:\/.*)?$/.test(page.url());
|
|
188
|
+
const waitForBackend = async (timeout) => {
|
|
189
|
+
try {
|
|
190
|
+
await page.waitForURL(/\/backend(?:\/.*)?$/, { timeout });
|
|
191
|
+
return true;
|
|
192
|
+
} catch {
|
|
193
|
+
return hasBackendUrl();
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
await acknowledgeGlobalNotices(page);
|
|
197
|
+
const apiLoginForm = new URLSearchParams();
|
|
198
|
+
apiLoginForm.set("email", creds.email);
|
|
199
|
+
apiLoginForm.set("password", creds.password);
|
|
200
|
+
let apiLoginResponse = null;
|
|
201
|
+
for (let retry = 0; retry < 4; retry += 1) {
|
|
202
|
+
apiLoginResponse = await page.request.post("/api/auth/login", {
|
|
203
|
+
headers: {
|
|
204
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
205
|
+
},
|
|
206
|
+
data: apiLoginForm.toString()
|
|
207
|
+
}).catch(() => null);
|
|
208
|
+
if (!apiLoginResponse || apiLoginResponse.status() !== 429) break;
|
|
209
|
+
await new Promise((resolve2) => setTimeout(resolve2, 1e3 * 2 ** retry));
|
|
210
|
+
}
|
|
211
|
+
if (apiLoginResponse?.ok()) {
|
|
212
|
+
const apiLoginBody = await apiLoginResponse.json().catch(() => null);
|
|
213
|
+
const claims = typeof apiLoginBody?.token === "string" ? decodeJwtClaims(apiLoginBody.token) : null;
|
|
214
|
+
const baseUrl = process.env.BASE_URL || "http://localhost:3000";
|
|
215
|
+
const cookies = [];
|
|
216
|
+
if (claims?.tenantId) {
|
|
217
|
+
cookies.push({
|
|
218
|
+
name: "om_selected_tenant",
|
|
219
|
+
value: claims.tenantId,
|
|
220
|
+
url: baseUrl,
|
|
221
|
+
sameSite: "Lax"
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
if (claims?.orgId) {
|
|
225
|
+
cookies.push({
|
|
226
|
+
name: "om_selected_org",
|
|
227
|
+
value: claims.orgId,
|
|
228
|
+
url: baseUrl,
|
|
229
|
+
sameSite: "Lax"
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
if (cookies.length > 0) {
|
|
233
|
+
await page.context().addCookies(cookies);
|
|
234
|
+
}
|
|
235
|
+
await page.goto("/backend", { waitUntil: "domcontentloaded" });
|
|
236
|
+
if (await waitForBackend(8e3)) return;
|
|
237
|
+
}
|
|
238
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
239
|
+
await page.goto("/login", { waitUntil: "domcontentloaded" });
|
|
240
|
+
await dismissGlobalNoticesIfPresent(page);
|
|
241
|
+
await recoverClientSideErrorPageIfPresent(page);
|
|
242
|
+
await recoverGenericErrorPageIfPresent(page);
|
|
243
|
+
await page.waitForSelector(loginReadySelector, { state: "visible", timeout: 3e3 }).catch(() => null);
|
|
244
|
+
if (await page.getByLabel("Email").isVisible().catch(() => false)) break;
|
|
245
|
+
if (attempt === 3) {
|
|
246
|
+
throw new Error(`Login form is unavailable for role: ${role}; current URL: ${page.url()}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
await page.getByLabel("Email").fill(creds.email);
|
|
250
|
+
const passwordInput = page.getByLabel("Password", { exact: true }).first();
|
|
251
|
+
if (await passwordInput.isVisible().catch(() => false)) {
|
|
252
|
+
await passwordInput.fill(creds.password);
|
|
253
|
+
await passwordInput.press("Enter");
|
|
254
|
+
} else {
|
|
255
|
+
const submitButton = page.getByRole("button", { name: /login|sign in|continue with sso/i }).first();
|
|
256
|
+
await submitButton.click();
|
|
257
|
+
}
|
|
258
|
+
if (await waitForBackend(7e3)) return;
|
|
259
|
+
const loginForm = page.locator("form").first();
|
|
260
|
+
if (await loginForm.isVisible().catch(() => false)) {
|
|
261
|
+
await loginForm.evaluate((element) => {
|
|
262
|
+
const form = element;
|
|
263
|
+
form.requestSubmit();
|
|
264
|
+
}).catch(() => {
|
|
169
265
|
});
|
|
170
266
|
}
|
|
171
|
-
if (
|
|
172
|
-
|
|
267
|
+
if (await waitForBackend(5e3)) return;
|
|
268
|
+
const loginButton = page.getByRole("button", { name: /login|sign in|continue with sso/i }).first();
|
|
269
|
+
if (await loginButton.isVisible().catch(() => false)) {
|
|
270
|
+
await loginButton.click({ force: true });
|
|
173
271
|
}
|
|
174
|
-
await page.goto("/backend", { waitUntil: "domcontentloaded" });
|
|
175
272
|
if (await waitForBackend(8e3)) return;
|
|
273
|
+
throw new Error(`Login did not reach backend for role: ${role}; current URL: ${page.url()}`);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
await throwLoginErrorWithDiagnostics(page, role, authTrace.entries, error);
|
|
276
|
+
} finally {
|
|
277
|
+
authTrace.stop();
|
|
176
278
|
}
|
|
177
|
-
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
178
|
-
await page.goto("/login", { waitUntil: "domcontentloaded" });
|
|
179
|
-
await dismissGlobalNoticesIfPresent(page);
|
|
180
|
-
await recoverClientSideErrorPageIfPresent(page);
|
|
181
|
-
await recoverGenericErrorPageIfPresent(page);
|
|
182
|
-
await page.waitForSelector(loginReadySelector, { state: "visible", timeout: 3e3 }).catch(() => null);
|
|
183
|
-
if (await page.getByLabel("Email").isVisible().catch(() => false)) break;
|
|
184
|
-
if (attempt === 3) {
|
|
185
|
-
throw new Error(`Login form is unavailable for role: ${role}; current URL: ${page.url()}`);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
await page.getByLabel("Email").fill(creds.email);
|
|
189
|
-
const passwordInput = page.getByLabel("Password", { exact: true }).first();
|
|
190
|
-
if (await passwordInput.isVisible().catch(() => false)) {
|
|
191
|
-
await passwordInput.fill(creds.password);
|
|
192
|
-
await passwordInput.press("Enter");
|
|
193
|
-
} else {
|
|
194
|
-
const submitButton = page.getByRole("button", { name: /login|sign in|continue with sso/i }).first();
|
|
195
|
-
await submitButton.click();
|
|
196
|
-
}
|
|
197
|
-
if (await waitForBackend(7e3)) return;
|
|
198
|
-
const loginForm = page.locator("form").first();
|
|
199
|
-
if (await loginForm.isVisible().catch(() => false)) {
|
|
200
|
-
await loginForm.evaluate((element) => {
|
|
201
|
-
const form = element;
|
|
202
|
-
form.requestSubmit();
|
|
203
|
-
}).catch(() => {
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
if (await waitForBackend(5e3)) return;
|
|
207
|
-
const loginButton = page.getByRole("button", { name: /login|sign in|continue with sso/i }).first();
|
|
208
|
-
if (await loginButton.isVisible().catch(() => false)) {
|
|
209
|
-
await loginButton.click({ force: true });
|
|
210
|
-
}
|
|
211
|
-
if (await waitForBackend(8e3)) return;
|
|
212
|
-
throw new Error(`Login did not reach backend for role: ${role}; current URL: ${page.url()}`);
|
|
213
279
|
}
|
|
214
280
|
export {
|
|
215
281
|
DEFAULT_CREDENTIALS,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/helpers/integration/auth.ts"],
|
|
4
|
-
"sourcesContent": ["import { type Page } from '@playwright/test';\nimport { readFileSync } from 'fs';\nimport { resolve } from 'path';\n\nfunction loadEnvFileContent(): string | null {\n const candidatePaths = [\n resolve(process.cwd(), 'apps/mercato/.env'),\n resolve(process.cwd(), '.env'),\n ];\n\n for (const envPath of candidatePaths) {\n try {\n const content = readFileSync(envPath, 'utf-8');\n if (content.trim().length > 0) {\n return content;\n }\n } catch {\n continue;\n }\n }\n\n return null;\n}\n\nfunction loadEnvValue(key: string): string | undefined {\n if (process.env[key]) return process.env[key];\n const content = loadEnvFileContent();\n if (!content) return undefined;\n const match = content.match(new RegExp(`^${key}=(.+)$`, 'm'));\n return match?.[1]?.trim();\n}\n\nexport const DEFAULT_CREDENTIALS: Record<string, { email: string; password: string }> = {\n superadmin: {\n email: loadEnvValue('OM_INIT_SUPERADMIN_EMAIL') || 'superadmin@acme.com',\n password: loadEnvValue('OM_INIT_SUPERADMIN_PASSWORD') || 'secret',\n },\n admin: { email: 'admin@acme.com', password: 'secret' },\n employee: { email: 'employee@acme.com', password: 'secret' },\n};\n\nexport type Role = 'superadmin' | 'admin' | 'employee';\n\nfunction decodeJwtClaims(token: string): { tenantId?: string; orgId?: string | null } | null {\n const parts = token.split('.');\n if (parts.length < 2) return null;\n try {\n const normalized = parts[1].replace(/-/g, '+').replace(/_/g, '/');\n const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=');\n const payload = JSON.parse(Buffer.from(padded, 'base64').toString('utf8')) as {\n tenantId?: string;\n orgId?: string | null;\n };\n return payload;\n } catch {\n return null;\n }\n}\n\nasync function acknowledgeGlobalNotices(page: Page): Promise<void> {\n const baseUrl = process.env.BASE_URL || 'http://localhost:3000';\n await page.context().addCookies([\n {\n name: 'om_demo_notice_ack',\n value: 'ack',\n url: baseUrl,\n sameSite: 'Lax',\n },\n {\n name: 'om_cookie_notice_ack',\n value: 'ack',\n url: baseUrl,\n sameSite: 'Lax',\n },\n {\n name: 'om_feedback_suppress',\n value: '1',\n url: baseUrl,\n sameSite: 'Lax',\n },\n {\n name: 'om_feedback_shown',\n value: new Date().toISOString().slice(0, 10),\n url: baseUrl,\n sameSite: 'Lax',\n },\n ]);\n}\n\nasync function dismissGlobalNoticesIfPresent(page: Page): Promise<void> {\n const cookieAcceptButton = page.getByRole('button', { name: /accept cookies/i }).first();\n if (await cookieAcceptButton.isVisible().catch(() => false)) {\n await cookieAcceptButton.click();\n }\n\n const demoNotice = page.getByText(/this instance is provided for demo purposes only/i).first();\n if (await demoNotice.isVisible().catch(() => false)) {\n const noticeContainer = demoNotice.locator('xpath=ancestor::div[contains(@class,\"pointer-events-auto\")]').first();\n const dismissButton = noticeContainer.locator('button').first();\n if (await dismissButton.isVisible().catch(() => false)) {\n await dismissButton.click();\n }\n }\n\n const feedbackDialog = page.getByRole('dialog', { name: /Talk to Open Mercato team/i }).first();\n if (await feedbackDialog.isVisible().catch(() => false)) {\n const closeButton = feedbackDialog.getByRole('button', { name: /close/i }).first();\n if (await closeButton.isVisible().catch(() => false)) {\n await closeButton.click().catch(() => {});\n } else {\n await page.keyboard.press('Escape').catch(() => {});\n }\n }\n}\n\nasync function recoverClientSideErrorPageIfPresent(page: Page): Promise<void> {\n const clientErrorHeading = page\n .getByRole('heading', { name: /Application error: a client-side exception has occurred/i })\n .first();\n if (!(await clientErrorHeading.isVisible().catch(() => false))) return;\n await page.reload({ waitUntil: 'domcontentloaded' });\n await dismissGlobalNoticesIfPresent(page);\n}\n\nasync function recoverGenericErrorPageIfPresent(page: Page): Promise<void> {\n const errorHeading = page.getByRole('heading', { name: /^Something went wrong$/i }).first();\n if (!(await errorHeading.isVisible().catch(() => false))) return;\n const retryButton = page.getByRole('button', { name: /Try again/i }).first();\n if (await retryButton.isVisible().catch(() => false)) {\n await retryButton.click().catch(() => {});\n await page.waitForLoadState('domcontentloaded').catch(() => {});\n await page.waitForTimeout(500).catch(() => {});\n } else {\n await page.reload({ waitUntil: 'domcontentloaded' });\n }\n await dismissGlobalNoticesIfPresent(page);\n}\n\nexport async function login(page: Page, role: Role = 'admin'): Promise<void> {\n const creds = DEFAULT_CREDENTIALS[role];\n const loginReadySelector = 'form[data-auth-ready=\"1\"]';\n const hasBackendUrl = (): boolean => /\\/backend(?:\\/.*)?$/.test(page.url());\n const waitForBackend = async (timeout: number): Promise<boolean> => {\n try {\n await page.waitForURL(/\\/backend(?:\\/.*)?$/, { timeout });\n return true;\n } catch {\n return hasBackendUrl();\n }\n };\n\n await acknowledgeGlobalNotices(page);\n const apiLoginForm = new URLSearchParams();\n apiLoginForm.set('email', creds.email);\n apiLoginForm.set('password', creds.password);\n // Retry-on-429 against the auth rate limit (5/60s per email). Capped\n // exponential backoff: 1s, 2s, 4s \u2014 worst-case ~7s.\n let apiLoginResponse: Awaited<ReturnType<typeof page.request.post>> | null = null;\n for (let retry = 0; retry < 4; retry += 1) {\n apiLoginResponse = await page.request.post('/api/auth/login', {\n headers: {\n 'content-type': 'application/x-www-form-urlencoded',\n },\n data: apiLoginForm.toString(),\n }).catch(() => null);\n if (!apiLoginResponse || apiLoginResponse.status() !== 429) break;\n await new Promise((resolve) => setTimeout(resolve, 1000 * 2 ** retry));\n }\n if (apiLoginResponse?.ok()) {\n const apiLoginBody = (await apiLoginResponse.json().catch(() => null)) as { token?: string } | null;\n const claims = typeof apiLoginBody?.token === 'string' ? decodeJwtClaims(apiLoginBody.token) : null;\n const baseUrl = process.env.BASE_URL || 'http://localhost:3000';\n const cookies = [];\n if (claims?.tenantId) {\n cookies.push({\n name: 'om_selected_tenant',\n value: claims.tenantId,\n url: baseUrl,\n sameSite: 'Lax' as const,\n });\n }\n if (claims?.orgId) {\n cookies.push({\n name: 'om_selected_org',\n value: claims.orgId,\n url: baseUrl,\n sameSite: 'Lax' as const,\n });\n }\n if (cookies.length > 0) {\n await page.context().addCookies(cookies);\n }\n await page.goto('/backend', { waitUntil: 'domcontentloaded' });\n if (await waitForBackend(8_000)) return;\n }\n\n for (let attempt = 0; attempt < 4; attempt += 1) {\n await page.goto('/login', { waitUntil: 'domcontentloaded' });\n await dismissGlobalNoticesIfPresent(page);\n await recoverClientSideErrorPageIfPresent(page);\n await recoverGenericErrorPageIfPresent(page);\n await page.waitForSelector(loginReadySelector, { state: 'visible', timeout: 3_000 }).catch(() => null);\n if (await page.getByLabel('Email').isVisible().catch(() => false)) break;\n if (attempt === 3) {\n throw new Error(`Login form is unavailable for role: ${role}; current URL: ${page.url()}`);\n }\n }\n await page.getByLabel('Email').fill(creds.email);\n\n const passwordInput = page.getByLabel('Password', { exact: true }).first();\n if (await passwordInput.isVisible().catch(() => false)) {\n await passwordInput.fill(creds.password);\n await passwordInput.press('Enter');\n } else {\n const submitButton = page.getByRole('button', { name: /login|sign in|continue with sso/i }).first();\n await submitButton.click();\n }\n\n if (await waitForBackend(7_000)) return;\n\n const loginForm = page.locator('form').first();\n if (await loginForm.isVisible().catch(() => false)) {\n await loginForm.evaluate((element) => {\n const form = element as HTMLFormElement\n form.requestSubmit()\n }).catch(() => {})\n }\n if (await waitForBackend(5_000)) return;\n\n const loginButton = page.getByRole('button', { name: /login|sign in|continue with sso/i }).first();\n if (await loginButton.isVisible().catch(() => false)) {\n await loginButton.click({ force: true });\n }\n if (await waitForBackend(8_000)) return;\n\n throw new Error(`Login did not reach backend for role: ${role}; current URL: ${page.url()}`);\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { type Page, type Response } from '@playwright/test';\nimport { readFileSync } from 'fs';\nimport { resolve } from 'path';\n\nfunction loadEnvFileContent(): string | null {\n const candidatePaths = [\n resolve(process.cwd(), 'apps/mercato/.env'),\n resolve(process.cwd(), '.env'),\n ];\n\n for (const envPath of candidatePaths) {\n try {\n const content = readFileSync(envPath, 'utf-8');\n if (content.trim().length > 0) {\n return content;\n }\n } catch {\n continue;\n }\n }\n\n return null;\n}\n\nfunction loadEnvValue(key: string): string | undefined {\n if (process.env[key]) return process.env[key];\n const content = loadEnvFileContent();\n if (!content) return undefined;\n const match = content.match(new RegExp(`^${key}=(.+)$`, 'm'));\n return match?.[1]?.trim();\n}\n\nexport const DEFAULT_CREDENTIALS: Record<string, { email: string; password: string }> = {\n superadmin: {\n email: loadEnvValue('OM_INIT_SUPERADMIN_EMAIL') || 'superadmin@acme.com',\n password: loadEnvValue('OM_INIT_SUPERADMIN_PASSWORD') || 'secret',\n },\n admin: { email: 'admin@acme.com', password: 'secret' },\n employee: { email: 'employee@acme.com', password: 'secret' },\n};\n\nexport type Role = 'superadmin' | 'admin' | 'employee';\n\nfunction decodeJwtClaims(token: string): { tenantId?: string; orgId?: string | null } | null {\n const parts = token.split('.');\n if (parts.length < 2) return null;\n try {\n const normalized = parts[1].replace(/-/g, '+').replace(/_/g, '/');\n const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=');\n const payload = JSON.parse(Buffer.from(padded, 'base64').toString('utf8')) as {\n tenantId?: string;\n orgId?: string | null;\n };\n return payload;\n } catch {\n return null;\n }\n}\n\nasync function acknowledgeGlobalNotices(page: Page): Promise<void> {\n const baseUrl = process.env.BASE_URL || 'http://localhost:3000';\n await page.context().addCookies([\n {\n name: 'om_demo_notice_ack',\n value: 'ack',\n url: baseUrl,\n sameSite: 'Lax',\n },\n {\n name: 'om_cookie_notice_ack',\n value: 'ack',\n url: baseUrl,\n sameSite: 'Lax',\n },\n {\n name: 'om_feedback_suppress',\n value: '1',\n url: baseUrl,\n sameSite: 'Lax',\n },\n {\n name: 'om_feedback_shown',\n value: new Date().toISOString().slice(0, 10),\n url: baseUrl,\n sameSite: 'Lax',\n },\n ]);\n}\n\nasync function dismissGlobalNoticesIfPresent(page: Page): Promise<void> {\n const cookieAcceptButton = page.getByRole('button', { name: /accept cookies/i }).first();\n if (await cookieAcceptButton.isVisible().catch(() => false)) {\n await cookieAcceptButton.click();\n }\n\n const demoNotice = page.getByText(/this instance is provided for demo purposes only/i).first();\n if (await demoNotice.isVisible().catch(() => false)) {\n const noticeContainer = demoNotice.locator('xpath=ancestor::div[contains(@class,\"pointer-events-auto\")]').first();\n const dismissButton = noticeContainer.locator('button').first();\n if (await dismissButton.isVisible().catch(() => false)) {\n await dismissButton.click();\n }\n }\n\n const feedbackDialog = page.getByRole('dialog', { name: /Talk to Open Mercato team/i }).first();\n if (await feedbackDialog.isVisible().catch(() => false)) {\n const closeButton = feedbackDialog.getByRole('button', { name: /close/i }).first();\n if (await closeButton.isVisible().catch(() => false)) {\n await closeButton.click().catch(() => {});\n } else {\n await page.keyboard.press('Escape').catch(() => {});\n }\n }\n}\n\nasync function recoverClientSideErrorPageIfPresent(page: Page): Promise<void> {\n const clientErrorHeading = page\n .getByRole('heading', { name: /Application error: a client-side exception has occurred/i })\n .first();\n if (!(await clientErrorHeading.isVisible().catch(() => false))) return;\n await page.reload({ waitUntil: 'domcontentloaded' });\n await dismissGlobalNoticesIfPresent(page);\n}\n\nasync function recoverGenericErrorPageIfPresent(page: Page): Promise<void> {\n const errorHeading = page.getByRole('heading', { name: /^Something went wrong$/i }).first();\n if (!(await errorHeading.isVisible().catch(() => false))) return;\n const retryButton = page.getByRole('button', { name: /Try again/i }).first();\n if (await retryButton.isVisible().catch(() => false)) {\n await retryButton.click().catch(() => {});\n await page.waitForLoadState('domcontentloaded').catch(() => {});\n await page.waitForTimeout(500).catch(() => {});\n } else {\n await page.reload({ waitUntil: 'domcontentloaded' });\n }\n await dismissGlobalNoticesIfPresent(page);\n}\n\ntype AuthNavigationTraceEntry = {\n path: string;\n status: number;\n location: string | null;\n};\n\nfunction toPathOnly(rawUrl: string): string {\n try {\n const url = new URL(rawUrl);\n return `${url.pathname}${url.search}`;\n } catch {\n return '[unparseable-url]';\n }\n}\n\nfunction toRedactedLocation(rawLocation: string | undefined): string | null {\n if (!rawLocation) return null;\n try {\n const url = new URL(rawLocation, 'http://integration.local');\n return `${url.pathname}${url.search}`;\n } catch {\n return '[unparseable-location]';\n }\n}\n\nfunction createAuthNavigationTrace(page: Page): {\n entries: AuthNavigationTraceEntry[];\n stop: () => void;\n} {\n const entries: AuthNavigationTraceEntry[] = [];\n const onResponse = (response: Response) => {\n const path = toPathOnly(response.url());\n if (path === '/backend' || path.startsWith('/api/auth/session/refresh')) {\n entries.push({\n path,\n status: response.status(),\n location: toRedactedLocation(response.headers().location),\n });\n }\n };\n page.on('response', onResponse);\n return {\n entries,\n stop: () => page.off('response', onResponse),\n };\n}\n\nasync function formatLoginDiagnostics(\n page: Page,\n role: Role,\n entries: AuthNavigationTraceEntry[],\n): Promise<string> {\n const cookieNames = Array.from(\n new Set((await page.context().cookies()).map((cookie) => cookie.name)),\n ).sort((left, right) => left.localeCompare(right));\n const traceLines = entries.length\n ? entries.map((entry) => {\n const location = entry.location ? ` -> ${entry.location}` : '';\n return `${entry.status} ${entry.path}${location}`;\n })\n : ['none'];\n\n return [\n `Login diagnostics for role \"${role}\":`,\n `current URL: ${toPathOnly(page.url())}`,\n `browser cookie names: ${cookieNames.length ? cookieNames.join(', ') : 'none'}`,\n 'backend/refresh trace:',\n ...traceLines,\n ].join('\\n');\n}\n\nasync function throwLoginErrorWithDiagnostics(\n page: Page,\n role: Role,\n entries: AuthNavigationTraceEntry[],\n error: unknown,\n): Promise<never> {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error([\n `Login did not reach backend for role: ${role}.`,\n await formatLoginDiagnostics(page, role, entries),\n `Original error: ${message}`,\n ].join('\\n'));\n}\n\nexport async function login(page: Page, role: Role = 'admin'): Promise<void> {\n const authTrace = createAuthNavigationTrace(page);\n try {\n const creds = DEFAULT_CREDENTIALS[role];\n const loginReadySelector = 'form[data-auth-ready=\"1\"]';\n const hasBackendUrl = (): boolean => /\\/backend(?:\\/.*)?$/.test(page.url());\n const waitForBackend = async (timeout: number): Promise<boolean> => {\n try {\n await page.waitForURL(/\\/backend(?:\\/.*)?$/, { timeout });\n return true;\n } catch {\n return hasBackendUrl();\n }\n };\n\n await acknowledgeGlobalNotices(page);\n const apiLoginForm = new URLSearchParams();\n apiLoginForm.set('email', creds.email);\n apiLoginForm.set('password', creds.password);\n // Retry-on-429 against the auth rate limit (5/60s per email). Capped\n // exponential backoff: 1s, 2s, 4s \u2014 worst-case ~7s.\n let apiLoginResponse: Awaited<ReturnType<typeof page.request.post>> | null = null;\n for (let retry = 0; retry < 4; retry += 1) {\n apiLoginResponse = await page.request.post('/api/auth/login', {\n headers: {\n 'content-type': 'application/x-www-form-urlencoded',\n },\n data: apiLoginForm.toString(),\n }).catch(() => null);\n if (!apiLoginResponse || apiLoginResponse.status() !== 429) break;\n await new Promise((resolve) => setTimeout(resolve, 1000 * 2 ** retry));\n }\n if (apiLoginResponse?.ok()) {\n const apiLoginBody = (await apiLoginResponse.json().catch(() => null)) as { token?: string } | null;\n const claims = typeof apiLoginBody?.token === 'string' ? decodeJwtClaims(apiLoginBody.token) : null;\n const baseUrl = process.env.BASE_URL || 'http://localhost:3000';\n const cookies = [];\n if (claims?.tenantId) {\n cookies.push({\n name: 'om_selected_tenant',\n value: claims.tenantId,\n url: baseUrl,\n sameSite: 'Lax' as const,\n });\n }\n if (claims?.orgId) {\n cookies.push({\n name: 'om_selected_org',\n value: claims.orgId,\n url: baseUrl,\n sameSite: 'Lax' as const,\n });\n }\n if (cookies.length > 0) {\n await page.context().addCookies(cookies);\n }\n await page.goto('/backend', { waitUntil: 'domcontentloaded' });\n if (await waitForBackend(8_000)) return;\n }\n\n for (let attempt = 0; attempt < 4; attempt += 1) {\n await page.goto('/login', { waitUntil: 'domcontentloaded' });\n await dismissGlobalNoticesIfPresent(page);\n await recoverClientSideErrorPageIfPresent(page);\n await recoverGenericErrorPageIfPresent(page);\n await page.waitForSelector(loginReadySelector, { state: 'visible', timeout: 3_000 }).catch(() => null);\n if (await page.getByLabel('Email').isVisible().catch(() => false)) break;\n if (attempt === 3) {\n throw new Error(`Login form is unavailable for role: ${role}; current URL: ${page.url()}`);\n }\n }\n await page.getByLabel('Email').fill(creds.email);\n\n const passwordInput = page.getByLabel('Password', { exact: true }).first();\n if (await passwordInput.isVisible().catch(() => false)) {\n await passwordInput.fill(creds.password);\n await passwordInput.press('Enter');\n } else {\n const submitButton = page.getByRole('button', { name: /login|sign in|continue with sso/i }).first();\n await submitButton.click();\n }\n\n if (await waitForBackend(7_000)) return;\n\n const loginForm = page.locator('form').first();\n if (await loginForm.isVisible().catch(() => false)) {\n await loginForm.evaluate((element) => {\n const form = element as HTMLFormElement\n form.requestSubmit()\n }).catch(() => {})\n }\n if (await waitForBackend(5_000)) return;\n\n const loginButton = page.getByRole('button', { name: /login|sign in|continue with sso/i }).first();\n if (await loginButton.isVisible().catch(() => false)) {\n await loginButton.click({ force: true });\n }\n if (await waitForBackend(8_000)) return;\n\n throw new Error(`Login did not reach backend for role: ${role}; current URL: ${page.url()}`);\n } catch (error) {\n await throwLoginErrorWithDiagnostics(page, role, authTrace.entries, error);\n } finally {\n authTrace.stop();\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["resolve"]
|
|
7
7
|
}
|
|
@@ -2,9 +2,10 @@ import { z } from "zod";
|
|
|
2
2
|
import { makeCrudRoute } from "@open-mercato/shared/lib/crud/factory";
|
|
3
3
|
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
4
4
|
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
5
|
+
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
5
6
|
import { createSalesCrudOpenApi, createPagedListResponseSchema, defaultOkResponseSchema } from "../openapi.js";
|
|
6
7
|
import { withScopedPayload } from "../utils.js";
|
|
7
|
-
import { SalesDocumentAddress } from "../../data/entities.js";
|
|
8
|
+
import { SalesDocumentAddress, SalesOrder, SalesQuote } from "../../data/entities.js";
|
|
8
9
|
import {
|
|
9
10
|
documentAddressCreateSchema,
|
|
10
11
|
documentAddressDeleteSchema,
|
|
@@ -27,8 +28,90 @@ const routeMetadata = {
|
|
|
27
28
|
DELETE: { requireAuth: true }
|
|
28
29
|
};
|
|
29
30
|
const metadata = routeMetadata;
|
|
31
|
+
const DOCUMENT_ADDRESS_FEATURES = {
|
|
32
|
+
order: { view: "sales.orders.view", manage: "sales.orders.manage" },
|
|
33
|
+
quote: { view: "sales.quotes.view", manage: "sales.quotes.manage" }
|
|
34
|
+
};
|
|
35
|
+
function toDocumentKind(value) {
|
|
36
|
+
return value === "order" || value === "quote" ? value : null;
|
|
37
|
+
}
|
|
38
|
+
async function resolveDocumentKindForRead(query, ctx) {
|
|
39
|
+
if (query.documentKind) return query.documentKind;
|
|
40
|
+
const tenantId = ctx.auth?.tenantId ?? null;
|
|
41
|
+
const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? (Array.isArray(ctx.organizationIds) && ctx.organizationIds.length === 1 ? ctx.organizationIds[0] : null);
|
|
42
|
+
if (!query.documentId || !tenantId || !organizationId) return null;
|
|
43
|
+
const em = ctx.container.resolve("em");
|
|
44
|
+
const scope = { tenantId, organizationId };
|
|
45
|
+
const order = await findOneWithDecryption(em, SalesOrder, { id: query.documentId, tenantId, organizationId }, {}, scope);
|
|
46
|
+
if (order) return "order";
|
|
47
|
+
const quote = await findOneWithDecryption(em, SalesQuote, { id: query.documentId, tenantId, organizationId }, {}, scope);
|
|
48
|
+
if (quote) return "quote";
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
async function ensureDocumentAddressReadAccess(query, ctx) {
|
|
52
|
+
const documentKind = await resolveDocumentKindForRead(query, ctx);
|
|
53
|
+
const requiredFeatures = documentKind ? [DOCUMENT_ADDRESS_FEATURES[documentKind].view] : [DOCUMENT_ADDRESS_FEATURES.order.view, DOCUMENT_ADDRESS_FEATURES.quote.view];
|
|
54
|
+
await ensureDocumentAddressAccess(ctx, requiredFeatures);
|
|
55
|
+
return documentKind;
|
|
56
|
+
}
|
|
57
|
+
async function resolveExistingAddressKind(ctx, id, scope) {
|
|
58
|
+
const em = ctx.container.resolve("em");
|
|
59
|
+
const address = await findOneWithDecryption(
|
|
60
|
+
em,
|
|
61
|
+
SalesDocumentAddress,
|
|
62
|
+
{ id, tenantId: scope.tenantId, organizationId: scope.organizationId },
|
|
63
|
+
{},
|
|
64
|
+
scope
|
|
65
|
+
);
|
|
66
|
+
return toDocumentKind(address?.documentKind);
|
|
67
|
+
}
|
|
68
|
+
async function ensureDocumentAddressAccess(ctx, requiredFeatures, translate) {
|
|
69
|
+
const resolvedTranslate = translate ?? (await resolveTranslations()).translate;
|
|
70
|
+
const auth = ctx.auth;
|
|
71
|
+
if (!auth?.sub) {
|
|
72
|
+
throw new CrudHttpError(401, { error: resolvedTranslate("api.errors.unauthorized", "Unauthorized") });
|
|
73
|
+
}
|
|
74
|
+
let rbac = null;
|
|
75
|
+
try {
|
|
76
|
+
rbac = ctx.container.resolve("rbacService");
|
|
77
|
+
} catch {
|
|
78
|
+
rbac = null;
|
|
79
|
+
}
|
|
80
|
+
const ok = rbac ? await rbac.userHasAllFeatures(auth.sub, requiredFeatures, {
|
|
81
|
+
tenantId: auth.tenantId ?? null,
|
|
82
|
+
organizationId: ctx.selectedOrganizationId ?? auth.orgId ?? null
|
|
83
|
+
}) : false;
|
|
84
|
+
if (!ok) {
|
|
85
|
+
throw new CrudHttpError(403, {
|
|
86
|
+
error: resolvedTranslate("api.errors.forbidden", "Forbidden"),
|
|
87
|
+
requiredFeatures
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function ensureDocumentAddressKindAccess(ctx, documentKind, access, translate) {
|
|
92
|
+
await ensureDocumentAddressAccess(ctx, [DOCUMENT_ADDRESS_FEATURES[documentKind][access]], translate);
|
|
93
|
+
}
|
|
94
|
+
async function ensureDocumentAddressMutationAccess(ctx, input, options) {
|
|
95
|
+
const existingKind = await resolveExistingAddressKind(ctx, input.id, {
|
|
96
|
+
tenantId: input.tenantId,
|
|
97
|
+
organizationId: input.organizationId
|
|
98
|
+
});
|
|
99
|
+
const featureKinds = /* @__PURE__ */ new Set();
|
|
100
|
+
if (existingKind) featureKinds.add(existingKind);
|
|
101
|
+
if (!existingKind || options.includeTargetKind) featureKinds.add(input.documentKind);
|
|
102
|
+
await ensureDocumentAddressAccess(
|
|
103
|
+
ctx,
|
|
104
|
+
[...featureKinds].map((kind) => DOCUMENT_ADDRESS_FEATURES[kind].manage),
|
|
105
|
+
options.translate
|
|
106
|
+
);
|
|
107
|
+
}
|
|
30
108
|
const crud = makeCrudRoute({
|
|
31
109
|
metadata: routeMetadata,
|
|
110
|
+
hooks: {
|
|
111
|
+
beforeList: async (query, ctx) => {
|
|
112
|
+
await ensureDocumentAddressReadAccess(query, ctx);
|
|
113
|
+
}
|
|
114
|
+
},
|
|
32
115
|
orm: {
|
|
33
116
|
entity: SalesDocumentAddress,
|
|
34
117
|
idField: "id",
|
|
@@ -69,11 +152,12 @@ const crud = makeCrudRoute({
|
|
|
69
152
|
createdAt: "created_at",
|
|
70
153
|
updatedAt: "updated_at"
|
|
71
154
|
},
|
|
72
|
-
buildFilters: async (query) => {
|
|
155
|
+
buildFilters: async (query, ctx) => {
|
|
156
|
+
const documentKind = await resolveDocumentKindForRead(query, ctx);
|
|
73
157
|
const filters = {
|
|
74
158
|
document_id: { $eq: query.documentId }
|
|
75
159
|
};
|
|
76
|
-
if (query.documentKind) filters.document_kind = { $eq: query.documentKind };
|
|
160
|
+
if (query.documentKind ?? documentKind) filters.document_kind = { $eq: query.documentKind ?? documentKind };
|
|
77
161
|
return filters;
|
|
78
162
|
}
|
|
79
163
|
},
|
|
@@ -83,7 +167,9 @@ const crud = makeCrudRoute({
|
|
|
83
167
|
schema: rawBodySchema,
|
|
84
168
|
mapInput: async ({ raw, ctx }) => {
|
|
85
169
|
const { translate } = await resolveTranslations();
|
|
86
|
-
|
|
170
|
+
const input = documentAddressCreateSchema.parse(withScopedPayload(raw ?? {}, ctx, translate));
|
|
171
|
+
await ensureDocumentAddressKindAccess(ctx, input.documentKind, "manage", translate);
|
|
172
|
+
return input;
|
|
87
173
|
},
|
|
88
174
|
response: ({ result }) => ({ id: result?.id ?? null }),
|
|
89
175
|
status: 201
|
|
@@ -93,7 +179,9 @@ const crud = makeCrudRoute({
|
|
|
93
179
|
schema: rawBodySchema,
|
|
94
180
|
mapInput: async ({ raw, ctx }) => {
|
|
95
181
|
const { translate } = await resolveTranslations();
|
|
96
|
-
|
|
182
|
+
const input = documentAddressUpdateSchema.parse(withScopedPayload(raw ?? {}, ctx, translate));
|
|
183
|
+
await ensureDocumentAddressMutationAccess(ctx, input, { includeTargetKind: true, translate });
|
|
184
|
+
return input;
|
|
97
185
|
},
|
|
98
186
|
response: () => ({ ok: true })
|
|
99
187
|
},
|
|
@@ -110,7 +198,9 @@ const crud = makeCrudRoute({
|
|
|
110
198
|
error: translate("sales.documents.detail.error", "Document not found or inaccessible.")
|
|
111
199
|
});
|
|
112
200
|
}
|
|
113
|
-
|
|
201
|
+
const input = documentAddressDeleteSchema.parse(withScopedPayload({ id, documentId, documentKind }, ctx, translate));
|
|
202
|
+
await ensureDocumentAddressMutationAccess(ctx, input, { includeTargetKind: false, translate });
|
|
203
|
+
return input;
|
|
114
204
|
},
|
|
115
205
|
response: () => ({ ok: true })
|
|
116
206
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/sales/api/document-addresses/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { z } from 'zod'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { createSalesCrudOpenApi, createPagedListResponseSchema, defaultOkResponseSchema } from '../openapi'\nimport { withScopedPayload } from '../utils'\nimport { SalesDocumentAddress } from '../../data/entities'\nimport {\n documentAddressCreateSchema,\n documentAddressDeleteSchema,\n documentAddressUpdateSchema,\n} from '../../data/validators'\nimport { E } from '#generated/entities.ids.generated'\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst listSchema = z\n .object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n documentId: z.string().uuid(),\n documentKind: z.enum(['order', 'quote']).optional(),\n sortField: z.string().optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n })\n .passthrough()\n\nconst routeMetadata = {\n GET: { requireAuth: true },\n POST: { requireAuth: true },\n PUT: { requireAuth: true },\n DELETE: { requireAuth: true },\n}\n\nexport const metadata = routeMetadata\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: SalesDocumentAddress,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n indexer: { entityType: E.sales.sales_document_address },\n list: {\n schema: listSchema,\n entityId: E.sales.sales_document_address,\n fields: [\n 'id',\n 'organization_id',\n 'tenant_id',\n 'document_id',\n 'document_kind',\n 'customer_address_id',\n 'name',\n 'purpose',\n 'company_name',\n 'address_line1',\n 'address_line2',\n 'building_number',\n 'flat_number',\n 'city',\n 'region',\n 'postal_code',\n 'country',\n 'latitude',\n 'longitude',\n 'order_id',\n 'quote_id',\n 'created_at',\n 'updated_at',\n ],\n sortFieldMap: {\n createdAt: 'created_at',\n updatedAt: 'updated_at',\n },\n buildFilters: async (query: any) => {\n const filters: Record<string, any> = {\n document_id: { $eq: query.documentId },\n }\n\n if (query.documentKind) filters.document_kind = { $eq: query.documentKind }\n return filters\n },\n },\n actions: {\n create: {\n commandId: 'sales.document-addresses.create',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n return documentAddressCreateSchema.parse(withScopedPayload(raw ?? {}, ctx, translate))\n },\n response: ({ result }) => ({ id: result?.id ?? null }),\n status: 201,\n },\n update: {\n commandId: 'sales.document-addresses.update',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n return documentAddressUpdateSchema.parse(withScopedPayload(raw ?? {}, ctx, translate))\n },\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'sales.document-addresses.delete',\n schema: rawBodySchema,\n mapInput: async ({ parsed, ctx }) => {\n const { translate } = await resolveTranslations()\n const id =\n parsed?.body?.id ??\n parsed?.id ??\n parsed?.query?.id ??\n (ctx.request ? new URL(ctx.request.url).searchParams.get('id') : null)\n const documentId =\n parsed?.body?.documentId ??\n parsed?.query?.documentId ??\n (ctx.request ? new URL(ctx.request.url).searchParams.get('documentId') : null)\n const documentKind =\n parsed?.body?.documentKind ??\n parsed?.query?.documentKind ??\n (ctx.request ? new URL(ctx.request.url).searchParams.get('documentKind') : null)\n if (!id || !documentId || !documentKind) {\n throw new CrudHttpError(400, {\n error: translate('sales.documents.detail.error', 'Document not found or inaccessible.'),\n })\n }\n return documentAddressDeleteSchema.parse(withScopedPayload({ id, documentId, documentKind }, ctx, translate))\n },\n response: () => ({ ok: true }),\n },\n },\n})\n\nconst { GET, POST, PUT, DELETE } = crud\n\nexport { GET, POST, PUT, DELETE }\n\nconst documentAddressSchema = z.object({\n id: z.string().uuid(),\n document_id: z.string().uuid(),\n document_kind: z.enum(['order', 'quote']),\n customer_address_id: z.string().uuid().nullable().optional(),\n name: z.string().nullable().optional(),\n purpose: z.string().nullable().optional(),\n company_name: z.string().nullable().optional(),\n address_line1: z.string(),\n address_line2: z.string().nullable().optional(),\n building_number: z.string().nullable().optional(),\n flat_number: z.string().nullable().optional(),\n city: z.string().nullable().optional(),\n region: z.string().nullable().optional(),\n postal_code: z.string().nullable().optional(),\n country: z.string().nullable().optional(),\n latitude: z.number().nullable().optional(),\n longitude: z.number().nullable().optional(),\n created_at: z.string(),\n updated_at: z.string(),\n})\n\nexport const openApi = createSalesCrudOpenApi({\n resourceName: 'Document address',\n querySchema: listSchema,\n listResponseSchema: createPagedListResponseSchema(documentAddressSchema),\n create: {\n schema: documentAddressCreateSchema,\n responseSchema: z.object({ id: z.string().uuid().nullable() }),\n description: 'Creates a sales document address linked to an order or quote.',\n },\n update: {\n schema: documentAddressUpdateSchema,\n responseSchema: defaultOkResponseSchema,\n description: 'Updates a sales document address.',\n },\n del: {\n schema: documentAddressDeleteSchema.pick({ id: true, documentId: true, documentKind: true }),\n responseSchema: defaultOkResponseSchema,\n description: 'Deletes a sales document address.',\n },\n})\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod'\nimport { makeCrudRoute, type CrudCtx } from '@open-mercato/shared/lib/crud/factory'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { createSalesCrudOpenApi, createPagedListResponseSchema, defaultOkResponseSchema } from '../openapi'\nimport { withScopedPayload } from '../utils'\nimport { SalesDocumentAddress, SalesOrder, SalesQuote } from '../../data/entities'\nimport {\n documentAddressCreateSchema,\n documentAddressDeleteSchema,\n documentAddressUpdateSchema,\n} from '../../data/validators'\nimport { E } from '#generated/entities.ids.generated'\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst listSchema = z\n .object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n documentId: z.string().uuid(),\n documentKind: z.enum(['order', 'quote']).optional(),\n sortField: z.string().optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n })\n .passthrough()\n\nconst routeMetadata = {\n GET: { requireAuth: true },\n POST: { requireAuth: true },\n PUT: { requireAuth: true },\n DELETE: { requireAuth: true },\n}\n\nexport const metadata = routeMetadata\n\ntype DocumentKind = 'order' | 'quote'\ntype DocumentAddressAccess = 'view' | 'manage'\ntype Translate = (key: string, fallback?: string) => string\n\nconst DOCUMENT_ADDRESS_FEATURES: Record<DocumentKind, Record<DocumentAddressAccess, string>> = {\n order: { view: 'sales.orders.view', manage: 'sales.orders.manage' },\n quote: { view: 'sales.quotes.view', manage: 'sales.quotes.manage' },\n}\n\nfunction toDocumentKind(value: unknown): DocumentKind | null {\n return value === 'order' || value === 'quote' ? value : null\n}\n\nasync function resolveDocumentKindForRead(query: { documentId?: string; documentKind?: DocumentKind }, ctx: CrudCtx) {\n if (query.documentKind) return query.documentKind\n\n const tenantId = ctx.auth?.tenantId ?? null\n const organizationId =\n ctx.selectedOrganizationId ??\n ctx.auth?.orgId ??\n (Array.isArray(ctx.organizationIds) && ctx.organizationIds.length === 1 ? ctx.organizationIds[0] : null)\n if (!query.documentId || !tenantId || !organizationId) return null\n\n const em = ctx.container.resolve('em') as EntityManager\n const scope = { tenantId, organizationId }\n const order = await findOneWithDecryption(em, SalesOrder, { id: query.documentId, tenantId, organizationId }, {}, scope)\n if (order) return 'order'\n const quote = await findOneWithDecryption(em, SalesQuote, { id: query.documentId, tenantId, organizationId }, {}, scope)\n if (quote) return 'quote'\n return null\n}\n\nasync function ensureDocumentAddressReadAccess(query: { documentId?: string; documentKind?: DocumentKind }, ctx: CrudCtx) {\n const documentKind = await resolveDocumentKindForRead(query, ctx)\n const requiredFeatures = documentKind\n ? [DOCUMENT_ADDRESS_FEATURES[documentKind].view]\n : [DOCUMENT_ADDRESS_FEATURES.order.view, DOCUMENT_ADDRESS_FEATURES.quote.view]\n await ensureDocumentAddressAccess(ctx, requiredFeatures)\n return documentKind\n}\n\nasync function resolveExistingAddressKind(\n ctx: CrudCtx,\n id: string,\n scope: { tenantId: string; organizationId: string },\n): Promise<DocumentKind | null> {\n const em = ctx.container.resolve('em') as EntityManager\n const address = await findOneWithDecryption(\n em,\n SalesDocumentAddress,\n { id, tenantId: scope.tenantId, organizationId: scope.organizationId },\n {},\n scope,\n )\n return toDocumentKind(address?.documentKind)\n}\n\nasync function ensureDocumentAddressAccess(\n ctx: CrudCtx,\n requiredFeatures: string[],\n translate?: Translate,\n): Promise<void> {\n const resolvedTranslate = translate ?? (await resolveTranslations()).translate\n const auth = ctx.auth\n if (!auth?.sub) {\n throw new CrudHttpError(401, { error: resolvedTranslate('api.errors.unauthorized', 'Unauthorized') })\n }\n\n let rbac: RbacService | null = null\n try {\n rbac = ctx.container.resolve('rbacService') as RbacService\n } catch {\n rbac = null\n }\n\n const ok = rbac\n ? await rbac.userHasAllFeatures(auth.sub, requiredFeatures, {\n tenantId: auth.tenantId ?? null,\n organizationId: ctx.selectedOrganizationId ?? auth.orgId ?? null,\n })\n : false\n if (!ok) {\n throw new CrudHttpError(403, {\n error: resolvedTranslate('api.errors.forbidden', 'Forbidden'),\n requiredFeatures,\n })\n }\n}\n\nasync function ensureDocumentAddressKindAccess(\n ctx: CrudCtx,\n documentKind: DocumentKind,\n access: DocumentAddressAccess,\n translate?: Translate,\n): Promise<void> {\n await ensureDocumentAddressAccess(ctx, [DOCUMENT_ADDRESS_FEATURES[documentKind][access]], translate)\n}\n\nasync function ensureDocumentAddressMutationAccess(\n ctx: CrudCtx,\n input: { id: string; documentKind: DocumentKind; tenantId: string; organizationId: string },\n options: { includeTargetKind: boolean; translate?: Translate },\n): Promise<void> {\n const existingKind = await resolveExistingAddressKind(ctx, input.id, {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n })\n const featureKinds = new Set<DocumentKind>()\n if (existingKind) featureKinds.add(existingKind)\n if (!existingKind || options.includeTargetKind) featureKinds.add(input.documentKind)\n\n await ensureDocumentAddressAccess(\n ctx,\n [...featureKinds].map((kind) => DOCUMENT_ADDRESS_FEATURES[kind].manage),\n options.translate,\n )\n}\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n hooks: {\n beforeList: async (query, ctx) => {\n await ensureDocumentAddressReadAccess(query, ctx)\n },\n },\n orm: {\n entity: SalesDocumentAddress,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n indexer: { entityType: E.sales.sales_document_address },\n list: {\n schema: listSchema,\n entityId: E.sales.sales_document_address,\n fields: [\n 'id',\n 'organization_id',\n 'tenant_id',\n 'document_id',\n 'document_kind',\n 'customer_address_id',\n 'name',\n 'purpose',\n 'company_name',\n 'address_line1',\n 'address_line2',\n 'building_number',\n 'flat_number',\n 'city',\n 'region',\n 'postal_code',\n 'country',\n 'latitude',\n 'longitude',\n 'order_id',\n 'quote_id',\n 'created_at',\n 'updated_at',\n ],\n sortFieldMap: {\n createdAt: 'created_at',\n updatedAt: 'updated_at',\n },\n buildFilters: async (query: any, ctx) => {\n const documentKind = await resolveDocumentKindForRead(query, ctx)\n const filters: Record<string, any> = {\n document_id: { $eq: query.documentId },\n }\n\n if (query.documentKind ?? documentKind) filters.document_kind = { $eq: query.documentKind ?? documentKind }\n return filters\n },\n },\n actions: {\n create: {\n commandId: 'sales.document-addresses.create',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const input = documentAddressCreateSchema.parse(withScopedPayload(raw ?? {}, ctx, translate))\n await ensureDocumentAddressKindAccess(ctx, input.documentKind, 'manage', translate)\n return input\n },\n response: ({ result }) => ({ id: result?.id ?? null }),\n status: 201,\n },\n update: {\n commandId: 'sales.document-addresses.update',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const input = documentAddressUpdateSchema.parse(withScopedPayload(raw ?? {}, ctx, translate))\n await ensureDocumentAddressMutationAccess(ctx, input, { includeTargetKind: true, translate })\n return input\n },\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'sales.document-addresses.delete',\n schema: rawBodySchema,\n mapInput: async ({ parsed, ctx }) => {\n const { translate } = await resolveTranslations()\n const id =\n parsed?.body?.id ??\n parsed?.id ??\n parsed?.query?.id ??\n (ctx.request ? new URL(ctx.request.url).searchParams.get('id') : null)\n const documentId =\n parsed?.body?.documentId ??\n parsed?.query?.documentId ??\n (ctx.request ? new URL(ctx.request.url).searchParams.get('documentId') : null)\n const documentKind =\n parsed?.body?.documentKind ??\n parsed?.query?.documentKind ??\n (ctx.request ? new URL(ctx.request.url).searchParams.get('documentKind') : null)\n if (!id || !documentId || !documentKind) {\n throw new CrudHttpError(400, {\n error: translate('sales.documents.detail.error', 'Document not found or inaccessible.'),\n })\n }\n const input = documentAddressDeleteSchema.parse(withScopedPayload({ id, documentId, documentKind }, ctx, translate))\n await ensureDocumentAddressMutationAccess(ctx, input, { includeTargetKind: false, translate })\n return input\n },\n response: () => ({ ok: true }),\n },\n },\n})\n\nconst { GET, POST, PUT, DELETE } = crud\n\nexport { GET, POST, PUT, DELETE }\n\nconst documentAddressSchema = z.object({\n id: z.string().uuid(),\n document_id: z.string().uuid(),\n document_kind: z.enum(['order', 'quote']),\n customer_address_id: z.string().uuid().nullable().optional(),\n name: z.string().nullable().optional(),\n purpose: z.string().nullable().optional(),\n company_name: z.string().nullable().optional(),\n address_line1: z.string(),\n address_line2: z.string().nullable().optional(),\n building_number: z.string().nullable().optional(),\n flat_number: z.string().nullable().optional(),\n city: z.string().nullable().optional(),\n region: z.string().nullable().optional(),\n postal_code: z.string().nullable().optional(),\n country: z.string().nullable().optional(),\n latitude: z.number().nullable().optional(),\n longitude: z.number().nullable().optional(),\n created_at: z.string(),\n updated_at: z.string(),\n})\n\nexport const openApi = createSalesCrudOpenApi({\n resourceName: 'Document address',\n querySchema: listSchema,\n listResponseSchema: createPagedListResponseSchema(documentAddressSchema),\n create: {\n schema: documentAddressCreateSchema,\n responseSchema: z.object({ id: z.string().uuid().nullable() }),\n description: 'Creates a sales document address linked to an order or quote.',\n },\n update: {\n schema: documentAddressUpdateSchema,\n responseSchema: defaultOkResponseSchema,\n description: 'Updates a sales document address.',\n },\n del: {\n schema: documentAddressDeleteSchema.pick({ id: true, documentId: true, documentKind: true }),\n responseSchema: defaultOkResponseSchema,\n description: 'Deletes a sales document address.',\n },\n})\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -62,6 +62,11 @@ function applyDocumentAddressSnapshot(em, entity, snapshot) {
|
|
|
62
62
|
entity.order = snapshot.documentKind === "order" ? em.getReference(SalesOrder, snapshot.documentId) : null;
|
|
63
63
|
entity.quote = snapshot.documentKind === "quote" ? em.getReference(SalesQuote, snapshot.documentId) : null;
|
|
64
64
|
}
|
|
65
|
+
function assertDocumentAddressParent(entity, input) {
|
|
66
|
+
if (entity.documentId !== input.documentId || entity.documentKind !== input.documentKind) {
|
|
67
|
+
throw new CrudHttpError(404, { error: "sales.document.address.not_found" });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
65
70
|
async function emitDocumentAddressIndexSideEffects(ctx, action, snapshot) {
|
|
66
71
|
let dataEngine = null;
|
|
67
72
|
try {
|
|
@@ -196,6 +201,7 @@ const updateDocumentAddress = {
|
|
|
196
201
|
"sales.document.address.not_found"
|
|
197
202
|
);
|
|
198
203
|
ensureSameScope(entity, input.organizationId, input.tenantId);
|
|
204
|
+
assertDocumentAddressParent(entity, input);
|
|
199
205
|
const document = await requireDocument(em, input.documentKind, input.documentId, input.organizationId, input.tenantId);
|
|
200
206
|
if (input.documentKind === "order") {
|
|
201
207
|
await assertAddressEditable(em, {
|
|
@@ -276,6 +282,7 @@ const deleteDocumentAddress = {
|
|
|
276
282
|
"sales.document.address.not_found"
|
|
277
283
|
);
|
|
278
284
|
ensureSameScope(entity, input.organizationId, input.tenantId);
|
|
285
|
+
assertDocumentAddressParent(entity, input);
|
|
279
286
|
const document = await requireDocument(em, input.documentKind, input.documentId, input.organizationId, input.tenantId);
|
|
280
287
|
if (input.documentKind === "order") {
|
|
281
288
|
await assertAddressEditable(em, {
|