@akinon/next 2.0.0-beta.16 → 2.0.0-beta.18

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.
@@ -6,78 +6,221 @@ import {
6
6
  } from 'next/server';
7
7
  import Settings from 'settings';
8
8
  import { getUrlPathWithLocale } from '../utils/localization';
9
+ import logger from '../utils/log';
9
10
 
10
- const withOauthLogin =
11
- (middleware: NextMiddleware) =>
12
- async (req: NextRequest, event: NextFetchEvent) => {
13
- const url = req.nextUrl.clone();
14
- const loginUrlMatcherRegex = new RegExp(/^\/(\w+)\/login\/?$/);
15
- const loginCallbackUrlMatcherRegex = new RegExp(
16
- /^\/(\w+)\/login\/callback\/?$/
17
- );
18
- const ip = req.headers.get('x-forwarded-for') ?? '';
19
-
20
- const headers = {
21
- 'x-forwarded-host':
22
- req.headers.get('x-forwarded-host') || req.headers.get('host') || '',
23
- 'x-currency': req.cookies.get('pz-currency')?.value ?? '',
24
- 'x-forwarded-for': ip
11
+ const LOGIN_URL_REGEX = /^\/(\w+)\/login\/?$/;
12
+ const CALLBACK_URL_REGEX = /^\/(\w+)\/login\/callback\/?$/;
13
+
14
+ function buildCommerceHeaders(req: NextRequest): Record<string, string> {
15
+ return {
16
+ 'x-forwarded-host':
17
+ req.headers.get('x-forwarded-host') || req.headers.get('host') || '',
18
+ 'x-forwarded-for': req.headers.get('x-forwarded-for') ?? '',
19
+ 'x-forwarded-proto': req.headers.get('x-forwarded-proto') || 'https',
20
+ 'x-currency': req.cookies.get('pz-currency')?.value ?? '',
21
+ cookie: req.headers.get('cookie') ?? ''
22
+ };
23
+ }
24
+
25
+ async function getRequestBody(
26
+ req: NextRequest
27
+ ): Promise<{ content: string; contentType: string } | undefined> {
28
+ if (req.method !== 'POST') return undefined;
29
+
30
+ const content = await req.text();
31
+ if (!content.length) return undefined;
32
+
33
+ return {
34
+ content,
35
+ contentType:
36
+ req.headers.get('content-type') ?? 'application/x-www-form-urlencoded'
37
+ };
38
+ }
39
+
40
+ function fetchCommerce(
41
+ url: string,
42
+ req: NextRequest,
43
+ body?: { content: string; contentType: string }
44
+ ): Promise<Response> {
45
+ const headers = buildCommerceHeaders(req);
46
+
47
+ if (body) {
48
+ headers['content-type'] = body.contentType;
49
+ }
50
+
51
+ return fetch(url, {
52
+ method: body ? 'POST' : 'GET',
53
+ headers,
54
+ body: body?.content,
55
+ redirect: 'manual'
56
+ });
57
+ }
58
+
59
+ function forwardCookies(from: Response, to: NextResponse): void {
60
+ from.headers.getSetCookie().forEach((cookie) => {
61
+ to.headers.append('set-cookie', cookie);
62
+ });
63
+ }
64
+
65
+ function buildRedirectResponse(
66
+ commerceResponse: Response,
67
+ location: string,
68
+ origin: string
69
+ ): NextResponse {
70
+ const response = NextResponse.redirect(new URL(location, origin));
71
+ forwardCookies(commerceResponse, response);
72
+ return response;
73
+ }
74
+
75
+ function commercePassthrough(commerceResponse: Response): NextResponse {
76
+ return new NextResponse(commerceResponse.body, {
77
+ status: commerceResponse.status,
78
+ headers: commerceResponse.headers
79
+ });
80
+ }
81
+
82
+ function buildOAuthCallbackCookie(referer: string): string {
83
+ return `pz-oauth-callback-url=${encodeURIComponent(referer)}; Path=/`;
84
+ }
85
+
86
+ async function handleLogin(
87
+ req: NextRequest,
88
+ provider: string
89
+ ): Promise<{ response: NextResponse; redirected: boolean }> {
90
+ const commerceResponse = await fetchCommerce(
91
+ `${Settings.commerceUrl}/${provider}/login/`,
92
+ req
93
+ );
94
+
95
+ const location = commerceResponse.headers.get('location');
96
+ if (!location) {
97
+ return {
98
+ response: commercePassthrough(commerceResponse),
99
+ redirected: false
25
100
  };
101
+ }
26
102
 
27
- if (loginUrlMatcherRegex.test(url.pathname)) {
28
- const provider = url.pathname.match(loginUrlMatcherRegex)[1];
29
- const response = NextResponse.rewrite(
30
- `${Settings.commerceUrl}/${provider}/login/`,
31
- {
32
- headers
33
- }
34
- );
103
+ const response = buildRedirectResponse(
104
+ commerceResponse,
105
+ location,
106
+ req.nextUrl.origin
107
+ );
35
108
 
36
- if (req.headers.get('referer')) {
37
- response.cookies.set(
38
- 'pz-oauth-callback-url',
39
- req.headers.get('referer')
40
- );
41
- }
109
+ response.headers.append(
110
+ 'set-cookie',
111
+ buildOAuthCallbackCookie(req.headers.get('referer') || '')
112
+ );
42
113
 
43
- return response;
44
- }
114
+ return { response, redirected: true };
115
+ }
45
116
 
46
- if (loginCallbackUrlMatcherRegex.test(url.pathname)) {
47
- const provider = url.pathname.match(loginCallbackUrlMatcherRegex)[1];
117
+ async function handleCallback(
118
+ req: NextRequest,
119
+ provider: string,
120
+ search: string
121
+ ): Promise<{ response: NextResponse; redirected: boolean }> {
122
+ const body = await getRequestBody(req);
123
+ const commerceResponse = await fetchCommerce(
124
+ `${Settings.commerceUrl}/${provider}/login/callback/${search}`,
125
+ req,
126
+ body
127
+ );
48
128
 
49
- return NextResponse.rewrite(
50
- `${Settings.commerceUrl}/${provider}/login/callback/${url.search}`,
51
- {
52
- headers
53
- }
54
- );
55
- }
129
+ const location = commerceResponse.headers.get('location');
130
+ if (!location) {
131
+ return {
132
+ response: commercePassthrough(commerceResponse),
133
+ redirected: false
134
+ };
135
+ }
136
+
137
+ return {
138
+ response: buildRedirectResponse(
139
+ commerceResponse,
140
+ location,
141
+ req.nextUrl.origin
142
+ ),
143
+ redirected: true
144
+ };
145
+ }
146
+
147
+ function handleBasketRedirect(req: NextRequest): NextResponse | null {
148
+ const hasSession = req.cookies.get('osessionid');
149
+ const messages = req.cookies.get('messages')?.value;
150
+
151
+ if (!messages) return null;
152
+ if (!messages.includes('Successfully signed in') && !hasSession) return null;
153
+
154
+ let redirectUrl = `${req.nextUrl.origin}${getUrlPathWithLocale(
155
+ '/auth/oauth-login',
156
+ req.cookies.get('pz-locale')?.value
157
+ )}`;
158
+
159
+ const callbackUrl = req.cookies.get('pz-oauth-callback-url')?.value ?? '';
160
+ if (callbackUrl.length) {
161
+ redirectUrl += `?next=${encodeURIComponent(callbackUrl)}`;
162
+ }
163
+
164
+ const response = NextResponse.redirect(redirectUrl);
165
+ response.cookies.delete('messages');
166
+ response.cookies.delete('pz-oauth-callback-url');
167
+ return response;
168
+ }
56
169
 
57
- if (!url.pathname.startsWith('/baskets/basket')) {
170
+ const withOauthLogin =
171
+ (middleware: NextMiddleware) =>
172
+ async (req: NextRequest, event: NextFetchEvent) => {
173
+ const { pathname, search } = req.nextUrl;
174
+
175
+ if (!pathname.includes('/login') && !pathname.startsWith('/baskets/basket')) {
58
176
  return middleware(req, event);
59
177
  }
60
178
 
61
- const currentSessionId = req.cookies.get('osessionid');
179
+ logger.info('OAuth login redirect', {
180
+ host: req.headers.get('host'),
181
+ 'x-forwarded-host': req.headers.get('x-forwarded-host'),
182
+ 'x-forwarded-for': req.headers.get('x-forwarded-for')
183
+ });
62
184
 
63
- if (
64
- req.cookies.get('messages')?.value.includes('Successfully signed in') ||
65
- (currentSessionId && req.cookies.get('messages'))
66
- ) {
67
- let redirectUrlWithLocale = `${url.origin}${getUrlPathWithLocale(
68
- '/auth/oauth-login',
69
- req.cookies.get('pz-locale')?.value
70
- )}`;
71
- let callbackUrl = req.cookies.get('pz-oauth-callback-url')?.value ?? '';
185
+ const loginMatch = LOGIN_URL_REGEX.exec(pathname);
186
+ if (loginMatch) {
187
+ try {
188
+ const { response, redirected } = await handleLogin(req, loginMatch[1]);
189
+ if (!redirected) {
190
+ logger.warn('OAuth login: no redirect from commerce', {
191
+ provider: loginMatch[1]
192
+ });
193
+ }
194
+ return response;
195
+ } catch (error) {
196
+ logger.error('OAuth login fetch failed', { error });
197
+ return middleware(req, event);
198
+ }
199
+ }
72
200
 
73
- if (callbackUrl.length) {
74
- redirectUrlWithLocale += `?next=${encodeURIComponent(callbackUrl)}`;
201
+ const callbackMatch = CALLBACK_URL_REGEX.exec(pathname);
202
+ if (callbackMatch) {
203
+ try {
204
+ const { response, redirected } = await handleCallback(
205
+ req,
206
+ callbackMatch[1],
207
+ search
208
+ );
209
+ if (!redirected) {
210
+ logger.warn('OAuth callback: no redirect from commerce', {
211
+ provider: callbackMatch[1]
212
+ });
213
+ }
214
+ return response;
215
+ } catch (error) {
216
+ logger.error('OAuth callback fetch failed', { error });
217
+ return middleware(req, event);
75
218
  }
219
+ }
76
220
 
77
- const response = NextResponse.redirect(redirectUrlWithLocale);
78
- response.cookies.delete('messages');
79
- response.cookies.delete('pz-oauth-callback-url');
80
- return response;
221
+ if (pathname.startsWith('/baskets/basket')) {
222
+ const response = handleBasketRedirect(req);
223
+ if (response) return response;
81
224
  }
82
225
 
83
226
  return middleware(req, event);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@akinon/next",
3
3
  "description": "Core package for Project Zero Next",
4
- "version": "2.0.0-beta.16",
4
+ "version": "2.0.0-beta.18",
5
5
  "private": false,
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -35,7 +35,7 @@
35
35
  "set-cookie-parser": "2.6.0"
36
36
  },
37
37
  "devDependencies": {
38
- "@akinon/eslint-plugin-projectzero": "2.0.0-beta.16",
38
+ "@akinon/eslint-plugin-projectzero": "2.0.0-beta.18",
39
39
  "@babel/core": "7.26.10",
40
40
  "@babel/preset-env": "7.26.9",
41
41
  "@babel/preset-typescript": "7.27.0",
@@ -14,6 +14,7 @@ import {
14
14
  setHasGiftBox,
15
15
  setInstallmentOptions,
16
16
  setLoyaltyBalance,
17
+ setLoyaltyBalances,
17
18
  setPaymentChoices,
18
19
  setPaymentOptions,
19
20
  setRetailStores,
@@ -224,6 +225,14 @@ export const contextListMiddleware: Middleware = ({
224
225
  dispatch(setLoyaltyBalance(context.page_context.balance));
225
226
  }
226
227
 
228
+ if (context.page_context.balances) {
229
+ dispatch(setLoyaltyBalances(context.page_context.balances));
230
+ }
231
+
232
+ if (context.page_context.accounts) {
233
+ dispatch(setLoyaltyBalances(context.page_context.accounts));
234
+ }
235
+
227
236
  if (context.page_context.retail_stores) {
228
237
  dispatch(setRetailStores(context.page_context.retail_stores));
229
238
  }
@@ -9,6 +9,7 @@ import {
9
9
  CreditCardType,
10
10
  DeliveryOption,
11
11
  InstallmentOption,
12
+ LoyaltyBalanceItem,
12
13
  PaymentChoice,
13
14
  PaymentOption,
14
15
  CheckoutCreditPaymentOption,
@@ -49,6 +50,7 @@ export interface CheckoutState {
49
50
  bankAccounts: BankAccount[];
50
51
  selectedBankAccountPk: number;
51
52
  loyaltyBalance?: string;
53
+ loyaltyBalances?: LoyaltyBalanceItem[];
52
54
  retailStores: RetailStore[];
53
55
  attributeBasedShippingOptions: AttributeBasedShippingOption[];
54
56
  selectedShippingOptions: Record<string, number>;
@@ -188,6 +190,9 @@ const checkoutSlice = createSlice({
188
190
  setLoyaltyBalance(state, { payload }) {
189
191
  state.loyaltyBalance = payload;
190
192
  },
193
+ setLoyaltyBalances(state, { payload }) {
194
+ state.loyaltyBalances = payload;
195
+ },
191
196
  setRetailStores(state, { payload }) {
192
197
  state.retailStores = payload;
193
198
  },
@@ -234,6 +239,7 @@ export const {
234
239
  setBankAccounts,
235
240
  setSelectedBankAccountPk,
236
241
  setLoyaltyBalance,
242
+ setLoyaltyBalances,
237
243
  setRetailStores,
238
244
  setAttributeBasedShippingOptions,
239
245
  setSelectedShippingOptions,
@@ -68,6 +68,18 @@ export interface CheckoutPaymentOption {
68
68
  viewProps?: any;
69
69
  }
70
70
 
71
+ export interface LoyaltyBalanceItem {
72
+ label_id: number | null;
73
+ label: string | null;
74
+ balance: string;
75
+ currency?: string;
76
+ }
77
+
78
+ export interface AccountUsage {
79
+ label_id: number | null;
80
+ amount: number;
81
+ }
82
+
71
83
  export interface GiftBox {
72
84
  note: string;
73
85
  gift_video: boolean;
@@ -91,6 +103,7 @@ export interface PreOrder {
91
103
  notes?: string;
92
104
  user_phone_number?: string;
93
105
  loyalty_money?: string;
106
+ loyalty_account_usages?: AccountUsage[];
94
107
  currency_type_label?: string;
95
108
  is_guest?: boolean;
96
109
  is_post_order?: boolean;
@@ -151,6 +164,8 @@ export interface CheckoutContext {
151
164
  redirect_url?: string;
152
165
  context_data?: any;
153
166
  balance?: string;
167
+ balances?: LoyaltyBalanceItem[];
168
+ accounts?: LoyaltyBalanceItem[];
154
169
  attribute_based_shipping_options?: AttributeBasedShippingOption[];
155
170
  paymentData?: any;
156
171
  paymentMethod?: string;