@open-mercato/core 0.4.2-canary-19353c5970 → 0.4.2-canary-ad4e7882e9
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 +25 -6
- package/dist/modules/auth/api/login.js.map +2 -2
- package/dist/modules/auth/data/validators.js +2 -1
- package/dist/modules/auth/data/validators.js.map +2 -2
- package/dist/modules/auth/frontend/login.js +85 -1
- package/dist/modules/auth/frontend/login.js.map +2 -2
- package/dist/modules/auth/lib/setup-app.js +25 -12
- package/dist/modules/auth/lib/setup-app.js.map +2 -2
- package/dist/modules/auth/services/authService.js +21 -0
- package/dist/modules/auth/services/authService.js.map +2 -2
- package/dist/modules/business_rules/cli.js +2 -1
- package/dist/modules/business_rules/cli.js.map +2 -2
- package/dist/modules/directory/api/get/tenants/lookup.js +70 -0
- package/dist/modules/directory/api/get/tenants/lookup.js.map +7 -0
- package/dist/modules/notifications/migrations/Migration20260129082610.js +13 -0
- package/dist/modules/notifications/migrations/Migration20260129082610.js.map +7 -0
- package/dist/modules/query_index/cli.js +63 -7
- package/dist/modules/query_index/cli.js.map +2 -2
- package/dist/modules/workflows/cli.js +12 -12
- package/dist/modules/workflows/cli.js.map +2 -2
- package/package.json +2 -2
- package/src/modules/auth/api/__tests__/login.test.ts +2 -0
- package/src/modules/auth/api/login.ts +26 -7
- package/src/modules/auth/data/validators.ts +1 -0
- package/src/modules/auth/frontend/login.tsx +106 -2
- package/src/modules/auth/i18n/de.json +5 -0
- package/src/modules/auth/i18n/en.json +5 -0
- package/src/modules/auth/i18n/es.json +5 -0
- package/src/modules/auth/i18n/pl.json +5 -0
- package/src/modules/auth/lib/setup-app.ts +37 -15
- package/src/modules/auth/services/authService.ts +23 -0
- package/src/modules/business_rules/cli.ts +2 -1
- package/src/modules/directory/api/get/tenants/lookup.ts +75 -0
- package/src/modules/notifications/migrations/.snapshot-open-mercato.json +36 -0
- package/src/modules/notifications/migrations/Migration20260129082610.ts +13 -0
- package/src/modules/query_index/cli.ts +82 -13
- package/src/modules/workflows/cli.ts +12 -12
|
@@ -1,14 +1,39 @@
|
|
|
1
1
|
"use client"
|
|
2
|
-
import { useState } from 'react'
|
|
2
|
+
import { useEffect, useState } from 'react'
|
|
3
3
|
import Image from 'next/image'
|
|
4
4
|
import Link from 'next/link'
|
|
5
5
|
import { useRouter, useSearchParams } from 'next/navigation'
|
|
6
|
-
import { Card, CardContent, CardHeader,
|
|
6
|
+
import { Card, CardContent, CardHeader, CardDescription } from '@open-mercato/ui/primitives/card'
|
|
7
7
|
import { Input } from '@open-mercato/ui/primitives/input'
|
|
8
8
|
import { Label } from '@open-mercato/ui/primitives/label'
|
|
9
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
9
10
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
10
11
|
import { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'
|
|
11
12
|
import { clearAllOperations } from '@open-mercato/ui/backend/operations/store'
|
|
13
|
+
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
14
|
+
|
|
15
|
+
const loginTenantKey = 'om_login_tenant'
|
|
16
|
+
const loginTenantCookieMaxAge = 60 * 60 * 24 * 14
|
|
17
|
+
|
|
18
|
+
function readTenantCookie() {
|
|
19
|
+
if (typeof document === 'undefined') return null
|
|
20
|
+
const entries = document.cookie.split(';')
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
const [name, ...rest] = entry.trim().split('=')
|
|
23
|
+
if (name === loginTenantKey) return decodeURIComponent(rest.join('='))
|
|
24
|
+
}
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function setTenantCookie(value: string) {
|
|
29
|
+
if (typeof document === 'undefined') return
|
|
30
|
+
document.cookie = `${loginTenantKey}=${encodeURIComponent(value)}; path=/; max-age=${loginTenantCookieMaxAge}; samesite=lax`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function clearTenantCookie() {
|
|
34
|
+
if (typeof document === 'undefined') return
|
|
35
|
+
document.cookie = `${loginTenantKey}=; path=/; max-age=0; samesite=lax`
|
|
36
|
+
}
|
|
12
37
|
|
|
13
38
|
function extractErrorMessage(payload: unknown): string | null {
|
|
14
39
|
if (!payload) return null
|
|
@@ -56,6 +81,68 @@ export default function LoginPage() {
|
|
|
56
81
|
const translatedFeatures = requiredFeatures.map((feature) => translate(`features.${feature}`, feature))
|
|
57
82
|
const [error, setError] = useState<string | null>(null)
|
|
58
83
|
const [submitting, setSubmitting] = useState(false)
|
|
84
|
+
const [tenantId, setTenantId] = useState<string | null>(null)
|
|
85
|
+
const [tenantName, setTenantName] = useState<string | null>(null)
|
|
86
|
+
const [tenantLoading, setTenantLoading] = useState(false)
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
const tenantParam = (searchParams.get('tenant') || '').trim()
|
|
90
|
+
if (tenantParam) {
|
|
91
|
+
setTenantId(tenantParam)
|
|
92
|
+
window.localStorage.setItem(loginTenantKey, tenantParam)
|
|
93
|
+
setTenantCookie(tenantParam)
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
const storedTenant = window.localStorage.getItem(loginTenantKey) || readTenantCookie()
|
|
97
|
+
if (storedTenant) {
|
|
98
|
+
setTenantId(storedTenant)
|
|
99
|
+
}
|
|
100
|
+
}, [searchParams])
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (!tenantId) {
|
|
104
|
+
setTenantName(null)
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
let active = true
|
|
108
|
+
setTenantLoading(true)
|
|
109
|
+
apiCall<{ ok: boolean; tenant?: { id: string; name: string }; error?: string }>(
|
|
110
|
+
`/api/directory/tenants/lookup?tenantId=${encodeURIComponent(tenantId)}`,
|
|
111
|
+
)
|
|
112
|
+
.then(({ result }) => {
|
|
113
|
+
if (!active) return
|
|
114
|
+
if (result?.ok && result.tenant) {
|
|
115
|
+
setTenantName(result.tenant.name)
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
const message = translate('auth.login.errors.tenantInvalid', 'Tenant not found. Clear the tenant selection and try again.')
|
|
119
|
+
setTenantName(null)
|
|
120
|
+
setError(message)
|
|
121
|
+
})
|
|
122
|
+
.catch(() => {
|
|
123
|
+
if (!active) return
|
|
124
|
+
setTenantName(null)
|
|
125
|
+
setError(translate('auth.login.errors.tenantInvalid', 'Tenant not found. Clear the tenant selection and try again.'))
|
|
126
|
+
})
|
|
127
|
+
.finally(() => {
|
|
128
|
+
if (active) setTenantLoading(false)
|
|
129
|
+
})
|
|
130
|
+
return () => {
|
|
131
|
+
active = false
|
|
132
|
+
}
|
|
133
|
+
}, [tenantId, translate])
|
|
134
|
+
|
|
135
|
+
function handleClearTenant() {
|
|
136
|
+
window.localStorage.removeItem(loginTenantKey)
|
|
137
|
+
clearTenantCookie()
|
|
138
|
+
setTenantId(null)
|
|
139
|
+
setTenantName(null)
|
|
140
|
+
const params = new URLSearchParams(searchParams)
|
|
141
|
+
params.delete('tenant')
|
|
142
|
+
setError(null)
|
|
143
|
+
const query = params.toString()
|
|
144
|
+
router.replace(query ? `/login?${query}` : '/login')
|
|
145
|
+
}
|
|
59
146
|
|
|
60
147
|
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
|
|
61
148
|
e.preventDefault()
|
|
@@ -141,6 +228,9 @@ export default function LoginPage() {
|
|
|
141
228
|
</CardHeader>
|
|
142
229
|
<CardContent>
|
|
143
230
|
<form className="grid gap-3" onSubmit={onSubmit} noValidate>
|
|
231
|
+
{tenantId ? (
|
|
232
|
+
<input type="hidden" name="tenantId" value={tenantId} />
|
|
233
|
+
) : null}
|
|
144
234
|
{!!translatedRoles.length && (
|
|
145
235
|
<div className="rounded-md border border-blue-200 bg-blue-50 px-3 py-2 text-center text-xs text-blue-900">
|
|
146
236
|
{translate(
|
|
@@ -159,6 +249,20 @@ export default function LoginPage() {
|
|
|
159
249
|
})}
|
|
160
250
|
</div>
|
|
161
251
|
)}
|
|
252
|
+
{tenantId && (
|
|
253
|
+
<div className="rounded-md border border-emerald-200 bg-emerald-50 px-3 py-2 text-center text-xs text-emerald-900">
|
|
254
|
+
<div className="font-medium">
|
|
255
|
+
{tenantLoading
|
|
256
|
+
? translate('auth.login.tenantLoading', 'Loading tenant details...')
|
|
257
|
+
: translate('auth.login.tenantBanner', "You're logging in to {tenant} tenant.", {
|
|
258
|
+
tenant: tenantName || tenantId,
|
|
259
|
+
})}
|
|
260
|
+
</div>
|
|
261
|
+
<Button type="button" variant="ghost" size="sm" className="mt-2 text-emerald-900" onClick={handleClearTenant}>
|
|
262
|
+
{translate('auth.login.tenantClear', 'Clear')}
|
|
263
|
+
</Button>
|
|
264
|
+
</div>
|
|
265
|
+
)}
|
|
162
266
|
{error && (
|
|
163
267
|
<div className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-center text-sm text-red-700" role="alert" aria-live="polite">
|
|
164
268
|
{error}
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
"auth.manageAuthSettings": "Verwalte die Authentifizierungseinstellungen.",
|
|
21
21
|
"auth.login.errors.permissionDenied": "Du hast keine Berechtigung, auf diesen Bereich zuzugreifen. Bitte wende dich an deine Administration.",
|
|
22
22
|
"auth.login.errors.invalidCredentials": "Ungültige E-Mail oder ungültiges Passwort",
|
|
23
|
+
"auth.login.errors.tenantRequired": "Nutze den Login-Link aus deiner Tenant-Aktivierung, um fortzufahren.",
|
|
24
|
+
"auth.login.errors.tenantInvalid": "Tenant nicht gefunden. Entferne die Tenant-Auswahl und versuche es erneut.",
|
|
23
25
|
"auth.login.errors.generic": "Es ist ein Fehler aufgetreten. Bitte versuche es erneut.",
|
|
24
26
|
"auth.login.logoAlt": "Open Mercato Logo",
|
|
25
27
|
"auth.login.brandName": "Open Mercato",
|
|
@@ -27,6 +29,9 @@
|
|
|
27
29
|
"auth.login.requireRoleMessage": "Für den Zugriff ist die folgende Rolle erforderlich: {roles}",
|
|
28
30
|
"auth.login.requireRolesMessage": "Für den Zugriff ist eine der folgenden Rollen erforderlich: {roles}",
|
|
29
31
|
"auth.login.featureDenied": "Du hast keinen Zugriff auf diese Funktion ({feature}). Bitte wende dich an deine Administration.",
|
|
32
|
+
"auth.login.tenantBanner": "Du meldest dich beim Tenant {tenant} an.",
|
|
33
|
+
"auth.login.tenantLoading": "Tenant-Daten werden geladen...",
|
|
34
|
+
"auth.login.tenantClear": "Zurücksetzen",
|
|
30
35
|
"auth.login.rememberMe": "Angemeldet bleiben",
|
|
31
36
|
"auth.login.loading": "Wird geladen ...",
|
|
32
37
|
"auth.login.forgotPassword": "Passwort vergessen?",
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
"auth.manageAuthSettings": "Manage authentication settings.",
|
|
21
21
|
"auth.login.errors.permissionDenied": "You do not have permission to access this area. Please contact your administrator.",
|
|
22
22
|
"auth.login.errors.invalidCredentials": "Invalid email or password",
|
|
23
|
+
"auth.login.errors.tenantRequired": "Use the login link provided with your tenant activation to continue.",
|
|
24
|
+
"auth.login.errors.tenantInvalid": "Tenant not found. Clear the tenant selection and try again.",
|
|
23
25
|
"auth.login.errors.generic": "An error occurred. Please try again.",
|
|
24
26
|
"auth.login.logoAlt": "Open Mercato logo",
|
|
25
27
|
"auth.login.brandName": "Open Mercato",
|
|
@@ -27,6 +29,9 @@
|
|
|
27
29
|
"auth.login.requireRoleMessage": "Access requires role: {roles}",
|
|
28
30
|
"auth.login.requireRolesMessage": "Access requires one of the following roles: {roles}",
|
|
29
31
|
"auth.login.featureDenied": "You don't have access to this feature ({feature}). Please contact your administrator.",
|
|
32
|
+
"auth.login.tenantBanner": "You're logging in to {tenant} tenant.",
|
|
33
|
+
"auth.login.tenantLoading": "Loading tenant details...",
|
|
34
|
+
"auth.login.tenantClear": "Clear",
|
|
30
35
|
"auth.login.rememberMe": "Remember me",
|
|
31
36
|
"auth.login.loading": "Loading...",
|
|
32
37
|
"auth.login.forgotPassword": "Forgot password?",
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
"auth.manageAuthSettings": "Administra la configuración de autenticación.",
|
|
21
21
|
"auth.login.errors.permissionDenied": "No tienes permiso para acceder a esta área. Ponte en contacto con tu administrador.",
|
|
22
22
|
"auth.login.errors.invalidCredentials": "Correo electrónico o contraseña no válidos",
|
|
23
|
+
"auth.login.errors.tenantRequired": "Usa el enlace de inicio de sesión proporcionado con la activación de tu inquilino para continuar.",
|
|
24
|
+
"auth.login.errors.tenantInvalid": "No se encontró el inquilino. Borra la selección e inténtalo de nuevo.",
|
|
23
25
|
"auth.login.errors.generic": "Se produjo un error. Inténtalo de nuevo.",
|
|
24
26
|
"auth.login.logoAlt": "Logotipo de Open Mercato",
|
|
25
27
|
"auth.login.brandName": "Open Mercato",
|
|
@@ -27,6 +29,9 @@
|
|
|
27
29
|
"auth.login.requireRoleMessage": "El acceso requiere el rol: {roles}",
|
|
28
30
|
"auth.login.requireRolesMessage": "El acceso requiere uno de los siguientes roles: {roles}",
|
|
29
31
|
"auth.login.featureDenied": "No tienes acceso a esta funcionalidad ({feature}). Ponte en contacto con tu administrador.",
|
|
32
|
+
"auth.login.tenantBanner": "Estás iniciando sesión en el inquilino {tenant}.",
|
|
33
|
+
"auth.login.tenantLoading": "Cargando detalles del inquilino...",
|
|
34
|
+
"auth.login.tenantClear": "Borrar",
|
|
30
35
|
"auth.login.rememberMe": "Recordarme",
|
|
31
36
|
"auth.login.loading": "Cargando...",
|
|
32
37
|
"auth.login.forgotPassword": "¿Olvidaste tu contraseña?",
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
"auth.manageAuthSettings": "Zarządzaj ustawieniami uwierzytelniania.",
|
|
21
21
|
"auth.login.errors.permissionDenied": "Nie masz uprawnień do tego obszaru. Skontaktuj się z administratorem.",
|
|
22
22
|
"auth.login.errors.invalidCredentials": "Nieprawidłowy email lub hasło",
|
|
23
|
+
"auth.login.errors.tenantRequired": "Użyj linku logowania z aktywacji najemcy, aby kontynuować.",
|
|
24
|
+
"auth.login.errors.tenantInvalid": "Nie znaleziono najemcy. Wyczyść wybór i spróbuj ponownie.",
|
|
23
25
|
"auth.login.errors.generic": "Wystąpił błąd. Spróbuj ponownie.",
|
|
24
26
|
"auth.login.logoAlt": "Logo Open Mercato",
|
|
25
27
|
"auth.login.brandName": "Open Mercato",
|
|
@@ -27,6 +29,9 @@
|
|
|
27
29
|
"auth.login.requireRoleMessage": "Dostęp wymaga roli: {roles}",
|
|
28
30
|
"auth.login.requireRolesMessage": "Dostęp wymaga jednej z następujących ról: {roles}",
|
|
29
31
|
"auth.login.featureDenied": "Nie masz dostępu do tej funkcji ({feature}). Skontaktuj się z administratorem.",
|
|
32
|
+
"auth.login.tenantBanner": "Logujesz się do najemcy {tenant}.",
|
|
33
|
+
"auth.login.tenantLoading": "Ładowanie szczegółów najemcy...",
|
|
34
|
+
"auth.login.tenantClear": "Wyczyść",
|
|
30
35
|
"auth.login.rememberMe": "Zapamiętaj mnie",
|
|
31
36
|
"auth.login.loading": "Ładowanie...",
|
|
32
37
|
"auth.login.forgotPassword": "Nie pamiętasz hasła?",
|
|
@@ -16,6 +16,7 @@ import { DEFAULT_ENCRYPTION_MAPS } from '@open-mercato/core/modules/entities/lib
|
|
|
16
16
|
import { createKmsService } from '@open-mercato/shared/lib/encryption/kms'
|
|
17
17
|
import { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'
|
|
18
18
|
import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
19
|
+
import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
|
|
19
20
|
|
|
20
21
|
const DEFAULT_ROLE_NAMES = ['employee', 'admin', 'superadmin'] as const
|
|
21
22
|
const DEMO_SUPERADMIN_EMAIL = 'superadmin@acme.com'
|
|
@@ -175,20 +176,28 @@ export async function setupInitialTenant(
|
|
|
175
176
|
})
|
|
176
177
|
|
|
177
178
|
if (!existingUser) {
|
|
178
|
-
const baseUsers: Array<{
|
|
179
|
+
const baseUsers: Array<{
|
|
180
|
+
email: string
|
|
181
|
+
roles: string[]
|
|
182
|
+
name?: string | null
|
|
183
|
+
passwordHash?: string | null
|
|
184
|
+
}> = [
|
|
179
185
|
{ email: primaryUser.email, roles: primaryRoles, name: resolvePrimaryName(primaryUser) },
|
|
180
186
|
]
|
|
181
187
|
if (includeDerivedUsers) {
|
|
182
|
-
const [
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
188
|
+
const [, domain] = String(primaryUser.email).split('@')
|
|
189
|
+
const adminOverride = readEnvValue(DERIVED_EMAIL_ENV.admin)
|
|
190
|
+
const employeeOverride = readEnvValue(DERIVED_EMAIL_ENV.employee)
|
|
191
|
+
const adminEmail = adminOverride ?? (domain ? `admin@${domain}` : '')
|
|
192
|
+
const employeeEmail = employeeOverride ?? (domain ? `employee@${domain}` : '')
|
|
193
|
+
const adminPassword = readEnvValue('OM_INIT_ADMIN_PASSWORD')
|
|
194
|
+
const employeePassword = readEnvValue('OM_INIT_EMPLOYEE_PASSWORD')
|
|
195
|
+
const adminPasswordHash = adminPassword ? await resolvePasswordHash({ email: adminEmail, password: adminPassword }) : null
|
|
196
|
+
const employeePasswordHash = employeePassword
|
|
197
|
+
? await resolvePasswordHash({ email: employeeEmail, password: employeePassword })
|
|
198
|
+
: null
|
|
199
|
+
addUniqueBaseUser(baseUsers, { email: adminEmail, roles: ['admin'], passwordHash: adminPasswordHash })
|
|
200
|
+
addUniqueBaseUser(baseUsers, { email: employeeEmail, roles: ['employee'], passwordHash: employeePasswordHash })
|
|
192
201
|
}
|
|
193
202
|
const passwordHash = await resolvePasswordHash(primaryUser)
|
|
194
203
|
|
|
@@ -280,13 +289,14 @@ export async function setupInitialTenant(
|
|
|
280
289
|
}
|
|
281
290
|
|
|
282
291
|
for (const base of baseUsers) {
|
|
292
|
+
const resolvedPasswordHash = base.passwordHash ?? passwordHash
|
|
283
293
|
let user = await tem.findOne(User, { email: base.email })
|
|
284
294
|
const confirm = primaryUser.confirm ?? true
|
|
285
295
|
const encryptedPayload = encryptionService
|
|
286
296
|
? await encryptionService.encryptEntityPayload('auth:user', { email: base.email }, tenantId, organizationId)
|
|
287
297
|
: { email: base.email, emailHash: computeEmailHash(base.email) }
|
|
288
298
|
if (user) {
|
|
289
|
-
user.passwordHash =
|
|
299
|
+
user.passwordHash = resolvedPasswordHash
|
|
290
300
|
user.organizationId = organizationId
|
|
291
301
|
user.tenantId = tenantId
|
|
292
302
|
if (isTenantDataEncryptionEnabled()) {
|
|
@@ -301,7 +311,7 @@ export async function setupInitialTenant(
|
|
|
301
311
|
user = tem.create(User, {
|
|
302
312
|
email: (encryptedPayload as any).email ?? base.email,
|
|
303
313
|
emailHash: isTenantDataEncryptionEnabled() ? (encryptedPayload as any).emailHash ?? computeEmailHash(base.email) : undefined,
|
|
304
|
-
passwordHash,
|
|
314
|
+
passwordHash: resolvedPasswordHash,
|
|
305
315
|
organizationId,
|
|
306
316
|
tenantId,
|
|
307
317
|
name: base.name ?? undefined,
|
|
@@ -357,8 +367,8 @@ function readEnvValue(key: string): string | undefined {
|
|
|
357
367
|
}
|
|
358
368
|
|
|
359
369
|
function addUniqueBaseUser(
|
|
360
|
-
baseUsers: Array<{ email: string; roles: string[]; name?: string | null }>,
|
|
361
|
-
entry: { email: string; roles: string[]; name?: string | null },
|
|
370
|
+
baseUsers: Array<{ email: string; roles: string[]; name?: string | null; passwordHash?: string | null }>,
|
|
371
|
+
entry: { email: string; roles: string[]; name?: string | null; passwordHash?: string | null },
|
|
362
372
|
) {
|
|
363
373
|
if (!entry.email) return
|
|
364
374
|
const normalized = entry.email.toLowerCase()
|
|
@@ -366,6 +376,17 @@ function addUniqueBaseUser(
|
|
|
366
376
|
baseUsers.push(entry)
|
|
367
377
|
}
|
|
368
378
|
|
|
379
|
+
function isDemoModeEnabled(): boolean {
|
|
380
|
+
const parsed = parseBooleanToken(process.env.DEMO_MODE ?? '')
|
|
381
|
+
return parsed === false ? false : true
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function shouldKeepDemoSuperadminDuringInit(): boolean {
|
|
385
|
+
if (process.env.OM_INIT_FLOW !== 'true') return false
|
|
386
|
+
if (!readEnvValue('OM_INIT_SUPERADMIN_EMAIL')) return false
|
|
387
|
+
return isDemoModeEnabled()
|
|
388
|
+
}
|
|
389
|
+
|
|
369
390
|
async function resolvePasswordHash(input: PrimaryUserInput): Promise<string | null> {
|
|
370
391
|
if (typeof input.hashedPassword === 'string') return input.hashedPassword
|
|
371
392
|
if (input.password) return hash(input.password, 10)
|
|
@@ -514,6 +535,7 @@ async function ensureRoleAclFor(
|
|
|
514
535
|
|
|
515
536
|
async function deactivateDemoSuperAdminIfSelfOnboardingEnabled(em: EntityManager) {
|
|
516
537
|
if (process.env.SELF_SERVICE_ONBOARDING_ENABLED !== 'true') return
|
|
538
|
+
if (shouldKeepDemoSuperadminDuringInit()) return
|
|
517
539
|
try {
|
|
518
540
|
const user = await em.findOne(User, { email: DEMO_SUPERADMIN_EMAIL })
|
|
519
541
|
if (!user) return
|
|
@@ -18,6 +18,29 @@ export class AuthService {
|
|
|
18
18
|
} as any)
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
async findUsersByEmail(email: string) {
|
|
22
|
+
const emailHash = computeEmailHash(email)
|
|
23
|
+
return this.em.find(User, {
|
|
24
|
+
deletedAt: null,
|
|
25
|
+
$or: [
|
|
26
|
+
{ email },
|
|
27
|
+
{ emailHash },
|
|
28
|
+
],
|
|
29
|
+
} as any)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async findUserByEmailAndTenant(email: string, tenantId: string) {
|
|
33
|
+
const emailHash = computeEmailHash(email)
|
|
34
|
+
return this.em.findOne(User, {
|
|
35
|
+
tenantId,
|
|
36
|
+
deletedAt: null,
|
|
37
|
+
$or: [
|
|
38
|
+
{ email },
|
|
39
|
+
{ emailHash },
|
|
40
|
+
],
|
|
41
|
+
} as any)
|
|
42
|
+
}
|
|
43
|
+
|
|
21
44
|
async verifyPassword(user: User, password: string) {
|
|
22
45
|
if (!user.passwordHash) return false
|
|
23
46
|
return compare(password, user.passwordHash)
|
|
@@ -44,6 +44,7 @@ const seedGuardRules: ModuleCli = {
|
|
|
44
44
|
const rulesPath = path.join(__dirname, '../workflows/examples', 'guard-rules-example.json')
|
|
45
45
|
const rulesData = JSON.parse(fs.readFileSync(rulesPath, 'utf8'))
|
|
46
46
|
|
|
47
|
+
console.log('🧠 Seeding guard rules...')
|
|
47
48
|
let seededCount = 0
|
|
48
49
|
let skippedCount = 0
|
|
49
50
|
|
|
@@ -73,7 +74,7 @@ const seedGuardRules: ModuleCli = {
|
|
|
73
74
|
seededCount++
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
console.log(`\n
|
|
77
|
+
console.log(`\n✅ Guard rules seeding complete:`)
|
|
77
78
|
console.log(` - Seeded: ${seededCount}`)
|
|
78
79
|
console.log(` - Skipped (existing): ${skippedCount}`)
|
|
79
80
|
console.log(` - Total: ${rulesData.length}`)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
4
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
5
|
+
import { Tenant } from '@open-mercato/core/modules/directory/data/entities'
|
|
6
|
+
import type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
7
|
+
|
|
8
|
+
export const metadata = {
|
|
9
|
+
GET: {
|
|
10
|
+
requireAuth: false,
|
|
11
|
+
},
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const tenantLookupQuerySchema = z.object({
|
|
15
|
+
tenantId: z.string().uuid(),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
export async function GET(req: Request) {
|
|
19
|
+
const url = new URL(req.url)
|
|
20
|
+
const tenantId = url.searchParams.get('tenantId') || url.searchParams.get('tenant') || ''
|
|
21
|
+
const parsed = tenantLookupQuerySchema.safeParse({ tenantId })
|
|
22
|
+
if (!parsed.success) {
|
|
23
|
+
return NextResponse.json({ ok: false, error: 'Invalid tenant id.' }, { status: 400 })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const container = await createRequestContainer()
|
|
27
|
+
const em = (container.resolve('em') as EntityManager)
|
|
28
|
+
const tenant = await em.findOne(Tenant, { id: parsed.data.tenantId, deletedAt: null })
|
|
29
|
+
if (!tenant) {
|
|
30
|
+
return NextResponse.json({ ok: false, error: 'Tenant not found.' }, { status: 404 })
|
|
31
|
+
}
|
|
32
|
+
return NextResponse.json({
|
|
33
|
+
ok: true,
|
|
34
|
+
tenant: { id: String(tenant.id), name: tenant.name },
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const lookupTag = 'Directory'
|
|
39
|
+
|
|
40
|
+
const tenantLookupSuccessSchema = z.object({
|
|
41
|
+
ok: z.literal(true),
|
|
42
|
+
tenant: z.object({
|
|
43
|
+
id: z.string().uuid(),
|
|
44
|
+
name: z.string(),
|
|
45
|
+
}),
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const tenantLookupErrorSchema = z.object({
|
|
49
|
+
ok: z.literal(false),
|
|
50
|
+
error: z.string(),
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const tenantLookupDoc: OpenApiMethodDoc = {
|
|
54
|
+
summary: 'Public tenant lookup',
|
|
55
|
+
description: 'Resolves tenant metadata for login/activation flows.',
|
|
56
|
+
tags: [lookupTag],
|
|
57
|
+
query: tenantLookupQuerySchema,
|
|
58
|
+
responses: [
|
|
59
|
+
{ status: 200, description: 'Tenant resolved.', schema: tenantLookupSuccessSchema },
|
|
60
|
+
],
|
|
61
|
+
errors: [
|
|
62
|
+
{ status: 400, description: 'Invalid tenant id', schema: tenantLookupErrorSchema },
|
|
63
|
+
{ status: 404, description: 'Tenant not found', schema: tenantLookupErrorSchema },
|
|
64
|
+
],
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const openApi: OpenApiRouteDoc = {
|
|
68
|
+
tag: lookupTag,
|
|
69
|
+
summary: 'Public tenant lookup',
|
|
70
|
+
methods: {
|
|
71
|
+
GET: tenantLookupDoc,
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export default GET
|
|
@@ -34,6 +34,42 @@
|
|
|
34
34
|
"nullable": false,
|
|
35
35
|
"mappedType": "text"
|
|
36
36
|
},
|
|
37
|
+
"title_key": {
|
|
38
|
+
"name": "title_key",
|
|
39
|
+
"type": "text",
|
|
40
|
+
"unsigned": false,
|
|
41
|
+
"autoincrement": false,
|
|
42
|
+
"primary": false,
|
|
43
|
+
"nullable": true,
|
|
44
|
+
"mappedType": "text"
|
|
45
|
+
},
|
|
46
|
+
"body_key": {
|
|
47
|
+
"name": "body_key",
|
|
48
|
+
"type": "text",
|
|
49
|
+
"unsigned": false,
|
|
50
|
+
"autoincrement": false,
|
|
51
|
+
"primary": false,
|
|
52
|
+
"nullable": true,
|
|
53
|
+
"mappedType": "text"
|
|
54
|
+
},
|
|
55
|
+
"title_variables": {
|
|
56
|
+
"name": "title_variables",
|
|
57
|
+
"type": "jsonb",
|
|
58
|
+
"unsigned": false,
|
|
59
|
+
"autoincrement": false,
|
|
60
|
+
"primary": false,
|
|
61
|
+
"nullable": true,
|
|
62
|
+
"mappedType": "json"
|
|
63
|
+
},
|
|
64
|
+
"body_variables": {
|
|
65
|
+
"name": "body_variables",
|
|
66
|
+
"type": "jsonb",
|
|
67
|
+
"unsigned": false,
|
|
68
|
+
"autoincrement": false,
|
|
69
|
+
"primary": false,
|
|
70
|
+
"nullable": true,
|
|
71
|
+
"mappedType": "json"
|
|
72
|
+
},
|
|
37
73
|
"title": {
|
|
38
74
|
"name": "title",
|
|
39
75
|
"type": "text",
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
|
|
3
|
+
export class Migration20260129082610 extends Migration {
|
|
4
|
+
|
|
5
|
+
override async up(): Promise<void> {
|
|
6
|
+
this.addSql(`alter table "notifications" add column if not exists "title_key" text null, add column if not exists "body_key" text null, add column if not exists "title_variables" jsonb null, add column if not exists "body_variables" jsonb null;`);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
override async down(): Promise<void> {
|
|
10
|
+
this.addSql(`alter table "notifications" drop column if exists "title_key", drop column if exists "body_key", drop column if exists "title_variables", drop column if exists "body_variables";`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
}
|