@akinon/next 2.0.0-beta.2 → 2.0.0-beta.20

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 (189) hide show
  1. package/.eslintrc.js +12 -0
  2. package/CHANGELOG.md +377 -7
  3. package/__tests__/next-config.test.ts +83 -0
  4. package/__tests__/tsconfig.json +23 -0
  5. package/api/auth.ts +133 -44
  6. package/api/barcode-search.ts +59 -0
  7. package/api/cache.ts +41 -5
  8. package/api/client.ts +21 -4
  9. package/api/form.ts +85 -0
  10. package/api/image-proxy.ts +75 -0
  11. package/api/product-categories.ts +53 -0
  12. package/api/similar-product-list.ts +63 -0
  13. package/api/similar-products.ts +111 -0
  14. package/api/virtual-try-on.ts +382 -0
  15. package/assets/styles/index.scss +84 -0
  16. package/babel.config.js +6 -0
  17. package/bin/pz-generate-routes.js +115 -0
  18. package/bin/pz-prebuild.js +1 -0
  19. package/bin/pz-predev.js +1 -0
  20. package/bin/pz-run-tests.js +99 -0
  21. package/bin/run-prebuild-tests.js +46 -0
  22. package/components/accordion.tsx +20 -5
  23. package/components/button.tsx +51 -36
  24. package/components/client-root.tsx +138 -2
  25. package/components/file-input.tsx +65 -3
  26. package/components/index.ts +1 -0
  27. package/components/input.tsx +1 -1
  28. package/components/link.tsx +46 -16
  29. package/components/logger-popup.tsx +213 -0
  30. package/components/modal.tsx +32 -16
  31. package/components/plugin-module.tsx +62 -3
  32. package/components/price.tsx +2 -2
  33. package/components/select.tsx +1 -1
  34. package/components/selected-payment-option-view.tsx +21 -0
  35. package/components/theme-editor/blocks/accordion-block.tsx +136 -0
  36. package/components/theme-editor/blocks/block-renderer-registry.tsx +77 -0
  37. package/components/theme-editor/blocks/button-block.tsx +593 -0
  38. package/components/theme-editor/blocks/counter-block.tsx +348 -0
  39. package/components/theme-editor/blocks/divider-block.tsx +20 -0
  40. package/components/theme-editor/blocks/embed-block.tsx +208 -0
  41. package/components/theme-editor/blocks/group-block.tsx +116 -0
  42. package/components/theme-editor/blocks/hotspot-block.tsx +147 -0
  43. package/components/theme-editor/blocks/icon-block.tsx +230 -0
  44. package/components/theme-editor/blocks/image-block.tsx +137 -0
  45. package/components/theme-editor/blocks/image-gallery-block.tsx +269 -0
  46. package/components/theme-editor/blocks/input-block.tsx +123 -0
  47. package/components/theme-editor/blocks/link-block.tsx +216 -0
  48. package/components/theme-editor/blocks/lottie-block.tsx +325 -0
  49. package/components/theme-editor/blocks/map-block.tsx +89 -0
  50. package/components/theme-editor/blocks/slider-block.tsx +595 -0
  51. package/components/theme-editor/blocks/tab-block.tsx +10 -0
  52. package/components/theme-editor/blocks/text-block.tsx +52 -0
  53. package/components/theme-editor/blocks/video-block.tsx +122 -0
  54. package/components/theme-editor/components/action-toolbar.tsx +305 -0
  55. package/components/theme-editor/components/designer-overlay.tsx +74 -0
  56. package/components/theme-editor/components/with-designer-features.tsx +142 -0
  57. package/components/theme-editor/dynamic-font-loader.tsx +79 -0
  58. package/components/theme-editor/hooks/use-designer-features.tsx +100 -0
  59. package/components/theme-editor/hooks/use-external-designer.tsx +95 -0
  60. package/components/theme-editor/hooks/use-native-widget-data.ts +188 -0
  61. package/components/theme-editor/hooks/use-visibility-context.ts +27 -0
  62. package/components/theme-editor/placeholder-registry.ts +31 -0
  63. package/components/theme-editor/sections/before-after-section.tsx +245 -0
  64. package/components/theme-editor/sections/contact-form-section.tsx +563 -0
  65. package/components/theme-editor/sections/countdown-campaign-banner-section.tsx +433 -0
  66. package/components/theme-editor/sections/coupon-banner-section.tsx +710 -0
  67. package/components/theme-editor/sections/divider-section.tsx +62 -0
  68. package/components/theme-editor/sections/featured-product-spotlight-section.tsx +507 -0
  69. package/components/theme-editor/sections/find-in-store-section.tsx +1995 -0
  70. package/components/theme-editor/sections/hover-showcase-section.tsx +326 -0
  71. package/components/theme-editor/sections/image-hotspot-section.tsx +142 -0
  72. package/components/theme-editor/sections/installment-options-section.tsx +1065 -0
  73. package/components/theme-editor/sections/notification-banner-section.tsx +173 -0
  74. package/components/theme-editor/sections/order-tracking-lookup-section.tsx +1379 -0
  75. package/components/theme-editor/sections/posts-slider-section.tsx +472 -0
  76. package/components/theme-editor/sections/pre-order-launch-banner-section.tsx +663 -0
  77. package/components/theme-editor/sections/section-renderer-registry.tsx +89 -0
  78. package/components/theme-editor/sections/section-wrapper.tsx +135 -0
  79. package/components/theme-editor/sections/shipping-threshold-progress-section.tsx +586 -0
  80. package/components/theme-editor/sections/stats-counter-section.tsx +486 -0
  81. package/components/theme-editor/sections/tabs-section.tsx +578 -0
  82. package/components/theme-editor/theme-block.tsx +102 -0
  83. package/components/theme-editor/theme-placeholder-client.tsx +218 -0
  84. package/components/theme-editor/theme-placeholder-wrapper.tsx +732 -0
  85. package/components/theme-editor/theme-placeholder.tsx +288 -0
  86. package/components/theme-editor/theme-section.tsx +1224 -0
  87. package/components/theme-editor/theme-settings-context.tsx +13 -0
  88. package/components/theme-editor/utils/index.ts +792 -0
  89. package/components/theme-editor/utils/iterator-utils.ts +234 -0
  90. package/components/theme-editor/utils/publish-window.ts +86 -0
  91. package/components/theme-editor/utils/visibility-rules.ts +188 -0
  92. package/data/client/account.ts +17 -2
  93. package/data/client/api.ts +2 -0
  94. package/data/client/basket.ts +66 -5
  95. package/data/client/checkout.ts +391 -99
  96. package/data/client/misc.ts +38 -2
  97. package/data/client/product.ts +19 -2
  98. package/data/client/user.ts +16 -8
  99. package/data/server/category.ts +11 -9
  100. package/data/server/flatpage.ts +11 -4
  101. package/data/server/form.ts +15 -4
  102. package/data/server/landingpage.ts +11 -4
  103. package/data/server/list.ts +5 -4
  104. package/data/server/menu.ts +11 -3
  105. package/data/server/product.ts +111 -55
  106. package/data/server/seo.ts +14 -4
  107. package/data/server/special-page.ts +5 -4
  108. package/data/server/widget.ts +90 -5
  109. package/data/urls.ts +16 -5
  110. package/hocs/client/with-segment-defaults.tsx +2 -2
  111. package/hocs/server/with-segment-defaults.tsx +65 -20
  112. package/hooks/index.ts +4 -0
  113. package/hooks/use-localization.ts +24 -10
  114. package/hooks/use-logger-context.tsx +114 -0
  115. package/hooks/use-logger.ts +92 -0
  116. package/hooks/use-loyalty-availability.ts +21 -0
  117. package/hooks/use-payment-options.ts +2 -1
  118. package/hooks/use-pz-params.ts +37 -0
  119. package/hooks/use-router.ts +51 -14
  120. package/hooks/use-sentry-uncaught-errors.ts +24 -0
  121. package/instrumentation/index.ts +10 -1
  122. package/instrumentation/node.ts +2 -20
  123. package/jest.config.js +25 -0
  124. package/lib/cache-handler.mjs +534 -16
  125. package/lib/cache.ts +272 -37
  126. package/localization/index.ts +2 -1
  127. package/localization/provider.tsx +2 -5
  128. package/middlewares/bfcache-headers.ts +18 -0
  129. package/middlewares/checkout-provider.ts +1 -1
  130. package/middlewares/complete-gpay.ts +32 -26
  131. package/middlewares/complete-masterpass.ts +33 -26
  132. package/middlewares/complete-wallet.ts +182 -0
  133. package/middlewares/default.ts +360 -215
  134. package/middlewares/index.ts +10 -2
  135. package/middlewares/locale.ts +34 -11
  136. package/middlewares/masterpass-rest-callback.ts +230 -0
  137. package/middlewares/oauth-login.ts +200 -57
  138. package/middlewares/pretty-url.ts +21 -8
  139. package/middlewares/redirection-payment.ts +32 -26
  140. package/middlewares/saved-card-redirection.ts +33 -26
  141. package/middlewares/three-d-redirection.ts +32 -26
  142. package/middlewares/url-redirection.ts +11 -1
  143. package/middlewares/wallet-complete-redirection.ts +206 -0
  144. package/package.json +25 -10
  145. package/plugins.d.ts +19 -4
  146. package/plugins.js +10 -1
  147. package/redux/actions.ts +47 -0
  148. package/redux/middlewares/checkout.ts +63 -138
  149. package/redux/middlewares/index.ts +14 -10
  150. package/redux/middlewares/pre-order/address.ts +7 -2
  151. package/redux/middlewares/pre-order/attribute-based-shipping-option.ts +7 -1
  152. package/redux/middlewares/pre-order/data-source-shipping-option.ts +7 -1
  153. package/redux/middlewares/pre-order/delivery-option.ts +7 -1
  154. package/redux/middlewares/pre-order/index.ts +16 -10
  155. package/redux/middlewares/pre-order/installment-option.ts +8 -1
  156. package/redux/middlewares/pre-order/payment-option-reset.ts +37 -0
  157. package/redux/middlewares/pre-order/payment-option.ts +7 -1
  158. package/redux/middlewares/pre-order/pre-order-validation.ts +8 -3
  159. package/redux/middlewares/pre-order/redirection.ts +8 -2
  160. package/redux/middlewares/pre-order/set-pre-order.ts +6 -2
  161. package/redux/middlewares/pre-order/shipping-option.ts +7 -1
  162. package/redux/middlewares/pre-order/shipping-step.ts +5 -1
  163. package/redux/reducers/checkout.ts +23 -3
  164. package/redux/reducers/index.ts +11 -3
  165. package/redux/reducers/root.ts +7 -2
  166. package/redux/reducers/widget.ts +80 -0
  167. package/sentry/index.ts +69 -13
  168. package/tailwind/content.js +16 -0
  169. package/types/commerce/account.ts +5 -1
  170. package/types/commerce/checkout.ts +35 -1
  171. package/types/commerce/widget.ts +33 -0
  172. package/types/index.ts +101 -6
  173. package/types/next-auth.d.ts +2 -2
  174. package/types/widget.ts +80 -0
  175. package/utils/app-fetch.ts +7 -2
  176. package/utils/generate-commerce-search-params.ts +3 -2
  177. package/utils/get-checkout-path.ts +3 -0
  178. package/utils/get-root-hostname.ts +28 -0
  179. package/utils/index.ts +64 -10
  180. package/utils/localization.ts +4 -0
  181. package/utils/mobile-3d-iframe.ts +8 -2
  182. package/utils/override-middleware.ts +7 -12
  183. package/utils/pz-segments.ts +92 -0
  184. package/utils/redirect-ignore.ts +35 -0
  185. package/utils/redirect.ts +9 -3
  186. package/utils/redirection-iframe.ts +8 -2
  187. package/utils/widget-styles.ts +107 -0
  188. package/views/error-page.tsx +93 -0
  189. package/with-pz-config.js +13 -6
package/api/auth.ts CHANGED
@@ -1,15 +1,27 @@
1
- import { NextApiRequest, NextApiResponse } from 'next';
2
- import NextAuth, { Session } from 'next-auth';
3
- import CredentialProvider from 'next-auth/providers/credentials';
1
+ import NextAuth, { Session, CredentialsSignin } from 'next-auth';
2
+ import Credentials from 'next-auth/providers/credentials';
4
3
  import { ROUTES } from 'routes';
5
4
  import { URLS, user } from '../data/urls';
6
5
  import Settings from 'settings';
7
6
  import { urlLocaleMatcherRegex } from '../utils';
8
7
  import logger from '@akinon/next/utils/log';
9
8
  import { AuthError } from '../types';
9
+ import getRootHostname from '../utils/get-root-hostname';
10
+ import { LocaleUrlStrategy } from '../localization';
11
+ import { cookies, headers } from 'next/headers';
12
+
13
+ import type { NextAuthConfig } from 'next-auth';
14
+
15
+ class PzCredentialsError extends CredentialsSignin {
16
+ code = 'credentials';
17
+ constructor(errors: AuthError[]) {
18
+ super();
19
+ this.code = JSON.stringify(errors);
20
+ }
21
+ }
10
22
 
11
23
  async function getCurrentUser(sessionId: string, currency = '') {
12
- const headers = {
24
+ const reqHeaders = {
13
25
  'Content-Type': 'application/json',
14
26
  Cookie: `osessionid=${sessionId}`,
15
27
  'x-currency': currency
@@ -17,7 +29,7 @@ async function getCurrentUser(sessionId: string, currency = '') {
17
29
 
18
30
  const currentUser = await (
19
31
  await fetch(URLS.user.currentUser, {
20
- headers
32
+ headers: reqHeaders
21
33
  })
22
34
  ).json();
23
35
 
@@ -40,15 +52,18 @@ async function getCurrentUser(sessionId: string, currency = '') {
40
52
  };
41
53
  }
42
54
 
43
- const nextAuthOptions = (req: NextApiRequest, res: NextApiResponse) => {
55
+ type CustomNextAuthOptions = () => Partial<NextAuthConfig>;
56
+
57
+ const getDefaultAuthConfig = (): NextAuthConfig => {
44
58
  return {
45
59
  providers: [
46
- CredentialProvider({
60
+ Credentials({
47
61
  id: 'oauth',
48
62
  name: 'credentials',
49
63
  credentials: {},
50
64
  authorize: async (credentials) => {
51
- const sessionId = req.cookies['osessionid'];
65
+ const cookieStore = await cookies();
66
+ const sessionId = cookieStore.get('osessionid')?.value;
52
67
 
53
68
  if (!sessionId) {
54
69
  return null;
@@ -56,12 +71,12 @@ const nextAuthOptions = (req: NextApiRequest, res: NextApiResponse) => {
56
71
 
57
72
  const currentUser = await getCurrentUser(
58
73
  sessionId,
59
- req.cookies['pz-currency'] ?? ''
74
+ cookieStore.get('pz-currency')?.value ?? ''
60
75
  );
61
76
  return currentUser;
62
77
  }
63
78
  }),
64
- CredentialProvider({
79
+ Credentials({
65
80
  id: 'default',
66
81
  name: 'credentials',
67
82
  credentials: {
@@ -76,32 +91,58 @@ const nextAuthOptions = (req: NextApiRequest, res: NextApiResponse) => {
76
91
  captchaValidated: {}
77
92
  },
78
93
  authorize: async (credentials) => {
79
- const headers: HeadersInit = new Headers();
94
+ const cookieStore = await cookies();
95
+ const headerStore = await headers();
96
+
97
+ const reqHeaders: HeadersInit = new Headers();
80
98
  const language = Settings.localization.locales.find(
81
- (item) => item.value === credentials.locale
99
+ (item) => item.value === (credentials as any).locale
82
100
  ).apiValue;
83
- const userIp = req.headers['x-forwarded-for']?.toString() ?? '';
84
-
85
- headers.set('Content-Type', 'application/json');
86
- headers.set('cookie', `${req.headers.cookie}`);
87
- headers.set('Accept-Language', `${language}`);
88
- headers.set('x-currency', req.cookies['pz-currency'] ?? '');
89
- headers.set('x-forwarded-for', userIp);
90
- headers.set(
101
+ const userIp = headerStore.get('x-forwarded-for') ?? '';
102
+
103
+ reqHeaders.set('Content-Type', 'application/json');
104
+ reqHeaders.set('cookie', headerStore.get('cookie') ?? '');
105
+ reqHeaders.set('Accept-Language', `${language}`);
106
+ reqHeaders.set(
107
+ 'x-currency',
108
+ cookieStore.get('pz-currency')?.value ?? ''
109
+ );
110
+ reqHeaders.set('x-forwarded-for', userIp);
111
+ reqHeaders.set(
91
112
  'x-app-device',
92
- req.headers['x-app-device']?.toString() ?? ''
113
+ headerStore.get('x-app-device') ?? ''
114
+ );
115
+
116
+ reqHeaders.set(
117
+ 'x-frontend-id',
118
+ cookieStore.get('pz-frontend-id')?.value || ''
93
119
  );
94
120
 
95
121
  logger.debug('Trying to login/register', {
96
- formType: credentials.formType,
122
+ formType: (credentials as any).formType,
97
123
  userIp
98
124
  });
99
125
 
126
+ const checkCurrentUser = await getCurrentUser(
127
+ cookieStore.get('osessionid')?.value ?? '',
128
+ cookieStore.get('pz-currency')?.value ?? ''
129
+ );
130
+
131
+ if (checkCurrentUser?.pk) {
132
+ const sessionCookie = reqHeaders
133
+ .get('cookie')
134
+ ?.match(/osessionid=\w+/)?.[0]
135
+ .replace(/osessionid=/, '');
136
+ if (sessionCookie) {
137
+ reqHeaders.set('cookie', sessionCookie);
138
+ }
139
+ }
140
+
100
141
  const apiRequest = await fetch(
101
- `${Settings.commerceUrl}${user[credentials.formType]}`,
142
+ `${Settings.commerceUrl}${user[(credentials as any).formType]}`,
102
143
  {
103
144
  method: 'POST',
104
- headers,
145
+ headers: reqHeaders,
105
146
  body: JSON.stringify(credentials)
106
147
  }
107
148
  );
@@ -133,12 +174,28 @@ const nextAuthOptions = (req: NextApiRequest, res: NextApiResponse) => {
133
174
 
134
175
  if (sessionId) {
135
176
  const maxAge = 30 * 24 * 60 * 60; // 30 days in seconds
136
- const cookieOptions = `Path=/; HttpOnly; Secure; Max-Age=${maxAge}`;
177
+ const { localeUrlStrategy } = Settings.localization;
137
178
 
138
- res.setHeader('Set-Cookie', [
139
- `osessionid=${sessionId}; ${cookieOptions}`,
140
- `sessionid=${sessionId}; ${cookieOptions}` // required to get 3D redirection form
141
- ]);
179
+ const fallbackHost =
180
+ headerStore.get('x-forwarded-host') ||
181
+ headerStore.get('host');
182
+ const hostname =
183
+ process.env.NEXT_PUBLIC_URL || `https://${fallbackHost}`;
184
+ const rootHostname =
185
+ localeUrlStrategy === LocaleUrlStrategy.Subdomain
186
+ ? getRootHostname(hostname)
187
+ : null;
188
+
189
+ const cookieOptions = {
190
+ path: '/',
191
+ httpOnly: true,
192
+ secure: true,
193
+ maxAge,
194
+ ...(rootHostname ? { domain: rootHostname } : {})
195
+ };
196
+
197
+ cookieStore.set('osessionid', sessionId, cookieOptions);
198
+ cookieStore.set('sessionid', sessionId, cookieOptions);
142
199
  }
143
200
 
144
201
  if (!response.key) {
@@ -159,8 +216,8 @@ const nextAuthOptions = (req: NextApiRequest, res: NextApiResponse) => {
159
216
  });
160
217
 
161
218
  logger.debug('Captcha required', {
162
- email: credentials.email,
163
- formType: credentials.formType
219
+ email: (credentials as any).email,
220
+ formType: (credentials as any).formType
164
221
  });
165
222
  } else if (apiRequest.status === 202) {
166
223
  errors.push({
@@ -179,13 +236,13 @@ const nextAuthOptions = (req: NextApiRequest, res: NextApiResponse) => {
179
236
  }
180
237
 
181
238
  if (errors.length) {
182
- throw new Error(JSON.stringify(errors));
239
+ throw new PzCredentialsError(errors);
183
240
  }
184
241
  }
185
242
 
186
243
  const currentUser = await getCurrentUser(
187
244
  sessionId,
188
- req.cookies['pz-currency'] ?? ''
245
+ cookieStore.get('pz-currency')?.value ?? ''
189
246
  );
190
247
  return currentUser;
191
248
  }
@@ -199,18 +256,20 @@ const nextAuthOptions = (req: NextApiRequest, res: NextApiResponse) => {
199
256
 
200
257
  return token;
201
258
  },
202
- async session({ session, user, token }) {
203
- session.user = token.user as Session['user'];
259
+ async session({ session, token }) {
260
+ session.user = token.user as any;
204
261
  return session;
205
262
  },
206
263
  async redirect({ url, baseUrl }: { url: string; baseUrl: string }) {
264
+ const headerStore = await headers();
207
265
  const pathname = url.startsWith('/') ? url : url.replace(baseUrl, '');
208
266
  const pathnameWithoutLocale = pathname.replace(
209
267
  urlLocaleMatcherRegex,
210
268
  ''
211
269
  );
212
270
 
213
- const localeResults = req.headers.referer
271
+ const localeResults = headerStore
272
+ .get('referer')
214
273
  ?.replace(baseUrl, '')
215
274
  ?.match(urlLocaleMatcherRegex);
216
275
 
@@ -221,11 +280,16 @@ const nextAuthOptions = (req: NextApiRequest, res: NextApiResponse) => {
221
280
  signIn: () => {
222
281
  logger.debug('Successfully signed in');
223
282
  },
224
- signOut: () => {
225
- res.setHeader('Set-Cookie', [
226
- `osessionid=; Path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`,
227
- `sessionid=; Path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`
228
- ]);
283
+ signOut: async () => {
284
+ const cookieStore = await cookies();
285
+ cookieStore.set('osessionid', '', {
286
+ path: '/',
287
+ maxAge: 0
288
+ });
289
+ cookieStore.set('sessionid', '', {
290
+ path: '/',
291
+ maxAge: 0
292
+ });
229
293
  logger.debug('Successfully signed out');
230
294
  }
231
295
  },
@@ -236,8 +300,33 @@ const nextAuthOptions = (req: NextApiRequest, res: NextApiResponse) => {
236
300
  };
237
301
  };
238
302
 
239
- const Auth = (req, res) => {
240
- return NextAuth(req, res, nextAuthOptions(req, res));
303
+ export const createAuth = (customOptions?: CustomNextAuthOptions) => {
304
+ const baseConfig = getDefaultAuthConfig();
305
+ const customConfig = customOptions ? customOptions() : {};
306
+
307
+ const mergedConfig: NextAuthConfig = {
308
+ trustHost: true,
309
+ ...baseConfig,
310
+ ...customConfig,
311
+ providers: [
312
+ ...baseConfig.providers,
313
+ ...(customConfig.providers || [])
314
+ ],
315
+ callbacks: {
316
+ ...baseConfig.callbacks,
317
+ ...customConfig.callbacks
318
+ },
319
+ events: {
320
+ ...baseConfig.events,
321
+ ...customConfig.events
322
+ },
323
+ pages: {
324
+ ...baseConfig.pages,
325
+ ...customConfig.pages
326
+ }
327
+ };
328
+
329
+ return NextAuth(mergedConfig);
241
330
  };
242
331
 
243
- export default Auth;
332
+ export default createAuth;
@@ -0,0 +1,59 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import Settings from 'settings';
3
+
4
+ export async function GET(request: NextRequest) {
5
+ try {
6
+ const { searchParams } = new URL(request.url);
7
+ const barcode = searchParams.get('search_text');
8
+
9
+ if (!barcode) {
10
+ return NextResponse.json(
11
+ { error: 'Missing search_text parameter (barcode)' },
12
+ { status: 400 }
13
+ );
14
+ }
15
+
16
+ if (Settings.commerceUrl === 'default') {
17
+ return NextResponse.json(
18
+ { error: 'Commerce URL is not configured' },
19
+ { status: 500 }
20
+ );
21
+ }
22
+
23
+ const queryParams = new URLSearchParams();
24
+ queryParams.append('search_text', barcode);
25
+
26
+ searchParams.forEach((value, key) => {
27
+ if (key !== 'search_text') {
28
+ queryParams.append(key, value);
29
+ }
30
+ });
31
+
32
+ const apiUrl = `${Settings.commerceUrl}/list/?${queryParams.toString()}`;
33
+
34
+ const headers: Record<string, string> = {
35
+ Accept: 'application/json',
36
+ 'Content-Type': 'application/json'
37
+ };
38
+
39
+ const response = await fetch(apiUrl, {
40
+ method: 'GET',
41
+ headers
42
+ });
43
+
44
+ if (!response.ok) {
45
+ return NextResponse.json(
46
+ { error: `API request failed with status: ${response.status}` },
47
+ { status: response.status }
48
+ );
49
+ }
50
+
51
+ const data = await response.json();
52
+ return NextResponse.json(data);
53
+ } catch (error) {
54
+ return NextResponse.json(
55
+ { error: (error as Error).message },
56
+ { status: 500 }
57
+ );
58
+ }
59
+ }
package/api/cache.ts CHANGED
@@ -21,20 +21,56 @@ async function handleRequest(...args) {
21
21
  }
22
22
 
23
23
  const formData = await req.formData();
24
- const body = {} as { key: string; value?: string; expire?: number };
24
+ const body = {} as {
25
+ key: string;
26
+ value?: string;
27
+ expire?: number;
28
+ keyValuePairs?: string;
29
+ compressed?: string;
30
+ };
25
31
 
26
32
  formData.forEach((value, key) => {
27
33
  body[key] = value;
28
34
  });
29
35
 
30
- const { key, value, expire } = body;
31
- let response: string | boolean;
36
+ const { key, value, expire, keyValuePairs, compressed } = body;
37
+ let response: any;
32
38
 
33
39
  try {
34
40
  if (req.method === 'POST') {
35
- response = await Cache.get(key);
41
+ // GET request - check if compressed flag is set
42
+ if (compressed === 'true') {
43
+ response = await Cache.getCompressed(key);
44
+ } else {
45
+ response = await Cache.get(key);
46
+ }
36
47
  } else if (req.method === 'PUT') {
37
- response = await Cache.set(key, value, expire);
48
+ if (keyValuePairs) {
49
+ try {
50
+ const parsedKeyValuePairs = JSON.parse(keyValuePairs);
51
+ if (
52
+ typeof parsedKeyValuePairs !== 'object' ||
53
+ parsedKeyValuePairs === null ||
54
+ Array.isArray(parsedKeyValuePairs)
55
+ ) {
56
+ throw new Error('Invalid keyValuePairs format - must be an object');
57
+ }
58
+ response = await Cache.mset(parsedKeyValuePairs, expire);
59
+ } catch (error) {
60
+ logger.error('Invalid keyValuePairs in mset request', { error });
61
+ return NextResponse.json(
62
+ { error: 'Invalid keyValuePairs format' },
63
+ { status: 400 }
64
+ );
65
+ }
66
+ } else {
67
+ // SET request - check if compressed flag is set
68
+ if (compressed === 'true') {
69
+ response = await Cache.setCompressed(key, value, expire);
70
+ } else {
71
+ response = await Cache.set(key, value, expire);
72
+ }
73
+ }
38
74
  }
39
75
  } catch (error) {
40
76
  logger.error(error);
package/api/client.ts CHANGED
@@ -5,6 +5,8 @@ import logger from '../utils/log';
5
5
  import formatCookieString from '../utils/format-cookie-string';
6
6
  import cookieParser from 'set-cookie-parser';
7
7
  import { cookies } from 'next/headers';
8
+ import getRootHostname from '../utils/get-root-hostname';
9
+ import { LocaleUrlStrategy } from '../localization';
8
10
 
9
11
  interface RouteParams {
10
12
  params: {
@@ -13,8 +15,8 @@ interface RouteParams {
13
15
  }
14
16
 
15
17
  async function proxyRequest(...args) {
16
- const [req, { params }] = args as [req: Request, params: RouteParams];
17
- const resolvedParams = await params;
18
+ const [req, routeContext] = args as [req: Request, { params: Promise<RouteParams['params']> }];
19
+ const params = await routeContext.params;
18
20
  const { searchParams } = new URL(req.url);
19
21
  const commerceUrl = settings.commerceUrl;
20
22
 
@@ -33,7 +35,7 @@ async function proxyRequest(...args) {
33
35
  responseType: 'json'
34
36
  };
35
37
 
36
- const slug = `${resolvedParams.slug.join('/')}/`;
38
+ const slug = `${params.slug.join('/')}/`;
37
39
  const options_ = JSON.parse(
38
40
  decodeURIComponent((searchParams.get('options') as string) ?? '{}')
39
41
  );
@@ -191,8 +193,23 @@ async function proxyRequest(...args) {
191
193
  const responseHeaders: any = {};
192
194
 
193
195
  if (filteredCookies.length > 0) {
196
+ const { localeUrlStrategy } = settings.localization;
197
+
198
+ const fallbackHost =
199
+ req.headers.get('x-forwarded-host') || req.headers.get('host');
200
+ const hostname = process.env.NEXT_PUBLIC_URL || `https://${fallbackHost}`;
201
+ const rootHostname =
202
+ localeUrlStrategy === LocaleUrlStrategy.Subdomain
203
+ ? getRootHostname(hostname)
204
+ : null;
205
+
194
206
  responseHeaders['set-cookie'] = filteredCookies
195
- .map(formatCookieString)
207
+ .map((cookie) => {
208
+ if (!cookie.domain && rootHostname) {
209
+ cookie.domain = rootHostname;
210
+ }
211
+ return formatCookieString(cookie);
212
+ })
196
213
  .join(', ');
197
214
  }
198
215
 
package/api/form.ts ADDED
@@ -0,0 +1,85 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { getFormData } from '@akinon/next/data/server';
3
+
4
+ interface FormRouteParams {
5
+ params: Promise<{
6
+ id: string[];
7
+ }>;
8
+ }
9
+
10
+ interface FormConfig {
11
+ pk: number;
12
+ is_active: boolean;
13
+ }
14
+
15
+ type SubmissionData = Record<string, string | File>;
16
+
17
+ export async function POST(request: NextRequest, { params }: FormRouteParams) {
18
+ try {
19
+ const { id } = await params;
20
+ const formId = id?.[0];
21
+
22
+ if (!formId || isNaN(Number(formId))) {
23
+ return NextResponse.json(
24
+ { error: 'Valid form ID is required' },
25
+ { status: 400 }
26
+ );
27
+ }
28
+
29
+ const formConfig: FormConfig | null = await getFormData({
30
+ pk: Number(formId)
31
+ });
32
+
33
+ if (!formConfig || !formConfig.is_active) {
34
+ return NextResponse.json(
35
+ { error: 'Form not found or inactive' },
36
+ { status: 404 }
37
+ );
38
+ }
39
+
40
+ const formData = await request.formData();
41
+ const submissionData: SubmissionData = Object.fromEntries(
42
+ formData.entries()
43
+ );
44
+
45
+ return await processFormSubmission(formConfig, submissionData);
46
+ } catch (error) {
47
+ console.error('Form submission error:', error);
48
+
49
+ return NextResponse.json(
50
+ { error: 'Internal server error' },
51
+ { status: 500 }
52
+ );
53
+ }
54
+ }
55
+
56
+ async function processFormSubmission(
57
+ formConfig: FormConfig,
58
+ data: SubmissionData
59
+ ) {
60
+ const backendUrl = process.env.SERVICE_BACKEND_URL;
61
+ if (!backendUrl) {
62
+ throw new Error('SERVICE_BACKEND_URL is not defined');
63
+ }
64
+
65
+ const url = `${backendUrl}/forms/${formConfig.pk}/generate`;
66
+
67
+ const submissionPayload = {
68
+ ...data
69
+ };
70
+
71
+ const response = await fetch(url, {
72
+ method: 'POST',
73
+ headers: { 'Content-Type': 'application/json' },
74
+ body: JSON.stringify(submissionPayload)
75
+ });
76
+
77
+ const body = await response.text();
78
+
79
+ return new NextResponse(body, {
80
+ status: response.status,
81
+ headers: {
82
+ 'Content-Type': response.headers.get('content-type') ?? 'application/json'
83
+ }
84
+ });
85
+ }
@@ -0,0 +1,75 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+
3
+ async function proxyImage(imageUrl: string) {
4
+ if (!imageUrl) {
5
+ return NextResponse.json(
6
+ { success: false, error: 'Missing url parameter' },
7
+ { status: 400 }
8
+ );
9
+ }
10
+
11
+ const imageResponse = await fetch(imageUrl);
12
+
13
+ if (!imageResponse.ok) {
14
+ return NextResponse.json(
15
+ { success: false, error: 'Failed to fetch image from URL' },
16
+ { status: 400 }
17
+ );
18
+ }
19
+
20
+ const imageBuffer = await imageResponse.arrayBuffer();
21
+ const base64Image = Buffer.from(imageBuffer).toString('base64');
22
+ const contentType = imageResponse.headers.get('content-type') || 'image/jpeg';
23
+
24
+ return { base64Image, contentType, imageBuffer };
25
+ }
26
+
27
+ export async function GET(request: NextRequest) {
28
+ try {
29
+ const { searchParams } = new URL(request.url);
30
+ const imageUrl = searchParams.get('url');
31
+
32
+ const result = await proxyImage(imageUrl!);
33
+ if (result instanceof NextResponse) {
34
+ return result;
35
+ }
36
+
37
+ return new NextResponse(result.imageBuffer, {
38
+ status: 200,
39
+ headers: {
40
+ 'Content-Type': result.contentType,
41
+ 'Access-Control-Allow-Origin': '*',
42
+ 'Cache-Control': 'public, max-age=3600'
43
+ }
44
+ });
45
+ } catch (error) {
46
+ console.error('Image proxy error:', error);
47
+ return NextResponse.json(
48
+ { success: false, error: 'Internal server error' },
49
+ { status: 500 }
50
+ );
51
+ }
52
+ }
53
+
54
+ export async function POST(request: NextRequest) {
55
+ try {
56
+ const body = await request.json();
57
+ const imageUrl = body.imageUrl;
58
+
59
+ const result = await proxyImage(imageUrl);
60
+ if (result instanceof NextResponse) {
61
+ return result;
62
+ }
63
+
64
+ return NextResponse.json({
65
+ success: true,
66
+ base64Image: `data:${result.contentType};base64,${result.base64Image}`
67
+ });
68
+ } catch (error) {
69
+ console.error('Image proxy POST error:', error);
70
+ return NextResponse.json(
71
+ { success: false, error: 'Internal server error' },
72
+ { status: 500 }
73
+ );
74
+ }
75
+ }
@@ -0,0 +1,53 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { getProductData } from '@akinon/next/data/server';
3
+
4
+ export async function GET(request: NextRequest) {
5
+ try {
6
+ const { searchParams } = new URL(request.url);
7
+ const pksParam = searchParams.get('pks');
8
+
9
+ if (!pksParam) {
10
+ return NextResponse.json(
11
+ { error: 'pks parameter required' },
12
+ { status: 400 }
13
+ );
14
+ }
15
+
16
+ const pks = pksParam.split(',').map(Number).filter(Boolean);
17
+
18
+ if (pks.length === 0) {
19
+ return NextResponse.json({ error: 'Invalid pks' }, { status: 400 });
20
+ }
21
+
22
+ const results = await Promise.all(
23
+ pks.map(async (pk) => {
24
+ try {
25
+ const { breadcrumbData } = await getProductData({ pk });
26
+
27
+ const categoryIds =
28
+ breadcrumbData
29
+ ?.map((item: any) => item.extra_context?.attributes?.category_id)
30
+ .filter(Boolean) || [];
31
+
32
+ return { pk, categoryIds };
33
+ } catch (error) {
34
+ console.error(`Error fetching product ${pk}:`, error);
35
+ return { pk, categoryIds: [] };
36
+ }
37
+ })
38
+ );
39
+
40
+ const mapping: Record<string, number[]> = {};
41
+ results.forEach(({ pk, categoryIds }) => {
42
+ mapping[String(pk)] = categoryIds;
43
+ });
44
+
45
+ return NextResponse.json(mapping);
46
+ } catch (error) {
47
+ console.error('Error in product-categories API:', error);
48
+ return NextResponse.json(
49
+ { error: 'Internal server error' },
50
+ { status: 500 }
51
+ );
52
+ }
53
+ }