@open-mercato/core 0.4.2-canary-19353c5970 → 0.4.2-canary-19703ca707

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.
Files changed (30) hide show
  1. package/dist/modules/auth/api/login.js +25 -6
  2. package/dist/modules/auth/api/login.js.map +2 -2
  3. package/dist/modules/auth/data/validators.js +2 -1
  4. package/dist/modules/auth/data/validators.js.map +2 -2
  5. package/dist/modules/auth/frontend/login.js +85 -1
  6. package/dist/modules/auth/frontend/login.js.map +2 -2
  7. package/dist/modules/auth/lib/setup-app.js +25 -12
  8. package/dist/modules/auth/lib/setup-app.js.map +2 -2
  9. package/dist/modules/auth/services/authService.js +21 -0
  10. package/dist/modules/auth/services/authService.js.map +2 -2
  11. package/dist/modules/business_rules/cli.js +2 -1
  12. package/dist/modules/business_rules/cli.js.map +2 -2
  13. package/dist/modules/directory/api/get/tenants/lookup.js +68 -0
  14. package/dist/modules/directory/api/get/tenants/lookup.js.map +7 -0
  15. package/dist/modules/workflows/cli.js +12 -12
  16. package/dist/modules/workflows/cli.js.map +2 -2
  17. package/package.json +2 -2
  18. package/src/modules/auth/api/__tests__/login.test.ts +2 -0
  19. package/src/modules/auth/api/login.ts +26 -7
  20. package/src/modules/auth/data/validators.ts +1 -0
  21. package/src/modules/auth/frontend/login.tsx +106 -2
  22. package/src/modules/auth/i18n/de.json +5 -0
  23. package/src/modules/auth/i18n/en.json +5 -0
  24. package/src/modules/auth/i18n/es.json +5 -0
  25. package/src/modules/auth/i18n/pl.json +5 -0
  26. package/src/modules/auth/lib/setup-app.ts +37 -15
  27. package/src/modules/auth/services/authService.ts +23 -0
  28. package/src/modules/business_rules/cli.ts +2 -1
  29. package/src/modules/directory/api/get/tenants/lookup.ts +73 -0
  30. package/src/modules/workflows/cli.ts +12 -12
@@ -15,6 +15,8 @@ jest.mock('@open-mercato/shared/lib/di/container', () => ({
15
15
  createRequestContainer: async () => ({
16
16
  resolve: (_: string) => ({
17
17
  findUserByEmail: async (email: string) => ({ id: 1, email, passwordHash: 'hash', tenantId: tenantId, organizationId: orgId }),
18
+ findUsersByEmail: async (email: string) => ([{ id: 1, email, passwordHash: 'hash', tenantId: tenantId, organizationId: orgId }]),
19
+ findUserByEmailAndTenant: async (email: string) => ({ id: 1, email, passwordHash: 'hash', tenantId: tenantId, organizationId: orgId }),
18
20
  verifyPassword: async () => true,
19
21
  getUserRoles: async (_user: any, _tenant: string | null | undefined) => ['admin'],
20
22
  updateLastLoginAt: async () => undefined,
@@ -17,15 +17,33 @@ export async function POST(req: Request) {
17
17
  const email = String(form.get('email') ?? '')
18
18
  const password = String(form.get('password') ?? '')
19
19
  const remember = parseBooleanToken(form.get('remember')?.toString()) === true
20
+ const tenantIdRaw = String(form.get('tenantId') ?? form.get('tenant') ?? '').trim()
20
21
  const requireRoleRaw = (String(form.get('requireRole') ?? form.get('role') ?? '')).trim()
21
22
  const requiredRoles = requireRoleRaw ? requireRoleRaw.split(',').map((s) => s.trim()).filter(Boolean) : []
22
- const parsed = userLoginSchema.pick({ email: true, password: true }).safeParse({ email, password })
23
+ const parsed = userLoginSchema.pick({ email: true, password: true, tenantId: true }).safeParse({
24
+ email,
25
+ password,
26
+ tenantId: tenantIdRaw || undefined,
27
+ })
23
28
  if (!parsed.success) {
24
29
  return NextResponse.json({ ok: false, error: translate('auth.login.errors.invalidCredentials', 'Invalid credentials') }, { status: 400 })
25
30
  }
26
31
  const container = await createRequestContainer()
27
32
  const auth = (container.resolve('authService') as AuthService)
28
- const user = await auth.findUserByEmail(parsed.data.email)
33
+ const tenantId = parsed.data.tenantId ?? null
34
+ let user = null
35
+ if (tenantId) {
36
+ user = await auth.findUserByEmailAndTenant(parsed.data.email, tenantId)
37
+ } else {
38
+ const users = await auth.findUsersByEmail(parsed.data.email)
39
+ if (users.length > 1) {
40
+ return NextResponse.json({
41
+ ok: false,
42
+ error: translate('auth.login.errors.tenantRequired', 'Use the login link provided with your tenant activation to continue.'),
43
+ }, { status: 400 })
44
+ }
45
+ user = users[0] ?? null
46
+ }
29
47
  if (!user || !user.passwordHash) {
30
48
  return NextResponse.json({ ok: false, error: translate('auth.login.errors.invalidCredentials', 'Invalid email or password') }, { status: 401 })
31
49
  }
@@ -35,26 +53,27 @@ export async function POST(req: Request) {
35
53
  }
36
54
  // Optional role requirement
37
55
  if (requiredRoles.length) {
38
- const userRoleNames = await auth.getUserRoles(user, user.tenantId ? String(user.tenantId) : null)
56
+ const userRoleNames = await auth.getUserRoles(user, tenantId ?? (user.tenantId ? String(user.tenantId) : null))
39
57
  const authorized = requiredRoles.some(r => userRoleNames.includes(r))
40
58
  if (!authorized) {
41
59
  return NextResponse.json({ ok: false, error: translate('auth.login.errors.permissionDenied', 'Not authorized for this area') }, { status: 403 })
42
60
  }
43
61
  }
44
62
  await auth.updateLastLoginAt(user)
45
- const userRoleNames = await auth.getUserRoles(user, user.tenantId ? String(user.tenantId) : null)
63
+ const resolvedTenantId = tenantId ?? (user.tenantId ? String(user.tenantId) : null)
64
+ const userRoleNames = await auth.getUserRoles(user, resolvedTenantId)
46
65
  try {
47
66
  const eventBus = (container.resolve('eventBus') as EventBus)
48
67
  void eventBus.emitEvent('query_index.coverage.warmup', {
49
- tenantId: user.tenantId ? String(user.tenantId) : null,
68
+ tenantId: resolvedTenantId,
50
69
  }).catch(() => undefined)
51
70
  } catch {
52
71
  // optional warmup
53
72
  }
54
73
  const token = signJwt({
55
74
  sub: String(user.id),
56
- tenantId: user.tenantId ? String(user.tenantId) : null,
57
- orgId: user.organizationId ? String(user.organizationId) : null,
75
+ tenantId: resolvedTenantId,
76
+ orgId: user.organizationId ? String(user.organizationId) : null,
58
77
  email: user.email,
59
78
  roles: userRoleNames
60
79
  })
@@ -8,6 +8,7 @@ export const userLoginSchema = z.object({
8
8
  email: z.string().email(),
9
9
  password: z.string().min(6),
10
10
  requireRole: z.string().optional(),
11
+ tenantId: z.string().uuid().optional(),
11
12
  })
12
13
 
13
14
  export const requestPasswordResetSchema = z.object({
@@ -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, CardTitle, CardDescription } from '@open-mercato/ui/primitives/card'
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<{ email: string; roles: string[]; name?: string | null }> = [
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 [local, domain] = String(primaryUser.email).split('@')
183
- const isSuperadminLocal = (local || '').toLowerCase() === 'superadmin' && !!domain
184
- if (isSuperadminLocal) {
185
- const adminOverride = readEnvValue(DERIVED_EMAIL_ENV.admin)
186
- const employeeOverride = readEnvValue(DERIVED_EMAIL_ENV.employee)
187
- const adminEmail = adminOverride ?? `admin@${domain}`
188
- const employeeEmail = employeeOverride ?? `employee@${domain}`
189
- addUniqueBaseUser(baseUsers, { email: adminEmail, roles: ['admin'] })
190
- addUniqueBaseUser(baseUsers, { email: employeeEmail, roles: ['employee'] })
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 = 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 Guard rules seeding complete:`)
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,73 @@
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
+ }
@@ -56,7 +56,7 @@ const seedDemo: ModuleCli = {
56
56
  })
57
57
 
58
58
  if (existing) {
59
- console.log(`✓ Demo workflow '${demoData.workflowId}' already exists (ID: ${existing.id})`)
59
+ console.log(`ℹ️ Demo workflow '${demoData.workflowId}' already exists (ID: ${existing.id})`)
60
60
  return
61
61
  }
62
62
 
@@ -69,7 +69,7 @@ const seedDemo: ModuleCli = {
69
69
 
70
70
  await em.persistAndFlush(workflow)
71
71
 
72
- console.log(`✓ Seeded demo workflow: ${workflow.workflowName}`)
72
+ console.log(`✅ Seeded demo workflow: ${workflow.workflowName}`)
73
73
  console.log(` - ID: ${workflow.id}`)
74
74
  console.log(` - Workflow ID: ${workflow.workflowId}`)
75
75
  console.log(` - Version: ${workflow.version}`)
@@ -107,15 +107,15 @@ const seedDemoWithRules: ModuleCli = {
107
107
  return
108
108
  }
109
109
 
110
- console.log('Seeding demo workflow with guard rules...\n')
110
+ console.log('🧩 Seeding demo workflow with guard rules...\n')
111
111
 
112
112
  try {
113
113
  // Seed the workflow definition
114
- console.log('1. Seeding demo workflow...')
114
+ console.log('1. 🧩 Seeding demo workflow...')
115
115
  await seedDemo.run(rest)
116
116
 
117
117
  // Seed the guard rules
118
- console.log('\n2. Seeding guard rules...')
118
+ console.log('\n2. 🧠 Seeding guard rules...')
119
119
  const { resolve } = await createRequestContainer()
120
120
  const em = resolve<EntityManager>('em')
121
121
 
@@ -153,7 +153,7 @@ const seedDemoWithRules: ModuleCli = {
153
153
  seededCount++
154
154
  }
155
155
 
156
- console.log(`\n Demo workflow with guard rules seeded successfully!`)
156
+ console.log(`\n Demo workflow with guard rules seeded successfully!`)
157
157
  console.log(` - Workflow: checkout_simple_v1`)
158
158
  console.log(` - Guard rules seeded: ${seededCount}`)
159
159
  console.log(` - Guard rules skipped: ${skippedCount}`)
@@ -195,7 +195,7 @@ const seedSalesPipeline: ModuleCli = {
195
195
  })
196
196
 
197
197
  if (existing) {
198
- console.log(`✓ Sales pipeline workflow '${pipelineData.workflowId}' already exists (ID: ${existing.id})`)
198
+ console.log(`ℹ️ Sales pipeline workflow '${pipelineData.workflowId}' already exists (ID: ${existing.id})`)
199
199
  return
200
200
  }
201
201
 
@@ -208,7 +208,7 @@ const seedSalesPipeline: ModuleCli = {
208
208
 
209
209
  await em.persistAndFlush(workflow)
210
210
 
211
- console.log(`✓ Seeded sales pipeline workflow: ${workflow.workflowName}`)
211
+ console.log(`✅ Seeded sales pipeline workflow: ${workflow.workflowName}`)
212
212
  console.log(` - ID: ${workflow.id}`)
213
213
  console.log(` - Workflow ID: ${workflow.workflowId}`)
214
214
  console.log(` - Version: ${workflow.version}`)
@@ -255,7 +255,7 @@ const seedSimpleApproval: ModuleCli = {
255
255
  })
256
256
 
257
257
  if (existing) {
258
- console.log(`✓ Simple approval workflow '${approvalData.workflowId}' already exists (ID: ${existing.id})`)
258
+ console.log(`ℹ️ Simple approval workflow '${approvalData.workflowId}' already exists (ID: ${existing.id})`)
259
259
  return
260
260
  }
261
261
 
@@ -268,7 +268,7 @@ const seedSimpleApproval: ModuleCli = {
268
268
 
269
269
  await em.persistAndFlush(workflow)
270
270
 
271
- console.log(`✓ Seeded simple approval workflow: ${workflow.workflowName}`)
271
+ console.log(`✅ Seeded simple approval workflow: ${workflow.workflowName}`)
272
272
  console.log(` - ID: ${workflow.id}`)
273
273
  console.log(` - Workflow ID: ${workflow.workflowId}`)
274
274
  console.log(` - Version: ${workflow.version}`)
@@ -351,7 +351,7 @@ const seedAll: ModuleCli = {
351
351
  return
352
352
  }
353
353
 
354
- console.log('Seeding all example workflows...\n')
354
+ console.log('🧩 Seeding all example workflows...\n')
355
355
 
356
356
  try {
357
357
  // Seed demo checkout with rules
@@ -366,7 +366,7 @@ const seedAll: ModuleCli = {
366
366
  await seedSimpleApproval.run(rest)
367
367
  console.log('')
368
368
 
369
- console.log(' All example workflows seeded successfully!')
369
+ console.log(' All example workflows seeded successfully!')
370
370
  } catch (error) {
371
371
  console.error('Error seeding workflows:', error)
372
372
  throw error