@open-mercato/core 0.6.6-develop.5612.1.d382eb2f33 → 0.6.6-develop.5619.1.29f01e2c42
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/generated/entities/api_key/index.js +2 -0
- package/dist/generated/entities/api_key/index.js.map +2 -2
- package/dist/generated/entities/attachment_partition/index.js +4 -0
- package/dist/generated/entities/attachment_partition/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +3 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/api_keys/data/entities.js +9 -2
- package/dist/modules/api_keys/data/entities.js.map +2 -2
- package/dist/modules/api_keys/migrations/Migration20260523234901.js +17 -0
- package/dist/modules/api_keys/migrations/Migration20260523234901.js.map +7 -0
- package/dist/modules/api_keys/services/apiKeyService.js +26 -0
- package/dist/modules/api_keys/services/apiKeyService.js.map +2 -2
- package/dist/modules/attachments/api/partitions/route.js +54 -12
- package/dist/modules/attachments/api/partitions/route.js.map +2 -2
- package/dist/modules/attachments/data/entities.js +7 -0
- package/dist/modules/attachments/data/entities.js.map +2 -2
- package/dist/modules/attachments/migrations/Migration20260524000000.js +27 -0
- package/dist/modules/attachments/migrations/Migration20260524000000.js.map +7 -0
- package/dist/modules/auth/api/roles/route.js +6 -35
- package/dist/modules/auth/api/roles/route.js.map +2 -2
- package/dist/modules/auth/cli.js +26 -4
- package/dist/modules/auth/cli.js.map +2 -2
- package/dist/modules/auth/commands/roles.js +34 -11
- package/dist/modules/auth/commands/roles.js.map +2 -2
- package/dist/modules/auth/lib/roleTenantGuard.js +41 -13
- package/dist/modules/auth/lib/roleTenantGuard.js.map +3 -3
- package/dist/modules/auth/lib/setup-app.js +88 -26
- package/dist/modules/auth/lib/setup-app.js.map +2 -2
- package/dist/modules/currencies/api/fetch-configs/route.js +4 -2
- package/dist/modules/currencies/api/fetch-configs/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/route.js +5 -1
- package/dist/modules/integrations/api/[id]/route.js.map +2 -2
- package/dist/modules/integrations/api/route.js +5 -1
- package/dist/modules/integrations/api/route.js.map +2 -2
- package/dist/modules/integrations/lib/credentials-service.js +6 -4
- package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
- package/dist/modules/sales/acl.js +6 -0
- package/dist/modules/sales/acl.js.map +2 -2
- package/dist/modules/sales/api/returns/route.js +43 -3
- package/dist/modules/sales/api/returns/route.js.map +2 -2
- package/dist/modules/sales/commands/returns.js +473 -213
- package/dist/modules/sales/commands/returns.js.map +2 -2
- package/dist/modules/sales/commands/shared.js +2 -0
- package/dist/modules/sales/commands/shared.js.map +2 -2
- package/dist/modules/sales/components/documents/ReturnEditDialog.js +125 -0
- package/dist/modules/sales/components/documents/ReturnEditDialog.js.map +7 -0
- package/dist/modules/sales/components/documents/ReturnsSection.js +102 -6
- package/dist/modules/sales/components/documents/ReturnsSection.js.map +2 -2
- package/dist/modules/sales/data/validators.js +13 -0
- package/dist/modules/sales/data/validators.js.map +2 -2
- package/dist/modules/sales/setup.js +1 -0
- package/dist/modules/sales/setup.js.map +2 -2
- package/generated/entities/api_key/index.ts +1 -0
- package/generated/entities/attachment_partition/index.ts +2 -0
- package/generated/entity-fields-registry.ts +3 -0
- package/package.json +7 -7
- package/src/modules/api_keys/data/entities.ts +15 -1
- package/src/modules/api_keys/migrations/.snapshot-open-mercato.json +18 -0
- package/src/modules/api_keys/migrations/Migration20260523234901.ts +17 -0
- package/src/modules/api_keys/services/apiKeyService.ts +63 -0
- package/src/modules/attachments/api/partitions/route.ts +58 -11
- package/src/modules/attachments/data/entities.ts +7 -0
- package/src/modules/attachments/migrations/.snapshot-open-mercato.json +28 -0
- package/src/modules/attachments/migrations/Migration20260524000000.ts +25 -0
- package/src/modules/auth/api/roles/route.ts +7 -38
- package/src/modules/auth/cli.ts +27 -4
- package/src/modules/auth/commands/roles.ts +39 -11
- package/src/modules/auth/lib/roleTenantGuard.ts +56 -17
- package/src/modules/auth/lib/setup-app.ts +160 -32
- package/src/modules/currencies/api/fetch-configs/route.ts +4 -2
- package/src/modules/integrations/api/[id]/route.ts +5 -2
- package/src/modules/integrations/api/route.ts +5 -2
- package/src/modules/integrations/lib/credentials-service.ts +15 -5
- package/src/modules/sales/acl.ts +6 -0
- package/src/modules/sales/api/returns/route.ts +41 -3
- package/src/modules/sales/commands/returns.ts +561 -229
- package/src/modules/sales/commands/shared.ts +1 -0
- package/src/modules/sales/components/documents/ReturnEditDialog.tsx +157 -0
- package/src/modules/sales/components/documents/ReturnsSection.tsx +105 -3
- package/src/modules/sales/data/validators.ts +15 -0
- package/src/modules/sales/i18n/de.json +11 -0
- package/src/modules/sales/i18n/en.json +11 -0
- package/src/modules/sales/i18n/es.json +11 -0
- package/src/modules/sales/i18n/pl.json +11 -0
- package/src/modules/sales/setup.ts +1 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { hash } from 'bcryptjs'
|
|
2
|
+
import { randomBytes } from 'node:crypto'
|
|
2
3
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
3
4
|
import { Role, RoleAcl, User, UserRole } from '@open-mercato/core/modules/auth/data/entities'
|
|
4
5
|
import { Tenant, Organization } from '@open-mercato/core/modules/directory/data/entities'
|
|
@@ -80,6 +81,29 @@ const DERIVED_EMAIL_ENV = {
|
|
|
80
81
|
employee: 'OM_INIT_EMPLOYEE_EMAIL',
|
|
81
82
|
} as const
|
|
82
83
|
|
|
84
|
+
export type DemoUserRole = 'superadmin' | 'admin' | 'employee'
|
|
85
|
+
export type DemoUserEmail = { role: DemoUserRole; email: string }
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Returns the canonical list of demo user emails the setup path may have
|
|
89
|
+
* seeded. Honors OM_INIT_ADMIN_EMAIL / OM_INIT_EMPLOYEE_EMAIL exactly the
|
|
90
|
+
* same way the derived-user seeding branch does, so the deactivation loop
|
|
91
|
+
* never drifts from the seeding loop.
|
|
92
|
+
*
|
|
93
|
+
* Pure function — no side effects, safe to unit-test in isolation.
|
|
94
|
+
*
|
|
95
|
+
* @internal Exported for tests; not part of the public auth API.
|
|
96
|
+
*/
|
|
97
|
+
export function resolveDemoUserEmails(): DemoUserEmail[] {
|
|
98
|
+
const adminEmail = readEnvValue(DERIVED_EMAIL_ENV.admin) ?? `admin@${DEFAULT_DERIVED_EMAIL_DOMAIN}`
|
|
99
|
+
const employeeEmail = readEnvValue(DERIVED_EMAIL_ENV.employee) ?? `employee@${DEFAULT_DERIVED_EMAIL_DOMAIN}`
|
|
100
|
+
return [
|
|
101
|
+
{ role: 'superadmin', email: DEMO_SUPERADMIN_EMAIL },
|
|
102
|
+
{ role: 'admin', email: adminEmail },
|
|
103
|
+
{ role: 'employee', email: employeeEmail },
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
|
|
83
107
|
export type SetupInitialTenantOptions = {
|
|
84
108
|
orgName: string
|
|
85
109
|
primaryUser: PrimaryUserInput
|
|
@@ -97,6 +121,20 @@ export type SetupInitialTenantOptions = {
|
|
|
97
121
|
* reusing or clobbering an existing organization.
|
|
98
122
|
*/
|
|
99
123
|
orgSlug?: string
|
|
124
|
+
/**
|
|
125
|
+
* Opt-in flag that allows seeding the derived admin/employee accounts when
|
|
126
|
+
* the OM_INIT_ADMIN_PASSWORD / OM_INIT_EMPLOYEE_PASSWORD env vars are unset.
|
|
127
|
+
*
|
|
128
|
+
* - In non-production (`NODE_ENV !== 'production'`): the demo accounts are
|
|
129
|
+
* seeded with the well-known `'secret'` password so local dev workflows
|
|
130
|
+
* (e.g. `mercato init`) stay predictable for developers.
|
|
131
|
+
* - In production: the fallback uses a randomly generated 96-bit password
|
|
132
|
+
* (printed once by the CLI) so an operator who deliberately opts in still
|
|
133
|
+
* avoids the historical hardcoded credential. When this flag is false/unset
|
|
134
|
+
* in production and the env vars are missing, `DerivedUserPasswordRequiredError`
|
|
135
|
+
* is thrown instead of silently seeding any account.
|
|
136
|
+
*/
|
|
137
|
+
allowDemoDerivedPasswords?: boolean
|
|
100
138
|
}
|
|
101
139
|
|
|
102
140
|
export class OrgSlugExistsError extends Error {
|
|
@@ -106,10 +144,19 @@ export class OrgSlugExistsError extends Error {
|
|
|
106
144
|
}
|
|
107
145
|
}
|
|
108
146
|
|
|
147
|
+
export class DerivedUserPasswordRequiredError extends Error {
|
|
148
|
+
constructor(public readonly missing: string[]) {
|
|
149
|
+
super(
|
|
150
|
+
`DERIVED_USER_PASSWORD_REQUIRED: missing ${missing.join(', ')} (set the env vars, pass allowDemoDerivedPasswords: true / --include-demo-users in non-production, or disable derived user seeding)`,
|
|
151
|
+
)
|
|
152
|
+
this.name = 'DerivedUserPasswordRequiredError'
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
109
156
|
export type SetupInitialTenantResult = {
|
|
110
157
|
tenantId: string
|
|
111
158
|
organizationId: string
|
|
112
|
-
users: Array<{ user: User; roles: string[]; created: boolean }>
|
|
159
|
+
users: Array<{ user: User; roles: string[]; created: boolean; generatedPassword?: string | null }>
|
|
113
160
|
reusedExistingUser: boolean
|
|
114
161
|
}
|
|
115
162
|
|
|
@@ -161,7 +208,7 @@ export async function setupInitialTenant(
|
|
|
161
208
|
let tenantId: string | undefined
|
|
162
209
|
let organizationId: string | undefined
|
|
163
210
|
let reusedExistingUser = false
|
|
164
|
-
const userSnapshots: Array<{ user: User; roles: string[]; created: boolean }> = []
|
|
211
|
+
const userSnapshots: Array<{ user: User; roles: string[]; created: boolean; generatedPassword?: string | null }> = []
|
|
165
212
|
|
|
166
213
|
await em.transactional(async (tem) => {
|
|
167
214
|
if (!existingUser) return
|
|
@@ -202,6 +249,7 @@ export async function setupInitialTenant(
|
|
|
202
249
|
roles: string[]
|
|
203
250
|
name?: string | null
|
|
204
251
|
passwordHash?: string | null
|
|
252
|
+
generatedPassword?: string | null
|
|
205
253
|
}> = [
|
|
206
254
|
{ email: primaryUser.email, roles: primaryRoles, name: resolvePrimaryName(primaryUser) },
|
|
207
255
|
]
|
|
@@ -210,14 +258,43 @@ export async function setupInitialTenant(
|
|
|
210
258
|
const employeeOverride = readEnvValue(DERIVED_EMAIL_ENV.employee)
|
|
211
259
|
const adminEmail = adminOverride ?? `admin@${DEFAULT_DERIVED_EMAIL_DOMAIN}`
|
|
212
260
|
const employeeEmail = employeeOverride ?? `employee@${DEFAULT_DERIVED_EMAIL_DOMAIN}`
|
|
213
|
-
const
|
|
214
|
-
const
|
|
215
|
-
const
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
:
|
|
219
|
-
|
|
220
|
-
|
|
261
|
+
const envAdminPwd = readEnvValue('OM_INIT_ADMIN_PASSWORD')
|
|
262
|
+
const envEmployeePwd = readEnvValue('OM_INIT_EMPLOYEE_PASSWORD')
|
|
263
|
+
const isProduction = process.env.NODE_ENV === 'production'
|
|
264
|
+
const allowDemo = options.allowDemoDerivedPasswords === true
|
|
265
|
+
if (isProduction && !allowDemo) {
|
|
266
|
+
const missing: string[] = []
|
|
267
|
+
if (!envAdminPwd) missing.push('OM_INIT_ADMIN_PASSWORD')
|
|
268
|
+
if (!envEmployeePwd) missing.push('OM_INIT_EMPLOYEE_PASSWORD')
|
|
269
|
+
if (missing.length) {
|
|
270
|
+
throw new DerivedUserPasswordRequiredError(missing)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// In non-production, fall back to the well-known DEMO_DERIVED_PASSWORD so
|
|
274
|
+
// local dev workflows stay predictable (admin@/employee@ login with the
|
|
275
|
+
// documented demo password). In production we either have env-supplied
|
|
276
|
+
// values, or — for opt-in --include-demo-users flows — we generate a
|
|
277
|
+
// random one-time password and surface it via `generatedPassword` so the
|
|
278
|
+
// operator can capture it. Production callers without env vars and
|
|
279
|
+
// without the opt-in already threw above.
|
|
280
|
+
const fallbackAdminPwd = isProduction ? generateDerivedPassword() : DEMO_DERIVED_PASSWORD
|
|
281
|
+
const fallbackEmployeePwd = isProduction ? generateDerivedPassword() : DEMO_DERIVED_PASSWORD
|
|
282
|
+
const adminPasswordPlain = envAdminPwd ?? fallbackAdminPwd
|
|
283
|
+
const employeePasswordPlain = envEmployeePwd ?? fallbackEmployeePwd
|
|
284
|
+
const adminPasswordHash = await hash(adminPasswordPlain, 10)
|
|
285
|
+
const employeePasswordHash = await hash(employeePasswordPlain, 10)
|
|
286
|
+
addUniqueBaseUser(baseUsers, {
|
|
287
|
+
email: adminEmail,
|
|
288
|
+
roles: ['admin'],
|
|
289
|
+
passwordHash: adminPasswordHash,
|
|
290
|
+
generatedPassword: envAdminPwd ? null : adminPasswordPlain,
|
|
291
|
+
})
|
|
292
|
+
addUniqueBaseUser(baseUsers, {
|
|
293
|
+
email: employeeEmail,
|
|
294
|
+
roles: ['employee'],
|
|
295
|
+
passwordHash: employeePasswordHash,
|
|
296
|
+
generatedPassword: envEmployeePwd ? null : employeePasswordPlain,
|
|
297
|
+
})
|
|
221
298
|
}
|
|
222
299
|
const passwordHash = await resolvePasswordHash(primaryUser)
|
|
223
300
|
|
|
@@ -327,7 +404,7 @@ export async function setupInitialTenant(
|
|
|
327
404
|
if (base.name) user.name = base.name
|
|
328
405
|
if (confirm) user.isConfirmed = true
|
|
329
406
|
tem.persist(user)
|
|
330
|
-
userSnapshots.push({ user, roles: base.roles, created: false })
|
|
407
|
+
userSnapshots.push({ user, roles: base.roles, created: false, generatedPassword: base.generatedPassword ?? null })
|
|
331
408
|
} else {
|
|
332
409
|
user = tem.create(User, {
|
|
333
410
|
email: (encryptedPayload as any).email ?? base.email,
|
|
@@ -340,7 +417,7 @@ export async function setupInitialTenant(
|
|
|
340
417
|
createdAt: new Date(),
|
|
341
418
|
})
|
|
342
419
|
tem.persist(user)
|
|
343
|
-
userSnapshots.push({ user, roles: base.roles, created: true })
|
|
420
|
+
userSnapshots.push({ user, roles: base.roles, created: true, generatedPassword: base.generatedPassword ?? null })
|
|
344
421
|
}
|
|
345
422
|
await tem.flush()
|
|
346
423
|
for (const roleName of base.roles) {
|
|
@@ -362,7 +439,7 @@ export async function setupInitialTenant(
|
|
|
362
439
|
}
|
|
363
440
|
|
|
364
441
|
await ensureDefaultRoleAcls(em, tenantId, resolvedModules, { includeSuperadminRole })
|
|
365
|
-
await
|
|
442
|
+
await deactivateDemoUsersIfSelfOnboardingEnabled(em)
|
|
366
443
|
|
|
367
444
|
// Call module onTenantCreated hooks
|
|
368
445
|
for (const mod of resolvedModules) {
|
|
@@ -394,8 +471,8 @@ function readEnvValue(key: string): string | undefined {
|
|
|
394
471
|
}
|
|
395
472
|
|
|
396
473
|
function addUniqueBaseUser(
|
|
397
|
-
baseUsers: Array<{ email: string; roles: string[]; name?: string | null; passwordHash?: string | null }>,
|
|
398
|
-
entry: { email: string; roles: string[]; name?: string | null; passwordHash?: string | null },
|
|
474
|
+
baseUsers: Array<{ email: string; roles: string[]; name?: string | null; passwordHash?: string | null; generatedPassword?: string | null }>,
|
|
475
|
+
entry: { email: string; roles: string[]; name?: string | null; passwordHash?: string | null; generatedPassword?: string | null },
|
|
399
476
|
) {
|
|
400
477
|
if (!entry.email) return
|
|
401
478
|
const normalized = entry.email.toLowerCase()
|
|
@@ -403,6 +480,26 @@ function addUniqueBaseUser(
|
|
|
403
480
|
baseUsers.push(entry)
|
|
404
481
|
}
|
|
405
482
|
|
|
483
|
+
/**
|
|
484
|
+
* Well-known demo password seeded for derived admin@/employee@ accounts in
|
|
485
|
+
* non-production (`NODE_ENV !== 'production'`) when no env override is set.
|
|
486
|
+
* Keeps the documented `yarn dev` / `mercato init` DX predictable. Never used
|
|
487
|
+
* in production: the production branch either consumes env-supplied values or
|
|
488
|
+
* falls back to `generateDerivedPassword()` so credentials remain non-guessable.
|
|
489
|
+
*/
|
|
490
|
+
const DEMO_DERIVED_PASSWORD = 'secret'
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Generate a 16-character base64url password (96 bits of entropy) for a derived
|
|
494
|
+
* demo user in production when no env override is provided AND the caller
|
|
495
|
+
* explicitly opted in via `allowDemoDerivedPasswords`. Surfaced via the
|
|
496
|
+
* `users[].generatedPassword` snapshot so CLI callers can print it to the
|
|
497
|
+
* operator — there is no other recovery path for these credentials.
|
|
498
|
+
*/
|
|
499
|
+
function generateDerivedPassword(): string {
|
|
500
|
+
return randomBytes(12).toString('base64url')
|
|
501
|
+
}
|
|
502
|
+
|
|
406
503
|
function isDemoModeEnabled(): boolean {
|
|
407
504
|
const parsed = parseBooleanToken(process.env.DEMO_MODE ?? '')
|
|
408
505
|
return parsed === false ? false : true
|
|
@@ -562,29 +659,60 @@ async function ensureRoleAclFor(
|
|
|
562
659
|
}
|
|
563
660
|
}
|
|
564
661
|
|
|
565
|
-
|
|
662
|
+
/**
|
|
663
|
+
* Neutralizes every demo account the setup path may have seeded
|
|
664
|
+
* (superadmin, admin, employee — honoring OM_INIT_*_EMAIL overrides)
|
|
665
|
+
* when SELF_SERVICE_ONBOARDING_ENABLED is on and the operator did not
|
|
666
|
+
* opt in to keeping demo credentials via shouldKeepDemoSuperadminDuringInit.
|
|
667
|
+
*
|
|
668
|
+
* Each user is processed in its own try/catch so a single failure (e.g.
|
|
669
|
+
* decryption error on a legacy row) does not skip the remaining accounts.
|
|
670
|
+
*
|
|
671
|
+
* @internal Exported for tests; not part of the public auth API.
|
|
672
|
+
*/
|
|
673
|
+
export async function deactivateDemoUsersIfSelfOnboardingEnabled(em: EntityManager) {
|
|
566
674
|
if (process.env.SELF_SERVICE_ONBOARDING_ENABLED !== 'true') return
|
|
567
675
|
if (shouldKeepDemoSuperadminDuringInit()) return
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
user
|
|
578
|
-
dirty =
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
676
|
+
for (const { role, email } of resolveDemoUserEmails()) {
|
|
677
|
+
try {
|
|
678
|
+
const user = await findOneWithDecryption(
|
|
679
|
+
em,
|
|
680
|
+
User,
|
|
681
|
+
{ email },
|
|
682
|
+
{},
|
|
683
|
+
{ tenantId: null, organizationId: null },
|
|
684
|
+
)
|
|
685
|
+
if (!user) continue
|
|
686
|
+
let dirty = false
|
|
687
|
+
if (user.passwordHash) {
|
|
688
|
+
user.passwordHash = null
|
|
689
|
+
dirty = true
|
|
690
|
+
}
|
|
691
|
+
if (user.isConfirmed !== false) {
|
|
692
|
+
user.isConfirmed = false
|
|
693
|
+
dirty = true
|
|
694
|
+
}
|
|
695
|
+
if (dirty) {
|
|
696
|
+
await em.persist(user).flush()
|
|
697
|
+
}
|
|
698
|
+
} catch (error) {
|
|
699
|
+
console.error(
|
|
700
|
+
`[auth.setup] failed to deactivate demo ${role} user (${email})`,
|
|
701
|
+
error,
|
|
702
|
+
)
|
|
582
703
|
}
|
|
583
|
-
} catch (error) {
|
|
584
|
-
console.error('[auth.setup] failed to deactivate demo superadmin user', error)
|
|
585
704
|
}
|
|
586
705
|
}
|
|
587
706
|
|
|
707
|
+
/**
|
|
708
|
+
* @deprecated Renamed to {@link deactivateDemoUsersIfSelfOnboardingEnabled}
|
|
709
|
+
* because the helper now neutralizes admin/employee demo accounts in addition
|
|
710
|
+
* to superadmin (security tracker finding #5). Kept as an internal alias to
|
|
711
|
+
* avoid breaking any out-of-tree caller that imported the old name; will be
|
|
712
|
+
* removed in a future major.
|
|
713
|
+
*/
|
|
714
|
+
const deactivateDemoSuperAdminIfSelfOnboardingEnabled = deactivateDemoUsersIfSelfOnboardingEnabled
|
|
715
|
+
|
|
588
716
|
/** Try to get modules from runtime registry; returns empty array if not yet registered. */
|
|
589
717
|
function tryGetModules(): Module[] {
|
|
590
718
|
try {
|
|
@@ -13,8 +13,10 @@ import {
|
|
|
13
13
|
import { currencyFetchConfigCreateSchema, currencyFetchConfigUpdateSchema } from '../../data/validators'
|
|
14
14
|
|
|
15
15
|
export const metadata = {
|
|
16
|
-
requireAuth: true,
|
|
17
|
-
requireFeatures: ['currencies.fetch.
|
|
16
|
+
GET: { requireAuth: true, requireFeatures: ['currencies.fetch.view'] },
|
|
17
|
+
POST: { requireAuth: true, requireFeatures: ['currencies.fetch.manage'] },
|
|
18
|
+
PUT: { requireAuth: true, requireFeatures: ['currencies.fetch.manage'] },
|
|
19
|
+
DELETE: { requireAuth: true, requireFeatures: ['currencies.fetch.manage'] },
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export async function GET(req: NextRequest) {
|
|
@@ -3,7 +3,7 @@ import { z } from 'zod'
|
|
|
3
3
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
4
4
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
5
5
|
import { getBundle, getBundleIntegrations, getIntegration } from '@open-mercato/shared/modules/integrations/types'
|
|
6
|
-
import type
|
|
6
|
+
import { CredentialsEncryptionUnavailableError, type CredentialsService } from '../../lib/credentials-service'
|
|
7
7
|
import type { IntegrationStateService } from '../../lib/state-service'
|
|
8
8
|
import type { IntegrationLogService } from '../../lib/log-service'
|
|
9
9
|
import { deriveIntegrationHealthStatus, getEffectiveHealthCheckConfig } from '../../lib/health-service'
|
|
@@ -60,7 +60,10 @@ export async function GET(req: Request, ctx: { params?: Promise<{ id?: string }>
|
|
|
60
60
|
const scope = { organizationId: auth.orgId as string, tenantId: auth.tenantId }
|
|
61
61
|
|
|
62
62
|
const [credentials, state, analyticsMap] = await Promise.all([
|
|
63
|
-
credentialsService.resolve(integration.id, scope)
|
|
63
|
+
credentialsService.resolve(integration.id, scope).catch((err) => {
|
|
64
|
+
if (err instanceof CredentialsEncryptionUnavailableError) return null
|
|
65
|
+
throw err
|
|
66
|
+
}),
|
|
64
67
|
stateService.resolveState(integration.id, scope),
|
|
65
68
|
logService.aggregateAnalytics([integration.id], scope, 30),
|
|
66
69
|
])
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
3
3
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
4
|
-
import type
|
|
4
|
+
import { CredentialsEncryptionUnavailableError, type CredentialsService } from '../lib/credentials-service'
|
|
5
5
|
import type { IntegrationStateService } from '../lib/state-service'
|
|
6
6
|
import type { IntegrationLogService } from '../lib/log-service'
|
|
7
7
|
import { deriveIntegrationHealthStatus, getEffectiveHealthCheckConfig } from '../lib/health-service'
|
|
@@ -101,7 +101,10 @@ export async function GET(req: Request) {
|
|
|
101
101
|
const baseRows: ListRow[] = await Promise.all(
|
|
102
102
|
getAllIntegrations().map(async (integration) => {
|
|
103
103
|
const [resolvedCredentials, state] = await Promise.all([
|
|
104
|
-
credentialsService.resolve(integration.id, scope)
|
|
104
|
+
credentialsService.resolve(integration.id, scope).catch((err) => {
|
|
105
|
+
if (err instanceof CredentialsEncryptionUnavailableError) return null
|
|
106
|
+
throw err
|
|
107
|
+
}),
|
|
105
108
|
stateService.resolveState(integration.id, scope),
|
|
106
109
|
])
|
|
107
110
|
|
|
@@ -13,12 +13,22 @@ import { EncryptionMap } from '../../entities/data/entities'
|
|
|
13
13
|
import { IntegrationCredentials } from '../data/entities'
|
|
14
14
|
|
|
15
15
|
const ENCRYPTED_CREDENTIALS_BLOB_KEY = '__om_encrypted_credentials_blob_v1'
|
|
16
|
-
const CREDENTIALS_ENCRYPTION_UNAVAILABLE_MESSAGE =
|
|
17
|
-
'Integration credentials encryption key is unavailable. Configure Vault KMS or TENANT_DATA_ENCRYPTION_FALLBACK_KEY.'
|
|
18
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Raised when integration credentials cannot be encrypted or decrypted because
|
|
19
|
+
* no tenant DEK is available — typically a production deployment with neither
|
|
20
|
+
* Vault nor `TENANT_DATA_ENCRYPTION_FALLBACK_KEY` (or equivalent env vars)
|
|
21
|
+
* configured. The credentials path deliberately fails closed instead of using
|
|
22
|
+
* a hardcoded fallback secret; see security tracker finding #7.
|
|
23
|
+
*/
|
|
19
24
|
export class CredentialsEncryptionUnavailableError extends Error {
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
readonly code = 'CREDENTIALS_ENCRYPTION_UNAVAILABLE'
|
|
26
|
+
constructor(tenantId: string) {
|
|
27
|
+
super(
|
|
28
|
+
`Cannot encrypt or decrypt integration credentials for tenant ${tenantId}: ` +
|
|
29
|
+
`no tenant DEK is available. Configure Vault (VAULT_ADDR/VAULT_TOKEN) or ` +
|
|
30
|
+
`set TENANT_DATA_ENCRYPTION_FALLBACK_KEY in the environment.`,
|
|
31
|
+
)
|
|
22
32
|
this.name = 'CredentialsEncryptionUnavailableError'
|
|
23
33
|
}
|
|
24
34
|
}
|
|
@@ -110,7 +120,7 @@ export function createCredentialsService(em: EntityManager) {
|
|
|
110
120
|
const created = await kms.createTenantDek(scope.tenantId)
|
|
111
121
|
if (created?.key) return created.key
|
|
112
122
|
|
|
113
|
-
throw new CredentialsEncryptionUnavailableError()
|
|
123
|
+
throw new CredentialsEncryptionUnavailableError(scope.tenantId)
|
|
114
124
|
}
|
|
115
125
|
|
|
116
126
|
async function encryptCredentialsBlob(
|
package/src/modules/sales/acl.ts
CHANGED
|
@@ -82,6 +82,12 @@ export const features = [
|
|
|
82
82
|
module: 'sales',
|
|
83
83
|
dependsOn: ['sales.returns.view', 'sales.orders.manage'],
|
|
84
84
|
},
|
|
85
|
+
{
|
|
86
|
+
id: 'sales.returns.manage',
|
|
87
|
+
title: 'Edit and delete order returns',
|
|
88
|
+
module: 'sales',
|
|
89
|
+
dependsOn: ['sales.returns.create'],
|
|
90
|
+
},
|
|
85
91
|
{
|
|
86
92
|
id: 'sales.invoices.manage',
|
|
87
93
|
title: 'Manage sales invoices',
|
|
@@ -5,7 +5,8 @@ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
|
5
5
|
import { splitCustomFieldPayload } from '@open-mercato/shared/lib/crud/custom-fields'
|
|
6
6
|
import { withScopedPayload } from '../utils'
|
|
7
7
|
import { SalesReturn, SalesReturnLine } from '../../data/entities'
|
|
8
|
-
import { returnCreateSchema } from '../../data/validators'
|
|
8
|
+
import { returnCreateSchema, returnUpdateSchema, returnDeleteSchema } from '../../data/validators'
|
|
9
|
+
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
9
10
|
import { E } from '#generated/entities.ids.generated'
|
|
10
11
|
import * as F from '#generated/entities/sales_return'
|
|
11
12
|
import { createPagedListResponseSchema } from '../openapi'
|
|
@@ -27,6 +28,8 @@ const listSchema = z
|
|
|
27
28
|
const routeMetadata = {
|
|
28
29
|
GET: { requireAuth: true, requireFeatures: ['sales.returns.view'] },
|
|
29
30
|
POST: { requireAuth: true, requireFeatures: ['sales.returns.create'] },
|
|
31
|
+
PUT: { requireAuth: true, requireFeatures: ['sales.returns.manage'] },
|
|
32
|
+
DELETE: { requireAuth: true, requireFeatures: ['sales.returns.manage'] },
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
const toNumber = (value: unknown): number => {
|
|
@@ -91,6 +94,31 @@ const crud = makeCrudRoute({
|
|
|
91
94
|
response: ({ result }) => ({ id: result?.returnId ?? null }),
|
|
92
95
|
status: 201,
|
|
93
96
|
},
|
|
97
|
+
update: {
|
|
98
|
+
commandId: 'sales.returns.update',
|
|
99
|
+
schema: rawBodySchema,
|
|
100
|
+
mapInput: async ({ raw, ctx }) => {
|
|
101
|
+
const { translate } = await resolveTranslations()
|
|
102
|
+
const scoped = withScopedPayload(raw ?? {}, ctx, translate)
|
|
103
|
+
const { base } = splitCustomFieldPayload(scoped)
|
|
104
|
+
return returnUpdateSchema.parse(base)
|
|
105
|
+
},
|
|
106
|
+
response: () => ({ ok: true }),
|
|
107
|
+
},
|
|
108
|
+
delete: {
|
|
109
|
+
commandId: 'sales.returns.delete',
|
|
110
|
+
schema: rawBodySchema,
|
|
111
|
+
mapInput: async ({ raw, ctx }) => {
|
|
112
|
+
const { translate } = await resolveTranslations()
|
|
113
|
+
const merged = { ...(raw?.body ?? {}), ...(raw?.query ?? {}) }
|
|
114
|
+
const payload = returnDeleteSchema.parse(withScopedPayload(merged, ctx, translate))
|
|
115
|
+
if (!payload.id || !payload.orderId) {
|
|
116
|
+
throw new CrudHttpError(400, { error: translate('sales.returns.notFound', 'Return not found.') })
|
|
117
|
+
}
|
|
118
|
+
return payload
|
|
119
|
+
},
|
|
120
|
+
response: () => ({ ok: true }),
|
|
121
|
+
},
|
|
94
122
|
},
|
|
95
123
|
hooks: {
|
|
96
124
|
afterList: async (payload, ctx) => {
|
|
@@ -131,10 +159,10 @@ const crud = makeCrudRoute({
|
|
|
131
159
|
},
|
|
132
160
|
})
|
|
133
161
|
|
|
134
|
-
const { GET, POST } = crud
|
|
162
|
+
const { GET, POST, PUT, DELETE } = crud
|
|
135
163
|
|
|
136
164
|
export const metadata = routeMetadata
|
|
137
|
-
export { GET, POST }
|
|
165
|
+
export { GET, POST, PUT, DELETE }
|
|
138
166
|
|
|
139
167
|
const returnSchema = z
|
|
140
168
|
.object({
|
|
@@ -167,6 +195,16 @@ export const openApi: OpenApiRouteDoc = {
|
|
|
167
195
|
requestBody: { schema: returnCreateSchema },
|
|
168
196
|
responses: [{ status: 201, description: 'Return created', schema: z.object({ id: z.string().uuid().nullable() }) }],
|
|
169
197
|
},
|
|
198
|
+
PUT: {
|
|
199
|
+
summary: 'Update return',
|
|
200
|
+
requestBody: { schema: returnUpdateSchema },
|
|
201
|
+
responses: [{ status: 200, description: 'Return updated', schema: z.object({ ok: z.boolean() }) }],
|
|
202
|
+
},
|
|
203
|
+
DELETE: {
|
|
204
|
+
summary: 'Delete return',
|
|
205
|
+
requestBody: { schema: returnDeleteSchema },
|
|
206
|
+
responses: [{ status: 200, description: 'Return deleted', schema: z.object({ ok: z.boolean() }) }],
|
|
207
|
+
},
|
|
170
208
|
},
|
|
171
209
|
}
|
|
172
210
|
|