Skip to content

Instantly share code, notes, and snippets.

@robksawyer
Last active June 2, 2024 06:07
Show Gist options
  • Save robksawyer/c082fc6e73cc20431431f57e49f28741 to your computer and use it in GitHub Desktop.
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…
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;
}
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;
}
// 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