@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.
Files changed (37) 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 +70 -0
  14. package/dist/modules/directory/api/get/tenants/lookup.js.map +7 -0
  15. package/dist/modules/notifications/migrations/Migration20260129082610.js +13 -0
  16. package/dist/modules/notifications/migrations/Migration20260129082610.js.map +7 -0
  17. package/dist/modules/query_index/cli.js +63 -7
  18. package/dist/modules/query_index/cli.js.map +2 -2
  19. package/dist/modules/workflows/cli.js +12 -12
  20. package/dist/modules/workflows/cli.js.map +2 -2
  21. package/package.json +2 -2
  22. package/src/modules/auth/api/__tests__/login.test.ts +2 -0
  23. package/src/modules/auth/api/login.ts +26 -7
  24. package/src/modules/auth/data/validators.ts +1 -0
  25. package/src/modules/auth/frontend/login.tsx +106 -2
  26. package/src/modules/auth/i18n/de.json +5 -0
  27. package/src/modules/auth/i18n/en.json +5 -0
  28. package/src/modules/auth/i18n/es.json +5 -0
  29. package/src/modules/auth/i18n/pl.json +5 -0
  30. package/src/modules/auth/lib/setup-app.ts +37 -15
  31. package/src/modules/auth/services/authService.ts +23 -0
  32. package/src/modules/business_rules/cli.ts +2 -1
  33. package/src/modules/directory/api/get/tenants/lookup.ts +75 -0
  34. package/src/modules/notifications/migrations/.snapshot-open-mercato.json +36 -0
  35. package/src/modules/notifications/migrations/Migration20260129082610.ts +13 -0
  36. package/src/modules/query_index/cli.ts +82 -13
  37. 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, 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,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
+ }