create-mercato-app 0.4.9-develop-7afbe1e834 → 0.4.9-develop-94fb251ed3
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/package.json +1 -1
- package/template/.env.example +21 -0
- package/template/src/app/(backend)/backend/[...slug]/page.tsx +27 -1
- package/template/src/app/(backend)/backend/page.tsx +21 -0
- package/template/src/app/(frontend)/[...slug]/page.tsx +27 -1
- package/template/src/app/api/[...slug]/route.ts +2 -2
- package/template/src/bootstrap.ts +2 -0
- package/template/src/modules/example/__integration__/TC-UMES-003.spec.ts +2 -1
- package/template/src/modules/example/api/notifications/route.ts +16 -19
package/package.json
CHANGED
package/template/.env.example
CHANGED
|
@@ -37,6 +37,27 @@ OM_ENABLE_ENTERPRISE_MODULES=false
|
|
|
37
37
|
# Requires OM_ENABLE_ENTERPRISE_MODULES=true
|
|
38
38
|
OM_ENABLE_ENTERPRISE_MODULES_SSO=false
|
|
39
39
|
|
|
40
|
+
# Enterprise security module (MFA, passkeys, sudo)
|
|
41
|
+
# Active when enterprise modules are enabled and the security module is installed.
|
|
42
|
+
# Optional dedicated signing secret for MFA setup tokens.
|
|
43
|
+
# Falls back to AUTH_JWT_SECRET, AUTH_SECRET, or JWT_SECRET when left blank.
|
|
44
|
+
# Enable enterprise security module (default: false)
|
|
45
|
+
# Requires OM_ENABLE_ENTERPRISE_MODULES=true
|
|
46
|
+
OM_ENABLE_ENTERPRISE_MODULES_SECURITY=false
|
|
47
|
+
|
|
48
|
+
# OM_SECURITY_MFA_SETUP_SECRET=change-me-mfa-setup-secret
|
|
49
|
+
OM_SECURITY_TOTP_ISSUER=Open Mercato
|
|
50
|
+
OM_SECURITY_TOTP_WINDOW=1
|
|
51
|
+
OM_SECURITY_OTP_EXPIRY_SECONDS=600
|
|
52
|
+
OM_SECURITY_OTP_MAX_ATTEMPTS=5
|
|
53
|
+
OM_SECURITY_SUDO_DEFAULT_TTL=300
|
|
54
|
+
OM_SECURITY_SUDO_MAX_TTL=1800
|
|
55
|
+
OM_SECURITY_WEBAUTHN_RP_NAME=Open Mercato
|
|
56
|
+
# Defaults to the hostname from APP_URL / NEXT_PUBLIC_APP_URL when left blank.
|
|
57
|
+
OM_SECURITY_WEBAUTHN_RP_ID=
|
|
58
|
+
OM_SECURITY_RECOVERY_CODE_COUNT=10
|
|
59
|
+
OM_SECURITY_MFA_EMERGENCY_BYPASS=false
|
|
60
|
+
|
|
40
61
|
# Admin email to notify about onboarding submissions
|
|
41
62
|
ADMIN_EMAIL=ops@your-domain.com
|
|
42
63
|
|
|
@@ -4,6 +4,7 @@ import Link from 'next/link'
|
|
|
4
4
|
import { findBackendMatch } from '@open-mercato/shared/modules/registry'
|
|
5
5
|
import { modules } from '@/.mercato/generated/modules.generated'
|
|
6
6
|
import { getAuthFromCookies } from '@open-mercato/shared/lib/auth/server'
|
|
7
|
+
import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
|
|
7
8
|
import { ApplyBreadcrumb } from '@open-mercato/ui/backend/AppShell'
|
|
8
9
|
import { AccessDeniedMessage } from '@open-mercato/ui/backend/detail'
|
|
9
10
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
@@ -13,6 +14,8 @@ import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacS
|
|
|
13
14
|
import { ComponentReplacementHandles, resolveRegisteredComponent } from '@open-mercato/shared/modules/widgets/component-registry'
|
|
14
15
|
import type { Metadata } from 'next'
|
|
15
16
|
import { resolveLocalizedTitleMetadata } from '@/lib/metadata'
|
|
17
|
+
import { resolvePageMiddlewareRedirect } from '@open-mercato/shared/lib/middleware/page-executor'
|
|
18
|
+
import { backendMiddlewareEntries } from '@/.mercato/generated/backend-middleware.generated'
|
|
16
19
|
|
|
17
20
|
type Awaitable<T> = T | Promise<T>
|
|
18
21
|
|
|
@@ -52,8 +55,16 @@ export default async function BackendCatchAll(props: BackendParams) {
|
|
|
52
55
|
const pathname = '/backend/' + (params.slug?.join('/') ?? '')
|
|
53
56
|
const match = findBackendMatch(modules, pathname)
|
|
54
57
|
if (!match) return notFound()
|
|
58
|
+
let auth: AuthContext = null
|
|
59
|
+
let container: Awaited<ReturnType<typeof createRequestContainer>> | null = null
|
|
60
|
+
const ensureContainer = async () => {
|
|
61
|
+
if (!container) {
|
|
62
|
+
container = await createRequestContainer()
|
|
63
|
+
}
|
|
64
|
+
return container
|
|
65
|
+
}
|
|
55
66
|
if (match.route.requireAuth) {
|
|
56
|
-
|
|
67
|
+
auth = await getAuthFromCookies()
|
|
57
68
|
if (!auth) redirect('/api/auth/session/refresh?redirect=' + encodeURIComponent(pathname))
|
|
58
69
|
const required = match.route.requireRoles || []
|
|
59
70
|
if (required.length) {
|
|
@@ -84,6 +95,21 @@ export default async function BackendCatchAll(props: BackendParams) {
|
|
|
84
95
|
if (!ok) return renderAccessDenied()
|
|
85
96
|
}
|
|
86
97
|
}
|
|
98
|
+
const middlewareRedirect = await resolvePageMiddlewareRedirect({
|
|
99
|
+
entries: backendMiddlewareEntries,
|
|
100
|
+
context: {
|
|
101
|
+
pathname,
|
|
102
|
+
mode: 'backend',
|
|
103
|
+
routeMeta: {
|
|
104
|
+
requireAuth: match.route.requireAuth,
|
|
105
|
+
requireRoles: match.route.requireRoles,
|
|
106
|
+
requireFeatures: match.route.requireFeatures,
|
|
107
|
+
},
|
|
108
|
+
auth,
|
|
109
|
+
ensureContainer,
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
if (middlewareRedirect) redirect(middlewareRedirect)
|
|
87
113
|
const pageHandle = ComponentReplacementHandles.page(pathname)
|
|
88
114
|
const Component = resolveRegisteredComponent(pageHandle, match.route.Component)
|
|
89
115
|
|
|
@@ -1,10 +1,31 @@
|
|
|
1
1
|
import { getAuthFromCookies } from '@open-mercato/shared/lib/auth/server'
|
|
2
2
|
import { redirect } from 'next/navigation'
|
|
3
3
|
import { DashboardScreen } from '@open-mercato/ui/backend/dashboard'
|
|
4
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
5
|
+
import { resolvePageMiddlewareRedirect } from '@open-mercato/shared/lib/middleware/page-executor'
|
|
6
|
+
import { backendMiddlewareEntries } from '@/.mercato/generated/backend-middleware.generated'
|
|
4
7
|
|
|
5
8
|
export default async function BackendIndex() {
|
|
6
9
|
const auth = await getAuthFromCookies()
|
|
7
10
|
if (!auth) redirect('/api/auth/session/refresh?redirect=/backend')
|
|
11
|
+
let container: Awaited<ReturnType<typeof createRequestContainer>> | null = null
|
|
12
|
+
const ensureContainer = async () => {
|
|
13
|
+
if (!container) {
|
|
14
|
+
container = await createRequestContainer()
|
|
15
|
+
}
|
|
16
|
+
return container
|
|
17
|
+
}
|
|
18
|
+
const middlewareRedirect = await resolvePageMiddlewareRedirect({
|
|
19
|
+
entries: backendMiddlewareEntries,
|
|
20
|
+
context: {
|
|
21
|
+
pathname: '/backend',
|
|
22
|
+
mode: 'backend',
|
|
23
|
+
routeMeta: { requireAuth: true },
|
|
24
|
+
auth,
|
|
25
|
+
ensureContainer,
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
if (middlewareRedirect) redirect(middlewareRedirect)
|
|
8
29
|
return (
|
|
9
30
|
<div className="p-6 space-y-6">
|
|
10
31
|
<DashboardScreen />
|
|
@@ -4,12 +4,15 @@ import { findFrontendMatch } from '@open-mercato/shared/modules/registry'
|
|
|
4
4
|
import { modules } from '@/.mercato/generated/modules.generated'
|
|
5
5
|
import { getAuthFromCookies } from '@open-mercato/shared/lib/auth/server'
|
|
6
6
|
import { AccessDeniedMessage } from '@open-mercato/ui/backend/detail'
|
|
7
|
+
import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
|
|
7
8
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
8
9
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
9
10
|
import { hasAllFeatures } from '@open-mercato/shared/lib/auth/featureMatch'
|
|
10
11
|
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
11
12
|
import type { Metadata } from 'next'
|
|
12
13
|
import { resolveLocalizedTitleMetadata } from '@/lib/metadata'
|
|
14
|
+
import { resolvePageMiddlewareRedirect } from '@open-mercato/shared/lib/middleware/page-executor'
|
|
15
|
+
import { frontendMiddlewareEntries } from '@/.mercato/generated/frontend-middleware.generated'
|
|
13
16
|
|
|
14
17
|
type FrontendParams = { params: Promise<{ slug: string[] }> }
|
|
15
18
|
|
|
@@ -68,8 +71,16 @@ export default async function SiteCatchAll({ params }: FrontendParams) {
|
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
// Staff auth gate
|
|
74
|
+
let auth: AuthContext = null
|
|
75
|
+
let container: Awaited<ReturnType<typeof createRequestContainer>> | null = null
|
|
76
|
+
const ensureContainer = async () => {
|
|
77
|
+
if (!container) {
|
|
78
|
+
container = await createRequestContainer()
|
|
79
|
+
}
|
|
80
|
+
return container
|
|
81
|
+
}
|
|
71
82
|
if (match.route.requireAuth) {
|
|
72
|
-
|
|
83
|
+
auth = await getAuthFromCookies()
|
|
73
84
|
if (!auth) redirect('/api/auth/session/refresh?redirect=' + encodeURIComponent(pathname))
|
|
74
85
|
const required = match.route.requireRoles || []
|
|
75
86
|
if (required.length) {
|
|
@@ -85,6 +96,21 @@ export default async function SiteCatchAll({ params }: FrontendParams) {
|
|
|
85
96
|
if (!ok) return renderAccessDenied()
|
|
86
97
|
}
|
|
87
98
|
}
|
|
99
|
+
const middlewareRedirect = await resolvePageMiddlewareRedirect({
|
|
100
|
+
entries: frontendMiddlewareEntries,
|
|
101
|
+
context: {
|
|
102
|
+
pathname,
|
|
103
|
+
mode: 'frontend',
|
|
104
|
+
routeMeta: {
|
|
105
|
+
requireAuth: match.route.requireAuth,
|
|
106
|
+
requireRoles: match.route.requireRoles,
|
|
107
|
+
requireFeatures: match.route.requireFeatures,
|
|
108
|
+
},
|
|
109
|
+
auth,
|
|
110
|
+
ensureContainer,
|
|
111
|
+
},
|
|
112
|
+
})
|
|
113
|
+
if (middlewareRedirect) redirect(middlewareRedirect)
|
|
88
114
|
const Component = match.route.Component
|
|
89
115
|
return <Component params={match.params} />
|
|
90
116
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NextResponse, type NextRequest } from 'next/server'
|
|
2
2
|
import { findApi, type HttpMethod } from '@open-mercato/shared/modules/registry'
|
|
3
|
-
import {
|
|
3
|
+
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
4
4
|
import { modules } from '@/.mercato/generated/modules.generated'
|
|
5
5
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
6
6
|
import { bootstrap } from '@/bootstrap'
|
|
@@ -135,7 +135,7 @@ async function checkAuthorization(
|
|
|
135
135
|
const guardContainer = await ensureContainer()
|
|
136
136
|
await enforceTenantSelection({ auth, container: guardContainer }, tenantCandidate)
|
|
137
137
|
} catch (error) {
|
|
138
|
-
if (error
|
|
138
|
+
if (isCrudHttpError(error)) {
|
|
139
139
|
return NextResponse.json(error.body ?? { error: t('api.errors.forbidden', 'Forbidden') }, { status: error.status })
|
|
140
140
|
}
|
|
141
141
|
throw error
|
|
@@ -53,11 +53,13 @@ import { messageTypes } from '@/.mercato/generated/message-types.generated'
|
|
|
53
53
|
import { messageObjectTypes } from '@/.mercato/generated/message-objects.generated'
|
|
54
54
|
import { registerMessageTypes } from '@open-mercato/core/modules/messages/lib/message-types-registry'
|
|
55
55
|
import { registerMessageObjectTypes } from '@open-mercato/core/modules/messages/lib/message-objects-registry'
|
|
56
|
+
import { runBootstrapRegistrations } from '@/.mercato/generated/bootstrap-registrations.generated'
|
|
56
57
|
|
|
57
58
|
// Register event configs globally (similar to search)
|
|
58
59
|
registerEventModuleConfigs(eventModuleConfigs)
|
|
59
60
|
registerMessageTypes(messageTypes, { replace: true })
|
|
60
61
|
registerMessageObjectTypes(messageObjectTypes, { replace: true })
|
|
62
|
+
runBootstrapRegistrations()
|
|
61
63
|
|
|
62
64
|
// Bootstrap factory from shared package
|
|
63
65
|
import { createBootstrap, isBootstrapped } from '@open-mercato/shared/lib/bootstrap'
|
|
@@ -615,8 +615,9 @@ test.describe('TC-UMES-003: Events & DOM Bridge', () => {
|
|
|
615
615
|
await page.getByTestId('phase-d-run-probe').click()
|
|
616
616
|
|
|
617
617
|
await expect(page.getByTestId('phase-d-status')).toContainText('ok')
|
|
618
|
-
await expect(page.getByTestId('phase-d-result')).toContainText(personId)
|
|
619
618
|
await expect(page.getByTestId('phase-d-result')).toContainText('_example')
|
|
619
|
+
await expect(page.getByTestId('phase-d-result')).toContainText('selectedRecord')
|
|
620
|
+
await expect(page.getByTestId('phase-d-result')).toContainText('inspectedCount')
|
|
620
621
|
await expect(page.getByTestId('phase-d-result')).toContainText('example.customer-todo-count')
|
|
621
622
|
} finally {
|
|
622
623
|
await deleteEntityIfExists(request, adminToken, '/api/customers/people', personId)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { resolveNotificationContext } from '@open-mercato/core/modules/notifications/lib/routeHelpers'
|
|
3
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
3
4
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
4
5
|
|
|
5
6
|
const emitNotificationSchema = z.object({
|
|
@@ -65,27 +66,23 @@ export async function POST(request: Request) {
|
|
|
65
66
|
return Response.json({ id: notification.id }, { status: 201 })
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
export const openApi = {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
'application/json'
|
|
76
|
-
|
|
77
|
-
},
|
|
69
|
+
export const openApi: OpenApiRouteDoc = {
|
|
70
|
+
tag: 'Example',
|
|
71
|
+
methods: {
|
|
72
|
+
POST: {
|
|
73
|
+
summary: 'Emit example actionable notification',
|
|
74
|
+
tags: ['Example'],
|
|
75
|
+
requestBody: {
|
|
76
|
+
contentType: 'application/json',
|
|
77
|
+
schema: emitNotificationSchema.optional(),
|
|
78
78
|
},
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
'application/json': {
|
|
85
|
-
schema: z.object({ id: z.string().uuid() }),
|
|
86
|
-
},
|
|
79
|
+
responses: [
|
|
80
|
+
{
|
|
81
|
+
status: 201,
|
|
82
|
+
description: 'Notification emitted',
|
|
83
|
+
schema: z.object({ id: z.string().uuid() }),
|
|
87
84
|
},
|
|
88
|
-
|
|
85
|
+
],
|
|
89
86
|
},
|
|
90
87
|
},
|
|
91
88
|
}
|