@open-mercato/core 0.6.6-develop.5483.1.a1129165ea → 0.6.6-develop.5503.1.6cdc4dda5f
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/dist/modules/auth/api/login.js +4 -13
- package/dist/modules/auth/api/login.js.map +2 -2
- package/dist/modules/auth/services/authService.js +4 -2
- package/dist/modules/auth/services/authService.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +18 -0
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/components/detail/PersonDetailTabs.js +11 -3
- package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +7 -5
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/api/login.ts +13 -13
- package/src/modules/auth/i18n/de.json +0 -1
- package/src/modules/auth/i18n/en.json +0 -1
- package/src/modules/auth/i18n/es.json +0 -1
- package/src/modules/auth/i18n/pl.json +0 -1
- package/src/modules/auth/services/authService.ts +14 -3
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +18 -0
- package/src/modules/customers/components/detail/PersonDetailTabs.tsx +12 -2
- package/src/modules/sales/commands/documents.ts +7 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.6-develop.
|
|
3
|
+
"version": "0.6.6-develop.5503.1.6cdc4dda5f",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -245,16 +245,16 @@
|
|
|
245
245
|
"zod": "^4.4.3"
|
|
246
246
|
},
|
|
247
247
|
"peerDependencies": {
|
|
248
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
249
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
250
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
248
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.5503.1.6cdc4dda5f",
|
|
249
|
+
"@open-mercato/shared": "0.6.6-develop.5503.1.6cdc4dda5f",
|
|
250
|
+
"@open-mercato/ui": "0.6.6-develop.5503.1.6cdc4dda5f",
|
|
251
251
|
"react": "^19.0.0",
|
|
252
252
|
"react-dom": "^19.0.0"
|
|
253
253
|
},
|
|
254
254
|
"devDependencies": {
|
|
255
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
256
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
257
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
255
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.5503.1.6cdc4dda5f",
|
|
256
|
+
"@open-mercato/shared": "0.6.6-develop.5503.1.6cdc4dda5f",
|
|
257
|
+
"@open-mercato/ui": "0.6.6-develop.5503.1.6cdc4dda5f",
|
|
258
258
|
"@testing-library/dom": "^10.4.1",
|
|
259
259
|
"@testing-library/jest-dom": "^6.9.1",
|
|
260
260
|
"@testing-library/react": "^16.3.1",
|
|
@@ -108,21 +108,21 @@ export async function POST(req: Request) {
|
|
|
108
108
|
user = await auth.findUserByEmailAndTenant(parsed.data.email, tenantId)
|
|
109
109
|
} else {
|
|
110
110
|
const users = await auth.findUsersByEmail(parsed.data.email)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
user = users[0] ?? null
|
|
118
|
-
}
|
|
119
|
-
if (!user || !user.passwordHash) {
|
|
120
|
-
void emitAuthEvent('auth.login.failed', { email: parsed.data.email, reason: 'invalid_credentials' }).catch(() => undefined)
|
|
121
|
-
return NextResponse.json({ ok: false, error: translate('auth.login.errors.invalidCredentials', 'Invalid email or password') }, { status: 401 })
|
|
111
|
+
// Never disclose that an email is registered across multiple tenants — a
|
|
112
|
+
// password-independent 400-vs-401 response is an account/topology oracle
|
|
113
|
+
// (issue #2242). Treat an ambiguous match as no resolvable user and fall
|
|
114
|
+
// through to the uniform invalid-credentials path; tenant-selection
|
|
115
|
+
// guidance is delivered out-of-band via the activation/login link.
|
|
116
|
+
user = users.length === 1 ? users[0] : null
|
|
122
117
|
}
|
|
118
|
+
// Always verify the password — verifyPassword runs a constant-time bcrypt
|
|
119
|
+
// comparison even when the user is missing or has no hash — so unknown-email,
|
|
120
|
+
// wrong-password, and multi-tenant cases return an identical 401 with
|
|
121
|
+
// identical latency.
|
|
123
122
|
const ok = await auth.verifyPassword(user, parsed.data.password)
|
|
124
|
-
if (!ok) {
|
|
125
|
-
|
|
123
|
+
if (!user || !ok) {
|
|
124
|
+
const reason = user?.passwordHash ? 'invalid_password' : 'invalid_credentials'
|
|
125
|
+
void emitAuthEvent('auth.login.failed', { email: parsed.data.email, reason }).catch(() => undefined)
|
|
126
126
|
return NextResponse.json({ ok: false, error: translate('auth.login.errors.invalidCredentials', 'Invalid email or password') }, { status: 401 })
|
|
127
127
|
}
|
|
128
128
|
// Optional role requirement
|
|
@@ -43,7 +43,6 @@
|
|
|
43
43
|
"auth.login.errors.invalidCredentials": "Ungültige E-Mail oder ungültiges Passwort",
|
|
44
44
|
"auth.login.errors.permissionDenied": "Du hast keine Berechtigung, auf diesen Bereich zuzugreifen. Bitte wende dich an deine Administration.",
|
|
45
45
|
"auth.login.errors.tenantInvalid": "Tenant nicht gefunden. Entferne die Tenant-Auswahl und versuche es erneut.",
|
|
46
|
-
"auth.login.errors.tenantRequired": "Nutze den Login-Link aus deiner Tenant-Aktivierung, um fortzufahren.",
|
|
47
46
|
"auth.login.featureDenied": "Du hast keinen Zugriff auf diese Funktion ({feature}). Bitte wende dich an deine Administration.",
|
|
48
47
|
"auth.login.forgotPassword": "Passwort vergessen?",
|
|
49
48
|
"auth.login.loading": "Wird geladen ...",
|
|
@@ -43,7 +43,6 @@
|
|
|
43
43
|
"auth.login.errors.invalidCredentials": "Invalid email or password",
|
|
44
44
|
"auth.login.errors.permissionDenied": "You do not have permission to access this area. Please contact your administrator.",
|
|
45
45
|
"auth.login.errors.tenantInvalid": "Tenant not found. Clear the tenant selection and try again.",
|
|
46
|
-
"auth.login.errors.tenantRequired": "Use the login link provided with your tenant activation to continue.",
|
|
47
46
|
"auth.login.featureDenied": "You don't have access to this feature ({feature}). Please contact your administrator.",
|
|
48
47
|
"auth.login.forgotPassword": "Forgot password?",
|
|
49
48
|
"auth.login.loading": "Loading...",
|
|
@@ -43,7 +43,6 @@
|
|
|
43
43
|
"auth.login.errors.invalidCredentials": "Correo electrónico o contraseña no válidos",
|
|
44
44
|
"auth.login.errors.permissionDenied": "No tienes permiso para acceder a esta área. Ponte en contacto con tu administrador.",
|
|
45
45
|
"auth.login.errors.tenantInvalid": "No se encontró el inquilino. Borra la selección e inténtalo de nuevo.",
|
|
46
|
-
"auth.login.errors.tenantRequired": "Usa el enlace de inicio de sesión proporcionado con la activación de tu inquilino para continuar.",
|
|
47
46
|
"auth.login.featureDenied": "No tienes acceso a esta funcionalidad ({feature}). Ponte en contacto con tu administrador.",
|
|
48
47
|
"auth.login.forgotPassword": "¿Olvidaste tu contraseña?",
|
|
49
48
|
"auth.login.loading": "Cargando...",
|
|
@@ -43,7 +43,6 @@
|
|
|
43
43
|
"auth.login.errors.invalidCredentials": "Nieprawidłowy email lub hasło",
|
|
44
44
|
"auth.login.errors.permissionDenied": "Nie masz uprawnień do tego obszaru. Skontaktuj się z administratorem.",
|
|
45
45
|
"auth.login.errors.tenantInvalid": "Nie znaleziono najemcy. Wyczyść wybór i spróbuj ponownie.",
|
|
46
|
-
"auth.login.errors.tenantRequired": "Użyj linku logowania z aktywacji najemcy, aby kontynuować.",
|
|
47
46
|
"auth.login.featureDenied": "Nie masz dostępu do tej funkcji ({feature}). Skontaktuj się z administratorem.",
|
|
48
47
|
"auth.login.forgotPassword": "Nie pamiętasz hasła?",
|
|
49
48
|
"auth.login.loading": "Ładowanie...",
|
|
@@ -5,6 +5,13 @@ import { emailHashLookupValues } from '@open-mercato/core/modules/auth/lib/email
|
|
|
5
5
|
import { generateAuthToken, hashAuthToken } from '@open-mercato/core/modules/auth/lib/tokenHash'
|
|
6
6
|
import { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
7
7
|
|
|
8
|
+
// A fixed, valid bcrypt hash (cost 10) of a throwaway value no real password
|
|
9
|
+
// can match. verifyPassword compares against it whenever the user is missing or
|
|
10
|
+
// has no password hash, so a failed login spends the same bcrypt CPU time
|
|
11
|
+
// regardless of whether the account exists — closing the timing side channel
|
|
12
|
+
// for account enumeration (issue #2242).
|
|
13
|
+
const TIMING_EQUALIZER_PASSWORD_HASH = '$2b$10$OcZrhmZpIzJOjkfwUrk7d.Nl0eHNzOvalBcBlt5Ran.4lj8R3HZg6'
|
|
14
|
+
|
|
8
15
|
export class AuthService {
|
|
9
16
|
constructor(private em: EntityManager) {}
|
|
10
17
|
|
|
@@ -48,9 +55,13 @@ export class AuthService {
|
|
|
48
55
|
)
|
|
49
56
|
}
|
|
50
57
|
|
|
51
|
-
async verifyPassword(user: User, password: string) {
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
async verifyPassword(user: User | null, password: string) {
|
|
59
|
+
const storedHash = user?.passwordHash ?? null
|
|
60
|
+
// Always run a bcrypt comparison — against a fixed dummy hash when the user
|
|
61
|
+
// is absent or has no password — so login latency does not reveal whether
|
|
62
|
+
// the account exists (timing-based enumeration, issue #2242).
|
|
63
|
+
const matched = await compare(password, storedHash ?? TIMING_EQUALIZER_PASSWORD_HASH)
|
|
64
|
+
return storedHash !== null && matched
|
|
54
65
|
}
|
|
55
66
|
|
|
56
67
|
async updateLastLoginAt(user: User) {
|
|
@@ -37,6 +37,7 @@ import { ScheduleActivityDialog, type ScheduleActivityEditData } from '../../../
|
|
|
37
37
|
import { PersonDetailHeader } from '../../../../components/detail/PersonDetailHeader'
|
|
38
38
|
import { ChangelogTab } from '../../../../components/detail/ChangelogTab'
|
|
39
39
|
import { PersonDetailTabs, resolveLegacyTab, type PersonTabId } from '../../../../components/detail/PersonDetailTabs'
|
|
40
|
+
import { AddressesSection } from '../../../../components/detail/AddressesSection'
|
|
40
41
|
import { PersonCompaniesSection } from '../../../../components/detail/PersonCompaniesSection'
|
|
41
42
|
import { MobilePersonDetail } from '../../../../components/detail/MobilePersonDetail'
|
|
42
43
|
import type { TagsSectionController } from '@open-mercato/ui/backend/detail'
|
|
@@ -541,6 +542,7 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
|
|
|
541
542
|
activitiesCount={interactionCount}
|
|
542
543
|
dealsCount={dealCount}
|
|
543
544
|
companiesCount={companyCount}
|
|
545
|
+
addressesCount={data?.counts?.addresses ?? 0}
|
|
544
546
|
tasksCount={todoCount}
|
|
545
547
|
sectionAction={sectionAction}
|
|
546
548
|
>
|
|
@@ -619,6 +621,22 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
|
|
|
619
621
|
)
|
|
620
622
|
}
|
|
621
623
|
|
|
624
|
+
if (activeTab === 'addresses') {
|
|
625
|
+
return (
|
|
626
|
+
<AddressesSection
|
|
627
|
+
entityId={personId}
|
|
628
|
+
emptyLabel={t('customers.people.detail.empty.addresses', 'No addresses linked to this person.')}
|
|
629
|
+
addActionLabel={t('customers.people.detail.addresses.add', 'Add address')}
|
|
630
|
+
emptyState={{
|
|
631
|
+
title: t('customers.people.detail.emptyState.addresses.title', 'No addresses yet'),
|
|
632
|
+
actionLabel: t('customers.people.detail.emptyState.addresses.action', 'Add address'),
|
|
633
|
+
}}
|
|
634
|
+
onActionChange={handleSectionActionChange}
|
|
635
|
+
translator={detailTranslator}
|
|
636
|
+
/>
|
|
637
|
+
)
|
|
638
|
+
}
|
|
639
|
+
|
|
622
640
|
if (activeTab === 'tasks') {
|
|
623
641
|
return (
|
|
624
642
|
<TasksSection
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
History,
|
|
14
14
|
Paperclip,
|
|
15
15
|
Plus,
|
|
16
|
+
MapPin,
|
|
16
17
|
} from 'lucide-react'
|
|
17
18
|
import type { SectionAction } from '@open-mercato/ui/backend/detail'
|
|
18
19
|
|
|
@@ -21,6 +22,7 @@ export type PersonTabId =
|
|
|
21
22
|
| 'emails'
|
|
22
23
|
| 'deals'
|
|
23
24
|
| 'companies'
|
|
25
|
+
| 'addresses'
|
|
24
26
|
| 'tasks'
|
|
25
27
|
| 'changelog'
|
|
26
28
|
| 'files'
|
|
@@ -40,13 +42,14 @@ type PersonDetailTabsProps = {
|
|
|
40
42
|
activitiesCount?: number
|
|
41
43
|
dealsCount?: number
|
|
42
44
|
companiesCount?: number
|
|
45
|
+
addressesCount?: number
|
|
43
46
|
tasksCount?: number
|
|
44
47
|
filesCount?: number
|
|
45
48
|
sectionAction?: SectionAction | null
|
|
46
49
|
children: React.ReactNode
|
|
47
50
|
}
|
|
48
51
|
|
|
49
|
-
const SUPPORTED_TAB_IDS = new Set<PersonTabId>(['activities', 'emails', 'deals', 'companies', 'tasks', 'changelog', 'files'])
|
|
52
|
+
const SUPPORTED_TAB_IDS = new Set<PersonTabId>(['activities', 'emails', 'deals', 'companies', 'addresses', 'tasks', 'changelog', 'files'])
|
|
50
53
|
|
|
51
54
|
export function resolveLegacyTab(tab: string | null | undefined): PersonTabId {
|
|
52
55
|
if (!tab) return 'activities'
|
|
@@ -77,6 +80,7 @@ export function PersonDetailTabs({
|
|
|
77
80
|
activitiesCount = 0,
|
|
78
81
|
dealsCount = 0,
|
|
79
82
|
companiesCount = 0,
|
|
83
|
+
addressesCount = 0,
|
|
80
84
|
tasksCount = 0,
|
|
81
85
|
filesCount = 0,
|
|
82
86
|
sectionAction = null,
|
|
@@ -109,6 +113,12 @@ export function PersonDetailTabs({
|
|
|
109
113
|
icon: <Building2 className="size-4" />,
|
|
110
114
|
badge: <CountBadge count={companiesCount} />,
|
|
111
115
|
},
|
|
116
|
+
{
|
|
117
|
+
id: 'addresses',
|
|
118
|
+
label: t('customers.people.detail.tabs.addresses', 'Addresses'),
|
|
119
|
+
icon: <MapPin className="size-4" />,
|
|
120
|
+
badge: <CountBadge count={addressesCount} />,
|
|
121
|
+
},
|
|
112
122
|
{
|
|
113
123
|
id: 'tasks',
|
|
114
124
|
label: t('customers.people.detail.tabs.tasks', 'Tasks'),
|
|
@@ -128,7 +138,7 @@ export function PersonDetailTabs({
|
|
|
128
138
|
badge: <CountBadge count={filesCount} />,
|
|
129
139
|
},
|
|
130
140
|
],
|
|
131
|
-
[t, activitiesCount, dealsCount, companiesCount, tasksCount, filesCount],
|
|
141
|
+
[t, activitiesCount, dealsCount, companiesCount, addressesCount, tasksCount, filesCount],
|
|
132
142
|
)
|
|
133
143
|
|
|
134
144
|
const allTabs: TabDef[] = React.useMemo(
|
|
@@ -719,11 +719,13 @@ async function resolveAddressSnapshot(
|
|
|
719
719
|
addressId?: string | null,
|
|
720
720
|
): Promise<Record<string, unknown> | null> {
|
|
721
721
|
if (!addressId) return null;
|
|
722
|
-
const address = await
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
tenantId,
|
|
726
|
-
|
|
722
|
+
const address = await findOneWithDecryption(
|
|
723
|
+
em,
|
|
724
|
+
CustomerAddress,
|
|
725
|
+
{ id: addressId, organizationId, tenantId },
|
|
726
|
+
undefined,
|
|
727
|
+
{ tenantId, organizationId },
|
|
728
|
+
);
|
|
727
729
|
if (!address) return null;
|
|
728
730
|
|
|
729
731
|
return {
|