@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.
- 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 +68 -0
- package/dist/modules/directory/api/get/tenants/lookup.js.map +7 -0
- 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 +73 -0
- 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({
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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
|
})
|
|
@@ -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,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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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('
|
|
369
|
+
console.log('✅ All example workflows seeded successfully!')
|
|
370
370
|
} catch (error) {
|
|
371
371
|
console.error('Error seeding workflows:', error)
|
|
372
372
|
throw error
|