@@ -10,12 +10,26 @@ import { getSession } from '@/lib/auth'
1010import { EnvCapabilityConfigurationError } from '@/lib/core/config/env-capabilities'
1111import { requireConfiguredOAuthClient } from '@/lib/core/config/env-capabilities.server'
1212import { getBaseUrl } from '@/lib/core/utils/urls'
13+ import { isSameOrigin } from '@/lib/core/utils/validation'
1314import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
15+ import { completeShopifyOAuthConnection } from '@/lib/oauth/shopify'
16+ import { parseShopifyOAuthState } from '@/lib/oauth/shopify-state'
1417
1518const logger = createLogger ( 'ShopifyCallback' )
1619
1720export const dynamic = 'force-dynamic'
1821
22+ function clearShopifyOAuthCookies ( response : NextResponse ) : NextResponse {
23+ response . cookies . delete ( 'shopify_oauth_state' )
24+ response . cookies . delete ( 'shopify_shop_domain' )
25+ response . cookies . delete ( 'shopify_credential_draft_id' )
26+ response . cookies . delete ( 'shopify_pending_token' )
27+ response . cookies . delete ( 'shopify_pending_shop' )
28+ response . cookies . delete ( 'shopify_pending_scope' )
29+ response . cookies . delete ( 'shopify_return_url' )
30+ return response
31+ }
32+
1933/**
2034 * Validates the HMAC signature from Shopify to ensure the request is authentic
2135 * @see https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/offline-access-tokens
@@ -59,9 +73,6 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
5973 shop : searchParams . get ( 'shop' ) || undefined ,
6074 } )
6175
62- const storedState = request . cookies . get ( 'shopify_oauth_state' ) ?. value
63- const storedShop = request . cookies . get ( 'shopify_shop_domain' ) ?. value
64-
6576 const {
6677 values : { SHOPIFY_CLIENT_ID : clientId , SHOPIFY_CLIENT_SECRET : clientSecret } ,
6778 } = requireConfiguredOAuthClient ( 'shopify' )
@@ -71,8 +82,8 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
7182 return NextResponse . redirect ( `${ baseUrl } /workspace?error=shopify_hmac_invalid` )
7283 }
7384
74- if ( ! state || state !== storedState ) {
75- logger . error ( 'State mismatch in Shopify OAuth callback' )
85+ if ( ! state ) {
86+ logger . error ( 'Missing state in Shopify OAuth callback' )
7687 return NextResponse . redirect ( `${ baseUrl } /workspace?error=shopify_state_mismatch` )
7788 }
7889
@@ -81,7 +92,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
8192 return NextResponse . redirect ( `${ baseUrl } /workspace?error=shopify_no_code` )
8293 }
8394
84- const shopDomain = shop || storedShop
95+ const shopDomain = shop
8596 if ( ! shopDomain ) {
8697 logger . error ( 'No shop domain available' )
8798 return NextResponse . redirect ( `${ baseUrl } /workspace?error=shopify_no_shop` )
@@ -92,6 +103,13 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
92103 return NextResponse . redirect ( `${ baseUrl } /workspace?error=shopify_invalid_shop` )
93104 }
94105
106+ const { draftId } = parseShopifyOAuthState ( {
107+ state,
108+ userId : session . user . id ,
109+ shopDomain,
110+ clientSecret,
111+ } )
112+
95113 const tokenResponse = await fetch ( `https://${ shopDomain } /admin/oauth/access_token` , {
96114 method : 'POST' ,
97115 headers : {
@@ -127,44 +145,30 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
127145 return NextResponse . redirect ( `${ baseUrl } /workspace?error=shopify_no_token` )
128146 }
129147
130- const storeUrl = new URL ( `${ baseUrl } /api/auth/oauth2/shopify/store` )
131-
132- const response = NextResponse . redirect ( storeUrl )
133-
134- response . cookies . set ( 'shopify_pending_token' , accessToken , {
135- httpOnly : true ,
136- secure : process . env . NODE_ENV === 'production' ,
137- sameSite : 'lax' ,
138- maxAge : 60 ,
139- path : '/' ,
140- } )
141-
142- response . cookies . set ( 'shopify_pending_shop' , shopDomain , {
143- httpOnly : true ,
144- secure : process . env . NODE_ENV === 'production' ,
145- sameSite : 'lax' ,
146- maxAge : 60 ,
147- path : '/' ,
148- } )
149-
150- response . cookies . set ( 'shopify_pending_scope' , scope || '' , {
151- httpOnly : true ,
152- secure : process . env . NODE_ENV === 'production' ,
153- sameSite : 'lax' ,
154- maxAge : 60 ,
155- path : '/' ,
148+ await completeShopifyOAuthConnection ( {
149+ accessToken,
150+ shopDomain,
151+ scope,
152+ userId : session . user . id ,
153+ draftId,
154+ signal : request . signal ,
156155 } )
157156
158- response . cookies . delete ( 'shopify_oauth_state' )
159- response . cookies . delete ( 'shopify_shop_domain' )
157+ const returnUrlCookie = request . cookies . get ( 'shopify_return_url' ) ?. value
158+ const redirectUrl =
159+ returnUrlCookie && isSameOrigin ( returnUrlCookie ) ? returnUrlCookie : `${ baseUrl } /workspace`
160+ const finalUrl = new URL ( redirectUrl )
161+ finalUrl . searchParams . set ( 'shopify_connected' , 'true' )
160162
161- return response
163+ return clearShopifyOAuthCookies ( NextResponse . redirect ( finalUrl ) )
162164 } catch ( error ) {
163165 logger . error ( 'Error in Shopify OAuth callback:' , error )
164166 const errorCode =
165167 error instanceof EnvCapabilityConfigurationError && error . capabilityId === 'oauth'
166168 ? 'shopify_config_error'
167169 : 'shopify_callback_error'
168- return NextResponse . redirect ( `${ baseUrl } /workspace?error=${ errorCode } ` )
170+ return clearShopifyOAuthCookies (
171+ NextResponse . redirect ( `${ baseUrl } /workspace?error=${ errorCode } ` )
172+ )
169173 }
170174} )
0 commit comments