Last active
June 2, 2024 06:07
-
-
Save robksawyer/c082fc6e73cc20431431f57e49f28741 to your computer and use it in GitHub Desktop.
Shopify's authentication and utility libraries are designed to work with native Node.js IncomingMessage and ServerResponse objects. Since Next.js uses its own request and response objects, these conversions are necessary to bridge the gap. By implementing these conversion functions, you can ensure that your Next.js application seamlessly integra…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { IncomingMessage } from "http"; | |
import { NextRequest } from "next/server"; | |
import { Socket } from "net"; | |
import { cookies } from "next/headers"; | |
export async function convertNextRequestToIncomingMessage( | |
request: NextRequest | |
): Promise<IncomingMessage> { | |
if (!request || typeof request !== "object") { | |
throw new Error("Invalid request object"); | |
} | |
const incomingMessage = new IncomingMessage(new Socket()); | |
try { | |
// Convert headers | |
incomingMessage.headers = Object.fromEntries(request.headers.entries()); | |
incomingMessage.method = request.method; | |
incomingMessage.url = request.url; | |
console.log("Converted headers:", incomingMessage.headers); | |
// Ensure all cookies are set in headers | |
const cookieStore = cookies(); | |
const allCookies = cookieStore.getAll(); | |
const cookieHeader = allCookies | |
.map((cookie) => `${cookie.name}=${cookie.value}`) | |
.join("; "); | |
if (cookieHeader) { | |
incomingMessage.headers["cookie"] = cookieHeader; | |
} else { | |
console.error("No cookies found"); | |
} | |
let body: Buffer | null = null; | |
// Convert body if it exists | |
if (request.body) { | |
const reader = request.body.getReader(); | |
const stream = new ReadableStream({ | |
start(controller) { | |
function push() { | |
reader.read().then(({ done, value }) => { | |
if (done) { | |
controller.close(); | |
return; | |
} | |
controller.enqueue(value); | |
push(); | |
}); | |
} | |
push(); | |
}, | |
}); | |
const response = new Response(stream); | |
const bodyBuffer = await response.arrayBuffer(); | |
body = Buffer.from(bodyBuffer); | |
if (body.length > 0) { | |
incomingMessage.push(body); | |
} | |
incomingMessage.push(null); | |
// Log the converted body | |
console.log("Converted body buffer:", body); | |
} | |
// Mimic NextApiRequest properties | |
const url = new URL(request.url); | |
const query = Object.fromEntries(url.searchParams.entries()); | |
console.log("Converted query parameters:", query); | |
const parsedBody = | |
body && body.length > 0 ? JSON.parse(body.toString()) : null; | |
console.log("Parsed body:", parsedBody); | |
(incomingMessage as any).query = query; | |
(incomingMessage as any).cookies = Object.fromEntries( | |
allCookies.map((cookie) => [cookie.name, cookie.value]) | |
); | |
(incomingMessage as any).body = parsedBody; | |
} catch (error) { | |
console.error("Error converting NextRequest to IncomingMessage:", error); | |
throw error; | |
} | |
return incomingMessage; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { ServerResponse } from "http"; | |
import { NextResponse } from "next/server"; | |
export function convertNextResponseToServerResponse( | |
nextResponse: NextResponse | |
): ServerResponse { | |
if (!nextResponse || typeof nextResponse !== "object") { | |
throw new Error("Invalid nextResponse object"); | |
} | |
const serverResponse = new ServerResponse({} as any); | |
try { | |
serverResponse.statusCode = nextResponse.status; | |
serverResponse.statusMessage = nextResponse.statusText; | |
for (const [key, value] of nextResponse.headers.entries()) { | |
serverResponse.setHeader(key, value); | |
} | |
// Implement write method | |
serverResponse.write = ( | |
chunk: any, | |
encodingOrCallback?: any, | |
callback?: any | |
): boolean => { | |
if (typeof encodingOrCallback === "function") { | |
encodingOrCallback(null); | |
} else if (typeof callback === "function") { | |
callback(null); | |
} | |
return true; // Ensure boolean is returned | |
}; | |
// Implement end method | |
serverResponse.end = ( | |
chunk: any, | |
encodingOrCallback?: any, | |
callback?: any | |
): ServerResponse => { | |
if (typeof encodingOrCallback === "function") { | |
encodingOrCallback(null); | |
} else if (typeof callback === "function") { | |
callback(null); | |
} | |
return serverResponse; // Ensure ServerResponse is returned | |
}; | |
} catch (error) { | |
console.error("Error converting NextResponse to ServerResponse:", error); | |
throw error; | |
} | |
return serverResponse; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// lib/supabaseSessionStorage.ts | |
import { SessionInterface } from "@shopify/shopify-api/dist/auth/session/types"; | |
import { CustomSessionStorage } from "@shopify/shopify-api/dist/auth/session"; | |
import { supabase } from "@/utils/supabaseClient"; | |
const DATABASE = "shopify_sessions"; | |
// Store session callback | |
const storeSession = async (session: SessionInterface): Promise<boolean> => { | |
try { | |
console.log("Storing session:", session); | |
// Ensure isOnline is a boolean | |
const isOnline = | |
typeof session.isOnline === "boolean" | |
? session.isOnline | |
: session.isOnline === "true"; | |
// Ensure isActive is a boolean and defaults to true if not a function | |
const isActive = | |
typeof session.isActive === "function" ? session.isActive() : true; | |
const sessionData = { | |
id: session.id, // Unique identifier provided by Shopify | |
shop_domain: session.shop, | |
state: session.state, | |
is_online: isOnline, | |
scope: session.scope || "", // Providing default empty string if scope is undefined | |
expires: session.expires ? session.expires.toISOString() : null, // Providing null if expires is undefined and converting to ISO string | |
online_access_info: session.onlineAccessInfo || {}, // Providing empty object if undefined | |
access_token: session.accessToken, | |
is_active: isActive, | |
}; | |
// Log the session data to debug issues | |
console.log("Session data to be saved:", sessionData); | |
// Save the session to Supabase | |
const { data, error } = await supabase.from(DATABASE).upsert([sessionData]); // Using upsert instead of insert | |
if (error) { | |
console.error("Error storing session:", error.message); | |
return false; | |
} | |
console.log("Session stored successfully:", session.id); | |
return true; | |
} catch (error) { | |
console.error("Error storing session:", error); | |
return false; | |
} | |
}; | |
// Load session callback | |
const loadSession = async ( | |
id: string | |
): Promise<SessionInterface | undefined> => { | |
try { | |
const { data, error } = await supabase | |
.from(DATABASE) | |
.select( | |
"shop_domain, access_token, is_online, state, scope, expires, online_access_info, is_active" | |
) | |
.eq("id", id) | |
.single(); | |
if (error) { | |
console.error("Error loading session:", error.message); | |
return undefined; | |
} | |
if (data) { | |
const session: SessionInterface = { | |
id: id, | |
shop: data.shop_domain, | |
state: data.state, | |
isOnline: data.is_online, | |
scope: data.scope, | |
expires: data.expires ? new Date(data.expires) : undefined, | |
onlineAccessInfo: data.online_access_info, | |
accessToken: data.access_token, | |
isActive: () => data.is_active, | |
}; | |
console.log("Session loaded successfully:", session.id); | |
return session; | |
} | |
return undefined; | |
} catch (error) { | |
console.error("Error loading session:", error); | |
return undefined; | |
} | |
}; | |
// Delete session callback | |
const deleteSession = async (id: string): Promise<boolean> => { | |
try { | |
const { error } = await supabase.from(DATABASE).delete().eq("id", id); | |
if (error) { | |
console.error("Error deleting session:", error.message); | |
return false; | |
} | |
console.log("Session deleted successfully:", id); | |
return true; | |
} catch (error) { | |
console.error("Error deleting session:", error); | |
return false; | |
} | |
}; | |
class SupabaseSessionStorage extends CustomSessionStorage { | |
constructor() { | |
super(storeSession, loadSession, deleteSession); | |
} | |
} | |
export const sessionStorage = new SupabaseSessionStorage(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment