@akinon/next 2.0.0-beta.12 → 2.0.0-beta.14

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 (93) hide show
  1. package/CHANGELOG.md +290 -29
  2. package/api/auth.ts +99 -77
  3. package/api/cache.ts +41 -5
  4. package/api/client.ts +3 -3
  5. package/api/form.ts +85 -0
  6. package/api/image-proxy.ts +75 -0
  7. package/api/product-categories.ts +53 -0
  8. package/api/similar-product-list.ts +63 -0
  9. package/api/similar-products.ts +111 -0
  10. package/api/virtual-try-on.ts +382 -0
  11. package/bin/pz-generate-routes.js +105 -0
  12. package/bin/pz-prebuild.js +1 -1
  13. package/bin/pz-predev.js +1 -0
  14. package/components/accordion.tsx +21 -6
  15. package/components/button.tsx +1 -1
  16. package/components/file-input.tsx +65 -3
  17. package/components/input.tsx +2 -2
  18. package/components/modal.tsx +32 -16
  19. package/components/plugin-module.tsx +61 -3
  20. package/components/select.tsx +2 -2
  21. package/components/selected-payment-option-view.tsx +21 -0
  22. package/data/client/checkout.ts +130 -74
  23. package/data/server/category.ts +11 -9
  24. package/data/server/flatpage.ts +4 -1
  25. package/data/server/form.ts +4 -1
  26. package/data/server/landingpage.ts +4 -1
  27. package/data/server/list.ts +5 -4
  28. package/data/server/menu.ts +4 -1
  29. package/data/server/product.ts +97 -52
  30. package/data/server/seo.ts +4 -1
  31. package/data/server/special-page.ts +5 -4
  32. package/data/server/widget.ts +4 -1
  33. package/data/urls.ts +3 -2
  34. package/hocs/client/with-segment-defaults.tsx +2 -2
  35. package/hocs/server/with-segment-defaults.tsx +65 -20
  36. package/hooks/index.ts +1 -0
  37. package/hooks/use-loyalty-availability.ts +21 -0
  38. package/hooks/use-payment-options.ts +2 -1
  39. package/hooks/use-pz-params.ts +37 -0
  40. package/instrumentation/index.ts +0 -1
  41. package/instrumentation/node.ts +2 -20
  42. package/jest.config.js +7 -1
  43. package/lib/cache-handler.mjs +534 -16
  44. package/lib/cache.ts +268 -34
  45. package/localization/provider.tsx +2 -5
  46. package/middlewares/checkout-provider.ts +1 -1
  47. package/middlewares/complete-gpay.ts +32 -26
  48. package/middlewares/complete-masterpass.ts +33 -26
  49. package/middlewares/complete-wallet.ts +182 -0
  50. package/middlewares/default.ts +346 -235
  51. package/middlewares/index.ts +8 -2
  52. package/middlewares/locale.ts +0 -1
  53. package/middlewares/masterpass-rest-callback.ts +230 -0
  54. package/middlewares/pretty-url.ts +21 -8
  55. package/middlewares/redirection-payment.ts +32 -26
  56. package/middlewares/saved-card-redirection.ts +33 -26
  57. package/middlewares/three-d-redirection.ts +32 -26
  58. package/middlewares/url-redirection.ts +9 -15
  59. package/middlewares/wallet-complete-redirection.ts +206 -0
  60. package/package.json +20 -11
  61. package/plugins.d.ts +19 -4
  62. package/plugins.js +9 -1
  63. package/redux/actions.ts +47 -0
  64. package/redux/middlewares/checkout.ts +20 -8
  65. package/redux/middlewares/index.ts +12 -10
  66. package/redux/middlewares/pre-order/address.ts +1 -1
  67. package/redux/middlewares/pre-order/attribute-based-shipping-option.ts +1 -1
  68. package/redux/middlewares/pre-order/data-source-shipping-option.ts +1 -1
  69. package/redux/middlewares/pre-order/delivery-option.ts +1 -1
  70. package/redux/middlewares/pre-order/index.ts +3 -1
  71. package/redux/middlewares/pre-order/installment-option.ts +2 -1
  72. package/redux/middlewares/pre-order/payment-option-reset.ts +37 -0
  73. package/redux/middlewares/pre-order/payment-option.ts +1 -1
  74. package/redux/middlewares/pre-order/pre-order-validation.ts +4 -3
  75. package/redux/middlewares/pre-order/redirection.ts +2 -2
  76. package/redux/middlewares/pre-order/set-pre-order.ts +2 -2
  77. package/redux/middlewares/pre-order/shipping-option.ts +1 -1
  78. package/redux/middlewares/pre-order/shipping-step.ts +1 -1
  79. package/redux/reducers/checkout.ts +9 -1
  80. package/redux/reducers/index.ts +5 -1
  81. package/sentry/index.ts +54 -17
  82. package/types/commerce/checkout.ts +11 -1
  83. package/types/index.ts +96 -6
  84. package/types/next-auth.d.ts +2 -2
  85. package/utils/app-fetch.ts +2 -2
  86. package/utils/generate-commerce-search-params.ts +3 -2
  87. package/utils/get-checkout-path.ts +3 -0
  88. package/utils/index.ts +61 -11
  89. package/utils/override-middleware.ts +1 -0
  90. package/utils/pz-segments.ts +92 -0
  91. package/utils/redirect-ignore.ts +35 -0
  92. package/utils/redirect.ts +9 -3
  93. package/with-pz-config.js +10 -4
package/api/auth.ts CHANGED
@@ -1,6 +1,5 @@
1
- import { NextApiRequest, NextApiResponse } from 'next';
2
- import NextAuth, { Session, NextAuthOptions } 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';
@@ -9,9 +8,20 @@ import logger from '@akinon/next/utils/log';
9
8
  import { AuthError } from '../types';
10
9
  import getRootHostname from '../utils/get-root-hostname';
11
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
+ }
12
22
 
13
23
  async function getCurrentUser(sessionId: string, currency = '') {
14
- const headers = {
24
+ const reqHeaders = {
15
25
  'Content-Type': 'application/json',
16
26
  Cookie: `osessionid=${sessionId}`,
17
27
  'x-currency': currency
@@ -19,7 +29,7 @@ async function getCurrentUser(sessionId: string, currency = '') {
19
29
 
20
30
  const currentUser = await (
21
31
  await fetch(URLS.user.currentUser, {
22
- headers
32
+ headers: reqHeaders
23
33
  })
24
34
  ).json();
25
35
 
@@ -42,23 +52,18 @@ async function getCurrentUser(sessionId: string, currency = '') {
42
52
  };
43
53
  }
44
54
 
45
- type CustomNextAuthOptions = (
46
- req: NextApiRequest,
47
- res: NextApiResponse
48
- ) => Partial<NextAuthOptions>;
55
+ type CustomNextAuthOptions = () => Partial<NextAuthConfig>;
49
56
 
50
- const defaultNextAuthOptions = (
51
- req: NextApiRequest,
52
- res: NextApiResponse
53
- ): NextAuthOptions => {
57
+ const getDefaultAuthConfig = (): NextAuthConfig => {
54
58
  return {
55
59
  providers: [
56
- CredentialProvider({
60
+ Credentials({
57
61
  id: 'oauth',
58
62
  name: 'credentials',
59
63
  credentials: {},
60
64
  authorize: async (credentials) => {
61
- const sessionId = req.cookies['osessionid'];
65
+ const cookieStore = await cookies();
66
+ const sessionId = cookieStore.get('osessionid')?.value;
62
67
 
63
68
  if (!sessionId) {
64
69
  return null;
@@ -66,12 +71,12 @@ const defaultNextAuthOptions = (
66
71
 
67
72
  const currentUser = await getCurrentUser(
68
73
  sessionId,
69
- req.cookies['pz-currency'] ?? ''
74
+ cookieStore.get('pz-currency')?.value ?? ''
70
75
  );
71
76
  return currentUser;
72
77
  }
73
78
  }),
74
- CredentialProvider({
79
+ Credentials({
75
80
  id: 'default',
76
81
  name: 'credentials',
77
82
  credentials: {
@@ -86,49 +91,58 @@ const defaultNextAuthOptions = (
86
91
  captchaValidated: {}
87
92
  },
88
93
  authorize: async (credentials) => {
89
- const headers: HeadersInit = new Headers();
94
+ const cookieStore = await cookies();
95
+ const headerStore = await headers();
96
+
97
+ const reqHeaders: HeadersInit = new Headers();
90
98
  const language = Settings.localization.locales.find(
91
- (item) => item.value === credentials.locale
99
+ (item) => item.value === (credentials as any).locale
92
100
  ).apiValue;
93
- const userIp = req.headers['x-forwarded-for']?.toString() ?? '';
94
-
95
- headers.set('Content-Type', 'application/json');
96
- headers.set('cookie', `${req.headers.cookie}`);
97
- headers.set('Accept-Language', `${language}`);
98
- headers.set('x-currency', req.cookies['pz-currency'] ?? '');
99
- headers.set('x-forwarded-for', userIp);
100
- 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(
101
112
  'x-app-device',
102
- req.headers['x-app-device']?.toString() ?? ''
113
+ headerStore.get('x-app-device') ?? ''
103
114
  );
104
115
 
105
- headers.set('x-frontend-id', req.cookies['pz-frontend-id'] || '');
116
+ reqHeaders.set(
117
+ 'x-frontend-id',
118
+ cookieStore.get('pz-frontend-id')?.value || ''
119
+ );
106
120
 
107
121
  logger.debug('Trying to login/register', {
108
- formType: credentials.formType,
122
+ formType: (credentials as any).formType,
109
123
  userIp
110
124
  });
111
125
 
112
126
  const checkCurrentUser = await getCurrentUser(
113
- req.cookies['osessionid'] ?? '',
114
- req.cookies['pz-currency'] ?? ''
127
+ cookieStore.get('osessionid')?.value ?? '',
128
+ cookieStore.get('pz-currency')?.value ?? ''
115
129
  );
116
130
 
117
131
  if (checkCurrentUser?.pk) {
118
- const sessionCookie = headers
132
+ const sessionCookie = reqHeaders
119
133
  .get('cookie')
120
134
  ?.match(/osessionid=\w+/)?.[0]
121
135
  .replace(/osessionid=/, '');
122
136
  if (sessionCookie) {
123
- headers.set('cookie', sessionCookie);
137
+ reqHeaders.set('cookie', sessionCookie);
124
138
  }
125
139
  }
126
140
 
127
141
  const apiRequest = await fetch(
128
- `${Settings.commerceUrl}${user[credentials.formType]}`,
142
+ `${Settings.commerceUrl}${user[(credentials as any).formType]}`,
129
143
  {
130
144
  method: 'POST',
131
- headers,
145
+ headers: reqHeaders,
132
146
  body: JSON.stringify(credentials)
133
147
  }
134
148
  );
@@ -163,21 +177,25 @@ const defaultNextAuthOptions = (
163
177
  const { localeUrlStrategy } = Settings.localization;
164
178
 
165
179
  const fallbackHost =
166
- req.headers['x-forwarded-host']?.toString() ||
167
- req.headers.host?.toString();
180
+ headerStore.get('x-forwarded-host') ||
181
+ headerStore.get('host');
168
182
  const hostname =
169
183
  process.env.NEXT_PUBLIC_URL || `https://${fallbackHost}`;
170
184
  const rootHostname =
171
185
  localeUrlStrategy === LocaleUrlStrategy.Subdomain
172
186
  ? getRootHostname(hostname)
173
187
  : null;
174
- const domainOption = rootHostname ? `Domain=${rootHostname};` : '';
175
- const cookieOptions = `Path=/; HttpOnly; Secure; Max-Age=${maxAge}; ${domainOption}`;
176
188
 
177
- res.setHeader('Set-Cookie', [
178
- `osessionid=${sessionId}; ${cookieOptions}`,
179
- `sessionid=${sessionId}; ${cookieOptions}` // required to get 3D redirection form
180
- ]);
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);
181
199
  }
182
200
 
183
201
  if (!response.key) {
@@ -198,8 +216,8 @@ const defaultNextAuthOptions = (
198
216
  });
199
217
 
200
218
  logger.debug('Captcha required', {
201
- email: credentials.email,
202
- formType: credentials.formType
219
+ email: (credentials as any).email,
220
+ formType: (credentials as any).formType
203
221
  });
204
222
  } else if (apiRequest.status === 202) {
205
223
  errors.push({
@@ -218,13 +236,13 @@ const defaultNextAuthOptions = (
218
236
  }
219
237
 
220
238
  if (errors.length) {
221
- throw new Error(JSON.stringify(errors));
239
+ throw new PzCredentialsError(errors);
222
240
  }
223
241
  }
224
242
 
225
243
  const currentUser = await getCurrentUser(
226
244
  sessionId,
227
- req.cookies['pz-currency'] ?? ''
245
+ cookieStore.get('pz-currency')?.value ?? ''
228
246
  );
229
247
  return currentUser;
230
248
  }
@@ -238,18 +256,20 @@ const defaultNextAuthOptions = (
238
256
 
239
257
  return token;
240
258
  },
241
- async session({ session, user, token }) {
242
- session.user = token.user as Session['user'];
259
+ async session({ session, token }) {
260
+ session.user = token.user as any;
243
261
  return session;
244
262
  },
245
263
  async redirect({ url, baseUrl }: { url: string; baseUrl: string }) {
264
+ const headerStore = await headers();
246
265
  const pathname = url.startsWith('/') ? url : url.replace(baseUrl, '');
247
266
  const pathnameWithoutLocale = pathname.replace(
248
267
  urlLocaleMatcherRegex,
249
268
  ''
250
269
  );
251
270
 
252
- const localeResults = req.headers.referer
271
+ const localeResults = headerStore
272
+ .get('referer')
253
273
  ?.replace(baseUrl, '')
254
274
  ?.match(urlLocaleMatcherRegex);
255
275
 
@@ -260,11 +280,16 @@ const defaultNextAuthOptions = (
260
280
  signIn: () => {
261
281
  logger.debug('Successfully signed in');
262
282
  },
263
- signOut: () => {
264
- res.setHeader('Set-Cookie', [
265
- `osessionid=; Path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`,
266
- `sessionid=; Path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`
267
- ]);
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
+ });
268
293
  logger.debug('Successfully signed out');
269
294
  }
270
295
  },
@@ -275,36 +300,33 @@ const defaultNextAuthOptions = (
275
300
  };
276
301
  };
277
302
 
278
- const Auth = (
279
- req: NextApiRequest,
280
- res: NextApiResponse,
281
- customOptions?: CustomNextAuthOptions
282
- ) => {
283
- const baseOptions = defaultNextAuthOptions(req, res);
284
- const customOptionsResult = customOptions ? customOptions(req, res) : {};
285
-
286
- const mergedOptions = {
287
- ...baseOptions,
288
- ...customOptionsResult,
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,
289
311
  providers: [
290
- ...baseOptions.providers,
291
- ...(customOptionsResult.providers || [])
312
+ ...baseConfig.providers,
313
+ ...(customConfig.providers || [])
292
314
  ],
293
315
  callbacks: {
294
- ...baseOptions.callbacks,
295
- ...customOptionsResult.callbacks
316
+ ...baseConfig.callbacks,
317
+ ...customConfig.callbacks
296
318
  },
297
319
  events: {
298
- ...baseOptions.events,
299
- ...customOptionsResult.events
320
+ ...baseConfig.events,
321
+ ...customConfig.events
300
322
  },
301
323
  pages: {
302
- ...baseOptions.pages,
303
- ...customOptionsResult.pages
324
+ ...baseConfig.pages,
325
+ ...customConfig.pages
304
326
  }
305
327
  };
306
328
 
307
- return NextAuth(req, res, mergedOptions);
329
+ return NextAuth(mergedConfig);
308
330
  };
309
331
 
310
- export default Auth;
332
+ export default createAuth;
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
@@ -15,8 +15,8 @@ interface RouteParams {
15
15
  }
16
16
 
17
17
  async function proxyRequest(...args) {
18
- const [req, { params }] = args as [req: Request, params: RouteParams];
19
- const resolvedParams = await params;
18
+ const [req, routeContext] = args as [req: Request, { params: Promise<RouteParams['params']> }];
19
+ const params = await routeContext.params;
20
20
  const { searchParams } = new URL(req.url);
21
21
  const commerceUrl = settings.commerceUrl;
22
22
 
@@ -35,7 +35,7 @@ async function proxyRequest(...args) {
35
35
  responseType: 'json'
36
36
  };
37
37
 
38
- const slug = `${resolvedParams.slug.join('/')}/`;
38
+ const slug = `${params.slug.join('/')}/`;
39
39
  const options_ = JSON.parse(
40
40
  decodeURIComponent((searchParams.get('options') as string) ?? '{}')
41
41
  );
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
+ }
@@ -0,0 +1,63 @@
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 dynamicFilter = request.headers.get('x-search-dynamic-filter');
8
+ const dynamicExclude = request.headers.get('x-search-dynamic-exclude');
9
+
10
+ if (!dynamicFilter && !dynamicExclude) {
11
+ return NextResponse.json(
12
+ {
13
+ error:
14
+ 'Missing x-search-dynamic-filter or x-search-dynamic-exclude header'
15
+ },
16
+ { status: 400 }
17
+ );
18
+ }
19
+
20
+ if (Settings.commerceUrl === 'default') {
21
+ return NextResponse.json(
22
+ { error: 'Commerce URL is not configured' },
23
+ { status: 500 }
24
+ );
25
+ }
26
+
27
+ const queryString = searchParams.toString();
28
+ const apiUrl = `${Settings.commerceUrl}/list${
29
+ queryString ? `?${queryString}` : ''
30
+ }`;
31
+
32
+ const headers: Record<string, string> = {
33
+ Accept: 'application/json',
34
+ 'Content-Type': 'application/json'
35
+ };
36
+
37
+ if (dynamicFilter) {
38
+ headers['x-search-dynamic-filter'] = dynamicFilter;
39
+ } else if (dynamicExclude) {
40
+ headers['x-search-dynamic-exclude'] = dynamicExclude;
41
+ }
42
+
43
+ const response = await fetch(apiUrl, {
44
+ method: 'GET',
45
+ headers
46
+ });
47
+
48
+ if (!response.ok) {
49
+ return NextResponse.json(
50
+ { error: `API request failed with status: ${response.status}` },
51
+ { status: response.status }
52
+ );
53
+ }
54
+
55
+ const data = await response.json();
56
+ return NextResponse.json(data);
57
+ } catch (error) {
58
+ return NextResponse.json(
59
+ { error: (error as Error).message },
60
+ { status: 500 }
61
+ );
62
+ }
63
+ }