@open-mercato/core 0.7.1-develop.7154.1.981330c924 → 0.7.1-develop.7171.1.9b31dbba45
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/AGENTS.md +2 -2
- package/dist/modules/auth/commands/users.js +14 -1
- package/dist/modules/auth/commands/users.js.map +2 -2
- package/dist/modules/auth/data/entities.js +2 -2
- package/dist/modules/auth/data/entities.js.map +2 -2
- package/dist/modules/auth/lib/backendChrome.js +26 -3
- package/dist/modules/auth/lib/backendChrome.js.map +2 -2
- package/dist/modules/auth/lib/profile-sections.js +1 -1
- package/dist/modules/auth/lib/profile-sections.js.map +2 -2
- package/dist/modules/auth/migrations/Migration20260907120000_auth.js +19 -0
- package/dist/modules/auth/migrations/Migration20260907120000_auth.js.map +7 -0
- package/dist/modules/communication_channels/backend/profile/communication-channels/page.meta.js +2 -2
- package/dist/modules/communication_channels/backend/profile/communication-channels/page.meta.js.map +1 -1
- package/dist/modules/customer_accounts/api/admin/demo-accounts.js +118 -0
- package/dist/modules/customer_accounts/api/admin/demo-accounts.js.map +7 -0
- package/dist/modules/customer_accounts/api/invitations/accept.js +23 -6
- package/dist/modules/customer_accounts/api/invitations/accept.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/CustomerAccountsSettingsPageClient.js +10 -13
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/CustomerAccountsSettingsPageClient.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/useDemoPortalAccounts.js +28 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/useDemoPortalAccounts.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js +6 -1
- package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js.map +2 -2
- package/dist/modules/customer_accounts/data/entities.js +4 -1
- package/dist/modules/customer_accounts/data/entities.js.map +2 -2
- package/dist/modules/customer_accounts/lib/exampleAccounts.js +9 -0
- package/dist/modules/customer_accounts/lib/exampleAccounts.js.map +7 -0
- package/dist/modules/customer_accounts/migrations/Migration20260827160000_customer_accounts.js +16 -0
- package/dist/modules/customer_accounts/migrations/Migration20260827160000_customer_accounts.js.map +7 -0
- package/dist/modules/customer_accounts/services/customerInvitationService.js +53 -6
- package/dist/modules/customer_accounts/services/customerInvitationService.js.map +2 -2
- package/dist/modules/customer_accounts/setup.js +2 -6
- package/dist/modules/customer_accounts/setup.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +55 -40
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
- package/dist/modules/customers/commands/people.js +16 -1
- package/dist/modules/customers/commands/people.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +18 -36
- package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +2 -2
- package/dist/modules/entities/api/records.js +18 -15
- package/dist/modules/entities/api/records.js.map +3 -3
- package/dist/modules/entities/lib/scoped-field-defs.js +52 -0
- package/dist/modules/entities/lib/scoped-field-defs.js.map +7 -0
- package/dist/modules/entities/lib/validation.js +4 -35
- package/dist/modules/entities/lib/validation.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/[id]/page.js +32 -54
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +12 -29
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +2 -2
- package/dist/modules/notifications/backend/profile/notification-preferences/page.meta.js +2 -2
- package/dist/modules/notifications/backend/profile/notification-preferences/page.meta.js.map +1 -1
- package/dist/modules/progress/lib/progressServiceImpl.js +14 -3
- package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
- package/dist/modules/sales/backend/sales/documents/[id]/page.js +9 -9
- package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
- package/dist/modules/sales/components/documents/LineItemDialog.js +50 -10
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/PaymentsSection.js +8 -3
- package/dist/modules/sales/components/documents/PaymentsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/ReturnEditDialog.js +2 -7
- package/dist/modules/sales/components/documents/ReturnEditDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/ReturnsSection.js +2 -7
- package/dist/modules/sales/components/documents/ReturnsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/ShipmentsSection.js +3 -8
- package/dist/modules/sales/components/documents/ShipmentsSection.js.map +2 -2
- package/dist/modules/wms/commands/sales-order-assignment.js +12 -0
- package/dist/modules/wms/commands/sales-order-assignment.js.map +2 -2
- package/dist/modules/wms/data/enrichers.js +25 -0
- package/dist/modules/wms/data/enrichers.js.map +2 -2
- package/dist/modules/wms/lib/enricherCacheTags.js +9 -0
- package/dist/modules/wms/lib/enricherCacheTags.js.map +7 -0
- package/dist/modules/wms/lib/invalidateInventoryEnricherCache.js +37 -0
- package/dist/modules/wms/lib/invalidateInventoryEnricherCache.js.map +7 -0
- package/dist/modules/wms/lib/inventoryEnricherCacheSubscriber.js +14 -0
- package/dist/modules/wms/lib/inventoryEnricherCacheSubscriber.js.map +7 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-balance.js +12 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-balance.js.map +7 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-inventory.js +12 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-inventory.js.map +7 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-movement.js +12 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-movement.js.map +7 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-profile.js +12 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-profile.js.map +7 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-reservation.js +12 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-reservation.js.map +7 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-sales-line.js +12 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-sales-line.js.map +7 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-sales-order.js +12 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-sales-order.js.map +7 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-variant.js +12 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-variant.js.map +7 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-warehouse.js +12 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-warehouse.js.map +7 -0
- package/package.json +7 -7
- package/src/modules/auth/commands/users.ts +14 -1
- package/src/modules/auth/data/entities.ts +2 -2
- package/src/modules/auth/lib/backendChrome.tsx +46 -2
- package/src/modules/auth/lib/profile-sections.tsx +14 -3
- package/src/modules/auth/migrations/.snapshot-open-mercato.json +4 -2
- package/src/modules/auth/migrations/Migration20260907120000_auth.ts +21 -0
- package/src/modules/communication_channels/backend/profile/communication-channels/page.meta.ts +2 -2
- package/src/modules/customer_accounts/AGENTS.md +3 -1
- package/src/modules/customer_accounts/api/admin/demo-accounts.ts +149 -0
- package/src/modules/customer_accounts/api/invitations/accept.ts +23 -6
- package/src/modules/customer_accounts/backend/customer_accounts/settings/CustomerAccountsSettingsPageClient.tsx +43 -41
- package/src/modules/customer_accounts/backend/customer_accounts/useDemoPortalAccounts.ts +42 -0
- package/src/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.tsx +10 -3
- package/src/modules/customer_accounts/data/entities.ts +5 -1
- package/src/modules/customer_accounts/i18n/de.json +4 -2
- package/src/modules/customer_accounts/i18n/en.json +4 -2
- package/src/modules/customer_accounts/i18n/es.json +4 -2
- package/src/modules/customer_accounts/i18n/ko.json +4 -2
- package/src/modules/customer_accounts/i18n/pl.json +4 -2
- package/src/modules/customer_accounts/lib/exampleAccounts.ts +18 -0
- package/src/modules/customer_accounts/migrations/.snapshot-open-mercato.json +5 -7
- package/src/modules/customer_accounts/migrations/Migration20260827160000_customer_accounts.ts +23 -0
- package/src/modules/customer_accounts/services/customerInvitationService.ts +89 -5
- package/src/modules/customer_accounts/setup.ts +2 -6
- package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +74 -41
- package/src/modules/customers/commands/people.ts +15 -0
- package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +19 -44
- package/src/modules/entities/api/records.ts +24 -15
- package/src/modules/entities/lib/scoped-field-defs.ts +76 -0
- package/src/modules/entities/lib/validation.ts +5 -45
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +33 -62
- package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +13 -37
- package/src/modules/notifications/backend/profile/notification-preferences/page.meta.ts +2 -2
- package/src/modules/progress/AGENTS.md +1 -1
- package/src/modules/progress/lib/progressServiceImpl.ts +26 -3
- package/src/modules/sales/backend/sales/documents/[id]/page.tsx +19 -25
- package/src/modules/sales/components/documents/LineItemDialog.tsx +53 -9
- package/src/modules/sales/components/documents/PaymentsSection.tsx +8 -8
- package/src/modules/sales/components/documents/ReturnEditDialog.tsx +2 -8
- package/src/modules/sales/components/documents/ReturnsSection.tsx +8 -7
- package/src/modules/sales/components/documents/ShipmentsSection.tsx +9 -8
- package/src/modules/sales/i18n/de.json +2 -0
- package/src/modules/sales/i18n/en.json +2 -0
- package/src/modules/sales/i18n/es.json +2 -0
- package/src/modules/sales/i18n/ko.json +2 -0
- package/src/modules/sales/i18n/pl.json +2 -0
- package/src/modules/wms/commands/sales-order-assignment.ts +20 -0
- package/src/modules/wms/data/enrichers.ts +25 -0
- package/src/modules/wms/lib/enricherCacheTags.ts +29 -0
- package/src/modules/wms/lib/invalidateInventoryEnricherCache.ts +71 -0
- package/src/modules/wms/lib/inventoryEnricherCacheSubscriber.ts +30 -0
- package/src/modules/wms/subscribers/invalidate-enricher-cache-balance.ts +9 -0
- package/src/modules/wms/subscribers/invalidate-enricher-cache-inventory.ts +9 -0
- package/src/modules/wms/subscribers/invalidate-enricher-cache-movement.ts +9 -0
- package/src/modules/wms/subscribers/invalidate-enricher-cache-profile.ts +9 -0
- package/src/modules/wms/subscribers/invalidate-enricher-cache-reservation.ts +9 -0
- package/src/modules/wms/subscribers/invalidate-enricher-cache-sales-line.ts +14 -0
- package/src/modules/wms/subscribers/invalidate-enricher-cache-sales-order.ts +18 -0
- package/src/modules/wms/subscribers/invalidate-enricher-cache-variant.ts +9 -0
- package/src/modules/wms/subscribers/invalidate-enricher-cache-warehouse.ts +9 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { compare as bcryptCompare } from 'bcryptjs'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'
|
|
5
|
+
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
6
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
7
|
+
import { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
8
|
+
import { CustomerUser, CustomerUserRole } from '@open-mercato/core/modules/customer_accounts/data/entities'
|
|
9
|
+
import { EXAMPLE_PORTAL_ACCOUNTS } from '@open-mercato/core/modules/customer_accounts/lib/exampleAccounts'
|
|
10
|
+
import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
11
|
+
import { lookupHashCandidates } from '@open-mercato/shared/lib/encryption/aes'
|
|
12
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
13
|
+
|
|
14
|
+
const FEATURE = 'customer_accounts.view'
|
|
15
|
+
|
|
16
|
+
export const metadata = {
|
|
17
|
+
GET: { requireAuth: true, requireFeatures: [FEATURE] },
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Reports which of the `seedExamples` portal accounts can actually be logged into
|
|
22
|
+
* in the caller's organization. An installation initialized with `--no-examples`,
|
|
23
|
+
* or an organization the examples were never seeded into, gets an empty list so
|
|
24
|
+
* the admin UI can hide the demo-credentials sections entirely (#5669).
|
|
25
|
+
*
|
|
26
|
+
* The surfaces this feeds claim "these credentials work", so mere existence is not
|
|
27
|
+
* enough: the lookup applies the same guards `api/login.ts` enforces (active,
|
|
28
|
+
* email-verified, not soft-deleted) and the advertised password is compared
|
|
29
|
+
* against the stored hash, so an account that was deactivated or whose password
|
|
30
|
+
* was changed since seeding drops out instead of being advertised.
|
|
31
|
+
*/
|
|
32
|
+
export async function GET(req: Request) {
|
|
33
|
+
const auth = await getAuthFromRequest(req)
|
|
34
|
+
if (!auth) {
|
|
35
|
+
return NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const container = await createRequestContainer()
|
|
39
|
+
const rbacService = container.resolve('rbacService') as RbacService
|
|
40
|
+
const hasAccess = await rbacService.userHasAllFeatures(auth.sub, [FEATURE], { tenantId: auth.tenantId, organizationId: auth.orgId })
|
|
41
|
+
if (!hasAccess) {
|
|
42
|
+
return NextResponse.json({ ok: false, error: 'Insufficient permissions' }, { status: 403 })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const em = container.resolve('em') as EntityManager
|
|
46
|
+
|
|
47
|
+
// Emails are stored encrypted, so the seeded addresses are matched through the
|
|
48
|
+
// deterministic lookup hash (both the current and the legacy candidate).
|
|
49
|
+
const accountByHash = new Map<string, (typeof EXAMPLE_PORTAL_ACCOUNTS)[number]>()
|
|
50
|
+
for (const account of EXAMPLE_PORTAL_ACCOUNTS) {
|
|
51
|
+
for (const candidate of lookupHashCandidates(account.email)) {
|
|
52
|
+
accountByHash.set(candidate, account)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const seededUsers = await findWithDecryption(
|
|
57
|
+
em,
|
|
58
|
+
CustomerUser,
|
|
59
|
+
{
|
|
60
|
+
emailHash: { $in: Array.from(accountByHash.keys()) },
|
|
61
|
+
tenantId: auth.tenantId,
|
|
62
|
+
organizationId: auth.orgId,
|
|
63
|
+
isActive: true,
|
|
64
|
+
emailVerifiedAt: { $ne: null },
|
|
65
|
+
deletedAt: null,
|
|
66
|
+
} as any,
|
|
67
|
+
undefined,
|
|
68
|
+
{ tenantId: auth.tenantId, organizationId: auth.orgId },
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const loginableUsers: typeof seededUsers = []
|
|
72
|
+
for (const user of seededUsers) {
|
|
73
|
+
const account = accountByHash.get(user.emailHash as string)
|
|
74
|
+
if (!account || !user.passwordHash) continue
|
|
75
|
+
if (await bcryptCompare(account.password, user.passwordHash)) loginableUsers.push(user)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (loginableUsers.length === 0) {
|
|
79
|
+
return NextResponse.json({ ok: true, items: [] })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const roleLinks = await findWithDecryption(
|
|
83
|
+
em,
|
|
84
|
+
CustomerUserRole,
|
|
85
|
+
{ user: { $in: loginableUsers.map((user) => user.id) } as any, deletedAt: null } as any,
|
|
86
|
+
{ populate: ['role'] },
|
|
87
|
+
{ tenantId: auth.tenantId, organizationId: auth.orgId },
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
const rolesByUserId = new Map<string, Array<{ id: string; name: string; slug: string }>>()
|
|
91
|
+
for (const link of roleLinks) {
|
|
92
|
+
const role = link.role
|
|
93
|
+
if (!role || role.deletedAt) continue
|
|
94
|
+
const entry = { id: role.id, name: role.name, slug: role.slug }
|
|
95
|
+
const bucket = rolesByUserId.get(link.user.id)
|
|
96
|
+
if (bucket) bucket.push(entry)
|
|
97
|
+
else rolesByUserId.set(link.user.id, [entry])
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const seenEmails = new Set<string>()
|
|
101
|
+
const items: Array<{ email: string; password: string; roles: Array<{ id: string; name: string; slug: string }> }> = []
|
|
102
|
+
for (const user of loginableUsers) {
|
|
103
|
+
const account = accountByHash.get(user.emailHash as string)
|
|
104
|
+
if (!account || seenEmails.has(account.email)) continue
|
|
105
|
+
seenEmails.add(account.email)
|
|
106
|
+
items.push({
|
|
107
|
+
email: account.email,
|
|
108
|
+
password: account.password,
|
|
109
|
+
roles: rolesByUserId.get(user.id) ?? [],
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Keep the response ordered like the seed list so the admin table is stable.
|
|
114
|
+
const orderByEmail = new Map(EXAMPLE_PORTAL_ACCOUNTS.map((account, index) => [account.email, index]))
|
|
115
|
+
items.sort((a, b) => (orderByEmail.get(a.email) ?? 0) - (orderByEmail.get(b.email) ?? 0))
|
|
116
|
+
|
|
117
|
+
return NextResponse.json({ ok: true, items })
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const demoAccountSchema = z.object({
|
|
121
|
+
email: z.string().email(),
|
|
122
|
+
password: z.string(),
|
|
123
|
+
roles: z.array(z.object({ id: z.string().uuid(), name: z.string(), slug: z.string() })),
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
const errorSchema = z.object({ ok: z.literal(false), error: z.string() })
|
|
127
|
+
|
|
128
|
+
const getMethodDoc: OpenApiMethodDoc = {
|
|
129
|
+
summary: 'List seeded demo portal accounts (admin)',
|
|
130
|
+
description:
|
|
131
|
+
'Returns the example-data portal accounts that exist in the caller’s organization and can still be logged into with the credentials the seeding hook created them with. Accounts that were deactivated, left unverified, soft-deleted, or had their password changed are omitted, as is every account when example data was not seeded.',
|
|
132
|
+
tags: ['Customer Accounts Admin'],
|
|
133
|
+
responses: [{
|
|
134
|
+
status: 200,
|
|
135
|
+
description: 'Seeded demo accounts present in the current organization',
|
|
136
|
+
schema: z.object({ ok: z.literal(true), items: z.array(demoAccountSchema) }),
|
|
137
|
+
}],
|
|
138
|
+
errors: [
|
|
139
|
+
{ status: 401, description: 'Not authenticated', schema: errorSchema },
|
|
140
|
+
{ status: 403, description: 'Insufficient permissions', schema: errorSchema },
|
|
141
|
+
],
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const openApi: OpenApiRouteDoc = {
|
|
145
|
+
summary: 'Seeded demo portal accounts (admin)',
|
|
146
|
+
methods: {
|
|
147
|
+
GET: getMethodDoc,
|
|
148
|
+
},
|
|
149
|
+
}
|
|
@@ -3,11 +3,15 @@ import { z } from 'zod'
|
|
|
3
3
|
import type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'
|
|
4
4
|
import { invitationAcceptSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'
|
|
5
5
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
CustomerInvitationService,
|
|
8
|
+
isCustomerInvitationEmailConflictError,
|
|
9
|
+
} from '@open-mercato/core/modules/customer_accounts/services/customerInvitationService'
|
|
7
10
|
import { CustomerSessionService } from '@open-mercato/core/modules/customer_accounts/services/customerSessionService'
|
|
8
11
|
import { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'
|
|
9
12
|
import { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'
|
|
10
13
|
import { getClientIp } from '@open-mercato/shared/lib/ratelimit/helpers'
|
|
14
|
+
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
11
15
|
|
|
12
16
|
export const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }
|
|
13
17
|
|
|
@@ -29,11 +33,23 @@ export async function POST(req: Request) {
|
|
|
29
33
|
const customerSessionService = container.resolve('customerSessionService') as CustomerSessionService
|
|
30
34
|
const customerRbacService = container.resolve('customerRbacService') as CustomerRbacService
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
let result: Awaited<ReturnType<typeof customerInvitationService.acceptInvitation>>
|
|
37
|
+
try {
|
|
38
|
+
result = await customerInvitationService.acceptInvitation(
|
|
39
|
+
parsed.data.token,
|
|
40
|
+
parsed.data.password,
|
|
41
|
+
parsed.data.displayName,
|
|
42
|
+
)
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (isCustomerInvitationEmailConflictError(error)) {
|
|
45
|
+
const { translate } = await resolveTranslations()
|
|
46
|
+
return NextResponse.json({
|
|
47
|
+
ok: false,
|
|
48
|
+
error: translate('customer_accounts.errors.emailAlreadyExists', 'An account with this email address already exists'),
|
|
49
|
+
}, { status: 409 })
|
|
50
|
+
}
|
|
51
|
+
throw error
|
|
52
|
+
}
|
|
37
53
|
if (!result) {
|
|
38
54
|
return NextResponse.json({ ok: false, error: 'Invalid or expired invitation' }, { status: 400 })
|
|
39
55
|
}
|
|
@@ -117,6 +133,7 @@ const methodDoc: OpenApiMethodDoc = {
|
|
|
117
133
|
],
|
|
118
134
|
errors: [
|
|
119
135
|
{ status: 400, description: 'Invalid or expired invitation', schema: errorSchema },
|
|
136
|
+
{ status: 409, description: 'An account with this email address already exists', schema: errorSchema },
|
|
120
137
|
],
|
|
121
138
|
}
|
|
122
139
|
|
|
@@ -6,12 +6,7 @@ import { FormHeader } from '@open-mercato/ui/backend/forms'
|
|
|
6
6
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
7
7
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
8
8
|
import { buildPortalRootUrl, buildPortalUrlPattern } from '../../../lib/portalUrl'
|
|
9
|
-
|
|
10
|
-
const DEMO_CREDENTIALS = [
|
|
11
|
-
{ email: 'alice.johnson@example.com', password: 'Password123!', role: 'Portal Admin' },
|
|
12
|
-
{ email: 'bob.smith@example.com', password: 'Password123!', role: 'Buyer' },
|
|
13
|
-
{ email: 'carol.white@example.com', password: 'Password123!', role: 'Viewer' },
|
|
14
|
-
] as const
|
|
9
|
+
import { useDemoPortalAccounts } from '../useDemoPortalAccounts'
|
|
15
10
|
|
|
16
11
|
export type CustomerAccountsSettingsPageClientProps = {
|
|
17
12
|
portalOrigin: string
|
|
@@ -20,6 +15,7 @@ export type CustomerAccountsSettingsPageClientProps = {
|
|
|
20
15
|
|
|
21
16
|
export function CustomerAccountsSettingsPageClient({ portalOrigin, portalOrgSlug = null }: CustomerAccountsSettingsPageClientProps) {
|
|
22
17
|
const t = useT()
|
|
18
|
+
const { accounts: demoAccounts } = useDemoPortalAccounts()
|
|
23
19
|
|
|
24
20
|
const portalUrl = useMemo(() => buildPortalUrlPattern(portalOrigin), [portalOrigin])
|
|
25
21
|
const portalRootUrl = useMemo(
|
|
@@ -73,43 +69,49 @@ export function CustomerAccountsSettingsPageClient({ portalOrigin, portalOrgSlug
|
|
|
73
69
|
</p>
|
|
74
70
|
</div>
|
|
75
71
|
|
|
76
|
-
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
<
|
|
82
|
-
<
|
|
83
|
-
<
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
</thead>
|
|
95
|
-
<tbody>
|
|
96
|
-
{DEMO_CREDENTIALS.map((cred) => (
|
|
97
|
-
<tr key={cred.email} className="border-b last:border-0">
|
|
98
|
-
<td className="py-2 pr-4 font-mono text-xs">{cred.email}</td>
|
|
99
|
-
<td className="py-2 pr-4 font-mono text-xs">{cred.password}</td>
|
|
100
|
-
<td className="py-2 text-xs">{cred.role}</td>
|
|
72
|
+
{demoAccounts.length > 0 ? (
|
|
73
|
+
<div className="rounded-lg border p-4 space-y-3">
|
|
74
|
+
<h3 className="text-sm font-semibold">
|
|
75
|
+
{t('customer_accounts.settings.demo_credentials.title', 'Demo Credentials')}
|
|
76
|
+
</h3>
|
|
77
|
+
<div className="overflow-x-auto">
|
|
78
|
+
<table className="w-full text-sm">
|
|
79
|
+
<thead>
|
|
80
|
+
<tr className="border-b text-left">
|
|
81
|
+
<th className="pb-2 pr-4 font-medium text-muted-foreground">
|
|
82
|
+
{t('customer_accounts.settings.demo_credentials.email', 'Email')}
|
|
83
|
+
</th>
|
|
84
|
+
<th className="pb-2 pr-4 font-medium text-muted-foreground">
|
|
85
|
+
{t('customer_accounts.settings.demo_credentials.password', 'Password')}
|
|
86
|
+
</th>
|
|
87
|
+
<th className="pb-2 font-medium text-muted-foreground">
|
|
88
|
+
{t('customer_accounts.settings.demo_credentials.role', 'Role')}
|
|
89
|
+
</th>
|
|
101
90
|
</tr>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
91
|
+
</thead>
|
|
92
|
+
<tbody>
|
|
93
|
+
{demoAccounts.map((account) => (
|
|
94
|
+
<tr key={account.email} className="border-b last:border-0">
|
|
95
|
+
<td className="py-2 pr-4 font-mono text-xs">{account.email}</td>
|
|
96
|
+
<td className="py-2 pr-4 font-mono text-xs">{account.password}</td>
|
|
97
|
+
<td className="py-2 text-xs">
|
|
98
|
+
{account.roles.length > 0
|
|
99
|
+
? account.roles.map((role) => role.name).join(', ')
|
|
100
|
+
: t('customer_accounts.settings.demo_credentials.no_role', 'No role assigned')}
|
|
101
|
+
</td>
|
|
102
|
+
</tr>
|
|
103
|
+
))}
|
|
104
|
+
</tbody>
|
|
105
|
+
</table>
|
|
106
|
+
</div>
|
|
107
|
+
<p className="text-xs text-muted-foreground">
|
|
108
|
+
{t(
|
|
109
|
+
'customer_accounts.settings.demo_credentials.note',
|
|
110
|
+
'These accounts were created automatically for this organization by the example-data seeding step during setup.',
|
|
111
|
+
)}
|
|
112
|
+
</p>
|
|
105
113
|
</div>
|
|
106
|
-
|
|
107
|
-
{t(
|
|
108
|
-
'customer_accounts.settings.demo_credentials.note',
|
|
109
|
-
'These credentials are only available if example data was seeded during setup.',
|
|
110
|
-
)}
|
|
111
|
-
</p>
|
|
112
|
-
</div>
|
|
114
|
+
) : null}
|
|
113
115
|
|
|
114
116
|
<div className="rounded-lg border p-4 space-y-3">
|
|
115
117
|
<h3 className="text-sm font-semibold">
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
5
|
+
|
|
6
|
+
export type DemoPortalAccount = {
|
|
7
|
+
email: string
|
|
8
|
+
password: string
|
|
9
|
+
roles: Array<{ id: string; name: string; slug: string }>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type DemoAccountsResponse = {
|
|
13
|
+
items?: DemoPortalAccount[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Loads the example-data portal accounts that exist in the current organization.
|
|
18
|
+
* Callers render demo credentials only for what comes back, so an installation
|
|
19
|
+
* started with `--no-examples` shows nothing at all (#5669). A failed lookup
|
|
20
|
+
* resolves to an empty list — never to a hardcoded fallback.
|
|
21
|
+
*/
|
|
22
|
+
export function useDemoPortalAccounts(): { accounts: DemoPortalAccount[] } {
|
|
23
|
+
const [accounts, setAccounts] = React.useState<DemoPortalAccount[]>([])
|
|
24
|
+
|
|
25
|
+
React.useEffect(() => {
|
|
26
|
+
let cancelled = false
|
|
27
|
+
async function load() {
|
|
28
|
+
try {
|
|
29
|
+
const call = await apiCall<DemoAccountsResponse>('/api/customer_accounts/admin/demo-accounts')
|
|
30
|
+
if (cancelled) return
|
|
31
|
+
const items = call.ok && Array.isArray(call.result?.items) ? call.result!.items : []
|
|
32
|
+
setAccounts(items)
|
|
33
|
+
} catch {
|
|
34
|
+
if (!cancelled) setAccounts([])
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
load()
|
|
38
|
+
return () => { cancelled = true }
|
|
39
|
+
}, [])
|
|
40
|
+
|
|
41
|
+
return { accounts }
|
|
42
|
+
}
|
package/src/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.tsx
CHANGED
|
@@ -22,6 +22,7 @@ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuarde
|
|
|
22
22
|
import { ListEmptyState } from '@open-mercato/ui/backend/filters/ListEmptyState'
|
|
23
23
|
import type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'
|
|
24
24
|
import { buildPortalRootUrl, buildPortalUrlPattern } from '../../../lib/portalUrl'
|
|
25
|
+
import { useDemoPortalAccounts } from '../useDemoPortalAccounts'
|
|
25
26
|
|
|
26
27
|
type UserRow = {
|
|
27
28
|
id: string
|
|
@@ -248,6 +249,7 @@ export function PortalUsersPageClient({ portalOrigin, portalOrgSlug = null }: Po
|
|
|
248
249
|
const [reloadToken, setReloadToken] = React.useState(0)
|
|
249
250
|
const [roleOptions, setRoleOptions] = React.useState<Array<{ value: string; label: string; id: string }>>([])
|
|
250
251
|
const [createDialogOpen, setCreateDialogOpen] = React.useState(false)
|
|
252
|
+
const { accounts: demoAccounts } = useDemoPortalAccounts()
|
|
251
253
|
|
|
252
254
|
const { runMutation, retryLastMutation } = useGuardedMutation<{
|
|
253
255
|
entityType: string
|
|
@@ -506,9 +508,14 @@ export function PortalUsersPageClient({ portalOrigin, portalOrgSlug = null }: Po
|
|
|
506
508
|
url: buildPortalUrlPattern(portalOrigin),
|
|
507
509
|
})}
|
|
508
510
|
</p>
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
511
|
+
{demoAccounts.length > 0 ? (
|
|
512
|
+
<p className="mt-0.5 text-xs text-status-info-text">
|
|
513
|
+
{t('customer_accounts.admin.portalInfo.credentials', 'Seeded demo credentials: {email} / {password}', {
|
|
514
|
+
email: demoAccounts[0].email,
|
|
515
|
+
password: demoAccounts[0].password,
|
|
516
|
+
})}
|
|
517
|
+
</p>
|
|
518
|
+
) : null}
|
|
512
519
|
</div>
|
|
513
520
|
<div className="flex shrink-0 flex-col gap-2">
|
|
514
521
|
<Button
|
|
@@ -13,7 +13,11 @@ export const DOMAIN_STATUSES: readonly DomainStatus[] = [
|
|
|
13
13
|
] as const
|
|
14
14
|
|
|
15
15
|
@Entity({ tableName: 'customer_users' })
|
|
16
|
-
@
|
|
16
|
+
@Index({
|
|
17
|
+
name: 'customer_users_tenant_email_hash_uniq',
|
|
18
|
+
expression:
|
|
19
|
+
'create unique index "customer_users_tenant_email_hash_uniq" on "customer_users" ("tenant_id", "email_hash") where "deleted_at" is null',
|
|
20
|
+
})
|
|
17
21
|
export class CustomerUser {
|
|
18
22
|
@PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
|
|
19
23
|
id!: string
|
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
"customer_accounts.admin.portalFeatures.users.manage.description": "Ermöglicht das Bearbeiten von Rollen und das Entfernen von Teammitgliedern",
|
|
134
134
|
"customer_accounts.admin.portalFeatures.users.view": "Teammitglieder anzeigen",
|
|
135
135
|
"customer_accounts.admin.portalFeatures.users.view.description": "Ermöglicht das Anzeigen anderer Teammitglieder in der Organisation",
|
|
136
|
-
"customer_accounts.admin.portalInfo.credentials": "Demo
|
|
136
|
+
"customer_accounts.admin.portalInfo.credentials": "Geseedete Demo-Zugangsdaten: {email} / {password}",
|
|
137
137
|
"customer_accounts.admin.portalInfo.description": "Manage customer portal accounts. Customers can self-register, log in, and access orders, quotes, and invoices through the portal.",
|
|
138
138
|
"customer_accounts.admin.portalInfo.open": "Open Portal",
|
|
139
139
|
"customer_accounts.admin.portalInfo.openConfiguration": "Open Configuration",
|
|
@@ -274,6 +274,7 @@
|
|
|
274
274
|
"customer_accounts.domainMapping.tls.diagnostics.title": "SSL certificate issue",
|
|
275
275
|
"customer_accounts.domainMapping.tls.retryButton": "Retry SSL",
|
|
276
276
|
"customer_accounts.domainMapping.verifyNow": "Check Now",
|
|
277
|
+
"customer_accounts.errors.emailAlreadyExists": "Es existiert bereits ein Konto mit dieser E-Mail-Adresse",
|
|
277
278
|
"customer_accounts.invitation.email.body": "Use this secure invitation link to create your password and access the customer portal.",
|
|
278
279
|
"customer_accounts.invitation.email.cta": "Accept invitation",
|
|
279
280
|
"customer_accounts.invitation.email.hint": "This invitation link expires in 72 hours. If you did not expect this invitation, you can ignore this email.",
|
|
@@ -307,7 +308,8 @@
|
|
|
307
308
|
"customer_accounts.search.badge.customerUser": "Kundenbenutzer",
|
|
308
309
|
"customer_accounts.settings.back": "Kundenbenutzer",
|
|
309
310
|
"customer_accounts.settings.demo_credentials.email": "Email",
|
|
310
|
-
"customer_accounts.settings.demo_credentials.
|
|
311
|
+
"customer_accounts.settings.demo_credentials.no_role": "Keine Rolle zugewiesen",
|
|
312
|
+
"customer_accounts.settings.demo_credentials.note": "Diese Konten wurden während der Einrichtung automatisch für diese Organisation durch den Schritt zum Seeden der Beispieldaten erstellt.",
|
|
311
313
|
"customer_accounts.settings.demo_credentials.password": "Password",
|
|
312
314
|
"customer_accounts.settings.demo_credentials.role": "Role",
|
|
313
315
|
"customer_accounts.settings.demo_credentials.title": "Demo Credentials",
|
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
"customer_accounts.admin.portalFeatures.users.manage.description": "Allows editing roles and removing team members",
|
|
134
134
|
"customer_accounts.admin.portalFeatures.users.view": "View team members",
|
|
135
135
|
"customer_accounts.admin.portalFeatures.users.view.description": "Allows viewing other team members in the organization",
|
|
136
|
-
"customer_accounts.admin.portalInfo.credentials": "
|
|
136
|
+
"customer_accounts.admin.portalInfo.credentials": "Seeded demo credentials: {email} / {password}",
|
|
137
137
|
"customer_accounts.admin.portalInfo.description": "Manage customer portal accounts. Customers can self-register, log in, and access orders, quotes, and invoices through the portal.",
|
|
138
138
|
"customer_accounts.admin.portalInfo.open": "Open Portal",
|
|
139
139
|
"customer_accounts.admin.portalInfo.openConfiguration": "Open Configuration",
|
|
@@ -274,6 +274,7 @@
|
|
|
274
274
|
"customer_accounts.domainMapping.tls.diagnostics.title": "SSL certificate issue",
|
|
275
275
|
"customer_accounts.domainMapping.tls.retryButton": "Retry SSL",
|
|
276
276
|
"customer_accounts.domainMapping.verifyNow": "Check Now",
|
|
277
|
+
"customer_accounts.errors.emailAlreadyExists": "An account with this email address already exists",
|
|
277
278
|
"customer_accounts.invitation.email.body": "Use this secure invitation link to create your password and access the customer portal.",
|
|
278
279
|
"customer_accounts.invitation.email.cta": "Accept invitation",
|
|
279
280
|
"customer_accounts.invitation.email.hint": "This invitation link expires in 72 hours. If you did not expect this invitation, you can ignore this email.",
|
|
@@ -307,7 +308,8 @@
|
|
|
307
308
|
"customer_accounts.search.badge.customerUser": "Customer user",
|
|
308
309
|
"customer_accounts.settings.back": "Customer Users",
|
|
309
310
|
"customer_accounts.settings.demo_credentials.email": "Email",
|
|
310
|
-
"customer_accounts.settings.demo_credentials.
|
|
311
|
+
"customer_accounts.settings.demo_credentials.no_role": "No role assigned",
|
|
312
|
+
"customer_accounts.settings.demo_credentials.note": "These accounts were created automatically for this organization by the example-data seeding step during setup.",
|
|
311
313
|
"customer_accounts.settings.demo_credentials.password": "Password",
|
|
312
314
|
"customer_accounts.settings.demo_credentials.role": "Role",
|
|
313
315
|
"customer_accounts.settings.demo_credentials.title": "Demo Credentials",
|
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
"customer_accounts.admin.portalFeatures.users.manage.description": "Permite editar roles y eliminar miembros del equipo",
|
|
134
134
|
"customer_accounts.admin.portalFeatures.users.view": "Ver miembros del equipo",
|
|
135
135
|
"customer_accounts.admin.portalFeatures.users.view.description": "Permite ver a otros miembros del equipo en la organización",
|
|
136
|
-
"customer_accounts.admin.portalInfo.credentials": "
|
|
136
|
+
"customer_accounts.admin.portalInfo.credentials": "Credenciales de demostración sembradas: {email} / {password}",
|
|
137
137
|
"customer_accounts.admin.portalInfo.description": "Manage customer portal accounts. Customers can self-register, log in, and access orders, quotes, and invoices through the portal.",
|
|
138
138
|
"customer_accounts.admin.portalInfo.open": "Open Portal",
|
|
139
139
|
"customer_accounts.admin.portalInfo.openConfiguration": "Open Configuration",
|
|
@@ -274,6 +274,7 @@
|
|
|
274
274
|
"customer_accounts.domainMapping.tls.diagnostics.title": "SSL certificate issue",
|
|
275
275
|
"customer_accounts.domainMapping.tls.retryButton": "Retry SSL",
|
|
276
276
|
"customer_accounts.domainMapping.verifyNow": "Check Now",
|
|
277
|
+
"customer_accounts.errors.emailAlreadyExists": "Ya existe una cuenta con esta dirección de correo electrónico",
|
|
277
278
|
"customer_accounts.invitation.email.body": "Use this secure invitation link to create your password and access the customer portal.",
|
|
278
279
|
"customer_accounts.invitation.email.cta": "Accept invitation",
|
|
279
280
|
"customer_accounts.invitation.email.hint": "This invitation link expires in 72 hours. If you did not expect this invitation, you can ignore this email.",
|
|
@@ -307,7 +308,8 @@
|
|
|
307
308
|
"customer_accounts.search.badge.customerUser": "Usuario de cliente",
|
|
308
309
|
"customer_accounts.settings.back": "Usuarios de clientes",
|
|
309
310
|
"customer_accounts.settings.demo_credentials.email": "Email",
|
|
310
|
-
"customer_accounts.settings.demo_credentials.
|
|
311
|
+
"customer_accounts.settings.demo_credentials.no_role": "Sin rol asignado",
|
|
312
|
+
"customer_accounts.settings.demo_credentials.note": "Estas cuentas se crearon automáticamente para esta organización mediante el paso de carga de datos de ejemplo durante la configuración.",
|
|
311
313
|
"customer_accounts.settings.demo_credentials.password": "Password",
|
|
312
314
|
"customer_accounts.settings.demo_credentials.role": "Role",
|
|
313
315
|
"customer_accounts.settings.demo_credentials.title": "Demo Credentials",
|
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
"customer_accounts.admin.portalFeatures.users.manage.description": "역할 편집 및 팀 구성원 제거 허용",
|
|
134
134
|
"customer_accounts.admin.portalFeatures.users.view": "팀원 보기",
|
|
135
135
|
"customer_accounts.admin.portalFeatures.users.view.description": "조직의 다른 팀 구성원을 볼 수 있습니다.",
|
|
136
|
-
"customer_accounts.admin.portalInfo.credentials": "데모 자격 증명:
|
|
136
|
+
"customer_accounts.admin.portalInfo.credentials": "시드된 데모 자격 증명: {email} / {password}",
|
|
137
137
|
"customer_accounts.admin.portalInfo.description": "고객 포털 계정을 관리합니다. 고객은 셀프 등록, 로그인을 할 수 있으며 포털을 통해 주문, 견적, 인보이스에 접근할 수 있습니다.",
|
|
138
138
|
"customer_accounts.admin.portalInfo.open": "포털 열기",
|
|
139
139
|
"customer_accounts.admin.portalInfo.openConfiguration": "구성 열기",
|
|
@@ -274,6 +274,7 @@
|
|
|
274
274
|
"customer_accounts.domainMapping.tls.diagnostics.title": "SSL 인증서 문제",
|
|
275
275
|
"customer_accounts.domainMapping.tls.retryButton": "SSL 재시도",
|
|
276
276
|
"customer_accounts.domainMapping.verifyNow": "지금 확인",
|
|
277
|
+
"customer_accounts.errors.emailAlreadyExists": "이 이메일 주소로 등록된 계정이 이미 있습니다",
|
|
277
278
|
"customer_accounts.invitation.email.body": "이 보안 초대 링크를 사용하여 비밀번호를 생성하고 고객 포털에 액세스하세요.",
|
|
278
279
|
"customer_accounts.invitation.email.cta": "초대 수락",
|
|
279
280
|
"customer_accounts.invitation.email.hint": "이 초대 링크는 72시간 후에 만료됩니다. 이 초대를 예상하지 못했다면 이 이메일을 무시해도 됩니다.",
|
|
@@ -307,7 +308,8 @@
|
|
|
307
308
|
"customer_accounts.search.badge.customerUser": "고객 사용자",
|
|
308
309
|
"customer_accounts.settings.back": "고객 사용자",
|
|
309
310
|
"customer_accounts.settings.demo_credentials.email": "이메일",
|
|
310
|
-
"customer_accounts.settings.demo_credentials.
|
|
311
|
+
"customer_accounts.settings.demo_credentials.no_role": "할당된 역할 없음",
|
|
312
|
+
"customer_accounts.settings.demo_credentials.note": "이 계정은 설정 중 예제 데이터 시딩 단계에서 이 조직을 위해 자동으로 생성되었습니다.",
|
|
311
313
|
"customer_accounts.settings.demo_credentials.password": "비밀번호",
|
|
312
314
|
"customer_accounts.settings.demo_credentials.role": "역할",
|
|
313
315
|
"customer_accounts.settings.demo_credentials.title": "데모 자격 증명",
|
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
"customer_accounts.admin.portalFeatures.users.manage.description": "Umożliwia edycję ról i usuwanie członków zespołu",
|
|
134
134
|
"customer_accounts.admin.portalFeatures.users.view": "Wyświetlanie członków zespołu",
|
|
135
135
|
"customer_accounts.admin.portalFeatures.users.view.description": "Umożliwia przeglądanie innych członków zespołu w organizacji",
|
|
136
|
-
"customer_accounts.admin.portalInfo.credentials": "
|
|
136
|
+
"customer_accounts.admin.portalInfo.credentials": "Zaseedowane dane demonstracyjne: {email} / {password}",
|
|
137
137
|
"customer_accounts.admin.portalInfo.description": "Zarządzaj kontami portalu klienta. Klienci mogą sami się rejestrować, logować oraz przeglądać zamówienia, oferty i faktury w portalu.",
|
|
138
138
|
"customer_accounts.admin.portalInfo.open": "Otwórz portal",
|
|
139
139
|
"customer_accounts.admin.portalInfo.openConfiguration": "Otwórz konfigurację",
|
|
@@ -274,6 +274,7 @@
|
|
|
274
274
|
"customer_accounts.domainMapping.tls.diagnostics.title": "Problem z certyfikatem SSL",
|
|
275
275
|
"customer_accounts.domainMapping.tls.retryButton": "Ponów SSL",
|
|
276
276
|
"customer_accounts.domainMapping.verifyNow": "Sprawdź teraz",
|
|
277
|
+
"customer_accounts.errors.emailAlreadyExists": "Konto z tym adresem e-mail już istnieje",
|
|
277
278
|
"customer_accounts.invitation.email.body": "Użyj bezpiecznego linku zaproszenia, aby utworzyć hasło i uzyskać dostęp do portalu klienta.",
|
|
278
279
|
"customer_accounts.invitation.email.cta": "Przyjmij zaproszenie",
|
|
279
280
|
"customer_accounts.invitation.email.hint": "Ten link zaproszenia wygasa po 72 godzinach. Jeśli nie spodziewasz się tego zaproszenia, możesz zignorować tę wiadomość.",
|
|
@@ -307,7 +308,8 @@
|
|
|
307
308
|
"customer_accounts.search.badge.customerUser": "Użytkownik klienta",
|
|
308
309
|
"customer_accounts.settings.back": "Użytkownicy klientów",
|
|
309
310
|
"customer_accounts.settings.demo_credentials.email": "E-mail",
|
|
310
|
-
"customer_accounts.settings.demo_credentials.
|
|
311
|
+
"customer_accounts.settings.demo_credentials.no_role": "Brak przypisanej roli",
|
|
312
|
+
"customer_accounts.settings.demo_credentials.note": "Te konta zostały utworzone automatycznie dla tej organizacji przez krok seedowania danych przykładowych podczas konfiguracji.",
|
|
311
313
|
"customer_accounts.settings.demo_credentials.password": "Hasło",
|
|
312
314
|
"customer_accounts.settings.demo_credentials.role": "Rola",
|
|
313
315
|
"customer_accounts.settings.demo_credentials.title": "Dane demonstracyjne",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type ExamplePortalAccount = {
|
|
2
|
+
email: string
|
|
3
|
+
displayName: string
|
|
4
|
+
password: string
|
|
5
|
+
roleSlug: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Portal accounts created by the module's `seedExamples` hook. Setup seeds from
|
|
10
|
+
* this list and the admin demo-credentials endpoint reports against it, so the
|
|
11
|
+
* advertised credentials can never drift from the seeded ones (#3198) and are
|
|
12
|
+
* only surfaced for accounts that actually exist in the current scope (#5669).
|
|
13
|
+
*/
|
|
14
|
+
export const EXAMPLE_PORTAL_ACCOUNTS: readonly ExamplePortalAccount[] = [
|
|
15
|
+
{ email: 'alice.johnson@example.com', displayName: 'Alice Johnson', password: 'Password123!', roleSlug: 'portal_admin' },
|
|
16
|
+
{ email: 'bob.smith@example.com', displayName: 'Bob Smith', password: 'Password123!', roleSlug: 'buyer' },
|
|
17
|
+
{ email: 'carol.white@example.com', displayName: 'Carol White', password: 'Password123!', roleSlug: 'viewer' },
|
|
18
|
+
] as const
|
|
@@ -1821,15 +1821,13 @@
|
|
|
1821
1821
|
"unique": true
|
|
1822
1822
|
},
|
|
1823
1823
|
{
|
|
1824
|
-
"columnNames": [
|
|
1825
|
-
"tenant_id",
|
|
1826
|
-
"email_hash"
|
|
1827
|
-
],
|
|
1828
|
-
"composite": true,
|
|
1829
|
-
"constraint": true,
|
|
1830
1824
|
"keyName": "customer_users_tenant_email_hash_uniq",
|
|
1825
|
+
"columnNames": [],
|
|
1826
|
+
"composite": false,
|
|
1827
|
+
"constraint": false,
|
|
1831
1828
|
"primary": false,
|
|
1832
|
-
"unique":
|
|
1829
|
+
"unique": false,
|
|
1830
|
+
"expression": "create unique index \"customer_users_tenant_email_hash_uniq\" on \"customer_users\" (\"tenant_id\", \"email_hash\") where \"deleted_at\" is null"
|
|
1833
1831
|
}
|
|
1834
1832
|
],
|
|
1835
1833
|
"checks": [],
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
|
|
3
|
+
export class Migration20260827160000_customer_accounts extends Migration {
|
|
4
|
+
|
|
5
|
+
override async up(): Promise<void> {
|
|
6
|
+
this.addSql(`alter table "customer_users" drop constraint "customer_users_tenant_email_hash_uniq";`);
|
|
7
|
+
this.addSql(`create unique index "customer_users_tenant_email_hash_uniq" on "customer_users" ("tenant_id", "email_hash") where "deleted_at" is null;`);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
override async down(): Promise<void> {
|
|
11
|
+
this.addSql(`drop index if exists "customer_users_tenant_email_hash_uniq";`);
|
|
12
|
+
// Once the partial index has been live, a soft-deleted row legitimately shares
|
|
13
|
+
// (tenant_id, email_hash) with a newer row — which is the point of this change —
|
|
14
|
+
// so the full constraint cannot be restored while both exist. Purge the losing
|
|
15
|
+
// soft-deleted duplicates first: this is destructive, but it only removes rows
|
|
16
|
+
// that were already logically deleted, and without it `down()` fails on exactly
|
|
17
|
+
// the data the migration enables. The surviving row is the active one, else the
|
|
18
|
+
// most recently created.
|
|
19
|
+
this.addSql(`delete from "customer_users" losing using "customer_users" winner where losing."deleted_at" is not null and losing."tenant_id" = winner."tenant_id" and losing."email_hash" = winner."email_hash" and losing."id" <> winner."id" and (winner."deleted_at" is null or winner."created_at" > losing."created_at" or (winner."created_at" = losing."created_at" and winner."id" > losing."id"));`);
|
|
20
|
+
this.addSql(`alter table "customer_users" add constraint "customer_users_tenant_email_hash_uniq" unique ("tenant_id", "email_hash");`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
}
|