@open-mercato/core 0.4.5-develop-03023b2707 → 0.4.5-develop-0c30cb4b11

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.
@@ -5,6 +5,8 @@ import {
5
5
  getCoreInjectionTables,
6
6
  invalidateInjectionWidgetCache,
7
7
  loadAllInjectionWidgets,
8
+ loadInjectionDataWidgetById,
9
+ loadInjectionDataWidgetsForSpot,
8
10
  loadInjectionWidgetById,
9
11
  loadInjectionWidgetsForSpot
10
12
  } from "@open-mercato/shared/modules/widgets/injection-loader";
@@ -13,6 +15,8 @@ export {
13
15
  getCoreInjectionWidgets,
14
16
  invalidateInjectionWidgetCache,
15
17
  loadAllInjectionWidgets,
18
+ loadInjectionDataWidgetById,
19
+ loadInjectionDataWidgetsForSpot,
16
20
  loadInjectionWidgetById,
17
21
  loadInjectionWidgetsForSpot,
18
22
  registerCoreInjectionTables,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/widgets/lib/injection.ts"],
4
- "sourcesContent": ["// Re-export from shared for backward compatibility\n// New code should import directly from @open-mercato/shared/modules/widgets/injection-loader\nexport {\n registerCoreInjectionWidgets,\n getCoreInjectionWidgets,\n registerCoreInjectionTables,\n getCoreInjectionTables,\n invalidateInjectionWidgetCache,\n loadAllInjectionWidgets,\n loadInjectionWidgetById,\n loadInjectionWidgetsForSpot,\n type LoadedInjectionWidget,\n} from '@open-mercato/shared/modules/widgets/injection-loader'\n"],
5
- "mappings": "AAEA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;",
4
+ "sourcesContent": ["// Re-export from shared for backward compatibility\n// New code should import directly from @open-mercato/shared/modules/widgets/injection-loader\nexport {\n registerCoreInjectionWidgets,\n getCoreInjectionWidgets,\n registerCoreInjectionTables,\n getCoreInjectionTables,\n invalidateInjectionWidgetCache,\n loadAllInjectionWidgets,\n loadInjectionDataWidgetById,\n loadInjectionDataWidgetsForSpot,\n loadInjectionWidgetById,\n loadInjectionWidgetsForSpot,\n type LoadedInjectionDataWidget,\n type LoadedInjectionWidget,\n} from '@open-mercato/shared/modules/widgets/injection-loader'\n"],
5
+ "mappings": "AAEA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.4.5-develop-03023b2707",
3
+ "version": "0.4.5-develop-0c30cb4b11",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -207,7 +207,7 @@
207
207
  }
208
208
  },
209
209
  "dependencies": {
210
- "@open-mercato/shared": "0.4.5-develop-03023b2707",
210
+ "@open-mercato/shared": "0.4.5-develop-0c30cb4b11",
211
211
  "@types/semver": "^7.5.8",
212
212
  "@xyflow/react": "^12.6.0",
213
213
  "ai": "^6.0.0",
@@ -98,13 +98,23 @@ export async function POST(req: Request) {
98
98
  email: user.email,
99
99
  roles: userRoleNames
100
100
  })
101
- const res = NextResponse.json({ ok: true, token, redirect: '/backend' })
102
- res.cookies.set('auth_token', token, { httpOnly: true, path: '/', sameSite: 'lax', secure: process.env.NODE_ENV === 'production', maxAge: 60 * 60 * 8 })
101
+ const responseData: { ok: true; token: string; redirect: string; refreshToken?: string } = {
102
+ ok: true,
103
+ token,
104
+ redirect: '/backend',
105
+ }
103
106
  if (remember) {
104
107
  const days = Number(process.env.REMEMBER_ME_DAYS || '30')
105
108
  const expiresAt = new Date(Date.now() + days * 24 * 60 * 60 * 1000)
106
109
  const sess = await auth.createSession(user, expiresAt)
107
- res.cookies.set('session_token', sess.token, { httpOnly: true, path: '/', sameSite: 'lax', secure: process.env.NODE_ENV === 'production', expires: expiresAt })
110
+ responseData.refreshToken = sess.token
111
+ }
112
+ const res = NextResponse.json(responseData)
113
+ res.cookies.set('auth_token', token, { httpOnly: true, path: '/', sameSite: 'lax', secure: process.env.NODE_ENV === 'production', maxAge: 60 * 60 * 8 })
114
+ if (remember && responseData.refreshToken) {
115
+ const days = Number(process.env.REMEMBER_ME_DAYS || '30')
116
+ const expiresAt = new Date(Date.now() + days * 24 * 60 * 60 * 1000)
117
+ res.cookies.set('session_token', responseData.refreshToken, { httpOnly: true, path: '/', sameSite: 'lax', secure: process.env.NODE_ENV === 'production', expires: expiresAt })
108
118
  }
109
119
  return res
110
120
  }
@@ -118,6 +128,7 @@ const loginSuccessSchema = z.object({
118
128
  ok: z.literal(true),
119
129
  token: z.string().describe('JWT token issued for subsequent API calls'),
120
130
  redirect: z.string().nullable().describe('Next location the client should navigate to'),
131
+ refreshToken: z.string().optional().describe('Long-lived refresh token for obtaining new access tokens (only present when remember=true)'),
121
132
  })
122
133
 
123
134
  const loginErrorSchema = z.object({
@@ -15,6 +15,7 @@ import { buildPasswordSchema } from '@open-mercato/shared/lib/auth/passwordPolic
15
15
 
16
16
  const profileResponseSchema = z.object({
17
17
  email: z.string().email(),
18
+ roles: z.array(z.string()),
18
19
  })
19
20
 
20
21
  const passwordSchema = buildPasswordSchema()
@@ -67,7 +68,7 @@ export async function GET(req: Request) {
67
68
  if (!user) {
68
69
  return NextResponse.json({ error: translate('auth.users.form.errors.notFound', 'User not found') }, { status: 404 })
69
70
  }
70
- return NextResponse.json({ email: String(user.email) })
71
+ return NextResponse.json({ email: String(user.email), roles: auth.roles ?? [] })
71
72
  } catch (err) {
72
73
  console.error('auth.profile.load failed', err)
73
74
  return NextResponse.json({ error: translate('auth.profile.form.errors.load', 'Failed to load profile.') }, { status: 400 })
@@ -41,25 +41,104 @@ export async function GET(req: Request) {
41
41
  return res
42
42
  }
43
43
 
44
+ export async function POST(req: Request) {
45
+ let token: string | null = null
46
+
47
+ try {
48
+ const body = await req.json()
49
+ const parsed = refreshRequestSchema.safeParse(body)
50
+ if (parsed.success) {
51
+ token = parsed.data.refreshToken
52
+ }
53
+ } catch {
54
+ // Invalid JSON
55
+ }
56
+
57
+ if (!token) {
58
+ return NextResponse.json({ ok: false, error: 'Missing or invalid refresh token' }, { status: 400 })
59
+ }
60
+
61
+ const c = await createRequestContainer()
62
+ const auth = c.resolve<AuthService>('authService')
63
+ const ctx = await auth.refreshFromSessionToken(token)
64
+
65
+ if (!ctx) {
66
+ return NextResponse.json({ ok: false, error: 'Invalid or expired refresh token' }, { status: 401 })
67
+ }
68
+
69
+ const { user, roles } = ctx
70
+ const jwt = signJwt({
71
+ sub: String(user.id),
72
+ tenantId: String(user.tenantId),
73
+ orgId: String(user.organizationId),
74
+ email: user.email,
75
+ roles,
76
+ })
77
+
78
+ const res = NextResponse.json({
79
+ ok: true,
80
+ accessToken: jwt,
81
+ expiresIn: 60 * 60 * 8,
82
+ })
83
+
84
+ res.cookies.set('auth_token', jwt, {
85
+ httpOnly: true,
86
+ path: '/',
87
+ sameSite: 'lax',
88
+ secure: process.env.NODE_ENV === 'production',
89
+ maxAge: 60 * 60 * 8,
90
+ })
91
+
92
+ return res
93
+ }
94
+
44
95
  export const metadata = {
45
96
  GET: { requireAuth: false },
97
+ POST: { requireAuth: false },
46
98
  }
47
99
 
48
100
  const refreshQuerySchema = z.object({
49
101
  redirect: z.string().optional().describe('Absolute or relative URL to redirect after refresh'),
50
102
  })
51
103
 
104
+ const refreshRequestSchema = z.object({
105
+ refreshToken: z.string().min(1).describe('The refresh token obtained from login'),
106
+ })
107
+
108
+ const refreshSuccessSchema = z.object({
109
+ ok: z.literal(true),
110
+ accessToken: z.string().describe('New JWT access token'),
111
+ expiresIn: z.number().describe('Token expiration time in seconds'),
112
+ })
113
+
114
+ const refreshErrorSchema = z.object({
115
+ ok: z.literal(false),
116
+ error: z.string(),
117
+ })
118
+
52
119
  export const openApi: OpenApiRouteDoc = {
53
120
  tag: 'Authentication & Accounts',
54
121
  summary: 'Refresh session token',
55
122
  methods: {
56
123
  GET: {
57
- summary: 'Refresh auth cookie from session token',
124
+ summary: 'Refresh auth cookie from session token (browser)',
58
125
  description: 'Exchanges an existing `session_token` cookie for a fresh JWT auth cookie and redirects the browser.',
59
126
  query: refreshQuerySchema,
60
127
  responses: [
61
128
  { status: 302, description: 'Redirect to target location when session is valid', mediaType: 'text/html' },
62
129
  ],
63
130
  },
131
+ POST: {
132
+ summary: 'Refresh access token (API/mobile)',
133
+ description: 'Exchanges a refresh token for a new JWT access token. Pass the refresh token obtained from login in the request body.',
134
+ requestBody: { schema: refreshRequestSchema, contentType: 'application/json' },
135
+ responses: [
136
+ { status: 200, description: 'New access token issued', schema: refreshSuccessSchema },
137
+ ],
138
+ errors: [
139
+ { status: 400, description: 'Missing refresh token', schema: refreshErrorSchema },
140
+ { status: 401, description: 'Invalid or expired token', schema: refreshErrorSchema },
141
+ ],
142
+ },
64
143
  },
65
144
  }
@@ -20,6 +20,7 @@ export type RoleSidebarPreferenceScope = {
20
20
  }
21
21
 
22
22
  export type SidebarItemLike<T = Record<string, unknown>> = {
23
+ id?: string
23
24
  href: string
24
25
  title: string
25
26
  defaultTitle: string
@@ -160,11 +161,17 @@ export function applySidebarPreference<T extends SidebarGroupLike>(
160
161
  if (!orderIndex.has(id)) orderIndex.set(id, idx)
161
162
  })
162
163
  const hiddenSet = new Set(normalized.hiddenItems ?? [])
164
+ const resolveItemKey = (item: SidebarItemLike): string => {
165
+ const candidate = item.id?.trim()
166
+ if (candidate && candidate.length > 0) return candidate
167
+ return item.href
168
+ }
163
169
  const applyItems = <TI extends SidebarItemLike>(items: TI[]): TI[] => {
164
170
  return items.map((item) => {
165
- const override = normalized.itemLabels?.[item.href]
171
+ const itemKey = resolveItemKey(item)
172
+ const override = normalized.itemLabels?.[itemKey] ?? normalized.itemLabels?.[item.href]
166
173
  const nextChildren = item.children ? applyItems(item.children) : undefined
167
- const hidden = hiddenSet.has(item.href)
174
+ const hidden = hiddenSet.has(itemKey) || hiddenSet.has(item.href)
168
175
  const next = {
169
176
  ...item,
170
177
  title: override && override.trim().length > 0 ? override.trim() : item.defaultTitle,
@@ -60,6 +60,7 @@ const crud = makeCrudRoute({
60
60
  tenantField: 'tenantId',
61
61
  softDeleteField: 'deletedAt',
62
62
  },
63
+ enrichers: { entityId: 'customers.person' },
63
64
  list: {
64
65
  schema: listSchema,
65
66
  entityId: E.customers.customer_entity,