Refactor: Remove console logs and improve OAuth state handling

- Removed unnecessary console logs from various functions to clean up the codebase.
- Updated `getProductFunction` and `oAuthFunction` in Shopify and Openfront integrations to use passed state for OAuth flow.
- Enhanced error handling in `createShop` and `handlePlatformActivation` functions with user-friendly messages.
- Adjusted the `CreatePlatform` component to provide feedback using toast notifications upon successful platform creation.
- Cleaned up webhook verification and other integration functions for better readability and maintainability.
This commit is contained in:
Junaid
2025-12-09 09:40:10 -06:00
parent 66553a9643
commit bfd129dfb0
25 changed files with 4747 additions and 3210 deletions
+11 -21
View File
@@ -1287,14 +1287,15 @@ async function getWebhooksFunction2({
}
async function oAuthFunction2({
platform,
callbackUrl
callbackUrl,
state
}) {
const clientId = platform.appKey || process.env.SHOPIFY_APP_KEY;
if (!clientId) {
throw new Error("Shopify OAuth requires appKey in platform config or SHOPIFY_APP_KEY environment variable");
}
const scopes5 = "read_products,write_products,read_orders,write_orders,read_inventory,write_inventory";
const shopifyAuthUrl = `https://${platform.domain}/admin/oauth/authorize?client_id=${clientId}&scope=${scopes5}&redirect_uri=${callbackUrl}&state=${Math.random().toString(36).substring(7)}`;
const shopifyAuthUrl = `https://${platform.domain}/admin/oauth/authorize?client_id=${clientId}&scope=${scopes5}&redirect_uri=${encodeURIComponent(callbackUrl)}&state=${encodeURIComponent(state)}`;
return { authUrl: shopifyAuthUrl };
}
async function oAuthCallbackFunction2({
@@ -1484,11 +1485,11 @@ async function getChannelWebhooks({ platform }) {
args: {}
});
}
async function handleChannelOAuth({ platform, callbackUrl }) {
async function handleChannelOAuth({ platform, callbackUrl, state }) {
return executeChannelAdapterFunction({
platform,
functionName: "oAuthFunction",
args: { callbackUrl }
args: { callbackUrl, state }
});
}
async function handleChannelOAuthCallback({ platform, code, shop, state, appKey, appSecret, redirectUri }) {
@@ -2836,14 +2837,15 @@ async function getWebhooksFunction4({
}
async function oAuthFunction4({
platform,
callbackUrl
callbackUrl,
state
}) {
const clientId = platform.appKey || process.env.SHOPIFY_APP_KEY;
if (!clientId) {
throw new Error("Shopify OAuth requires appKey in platform config or SHOPIFY_APP_KEY environment variable");
}
const scopes5 = "read_products,write_products,read_orders,write_orders,read_inventory,write_inventory";
const shopifyAuthUrl = `https://${platform.domain}/admin/oauth/authorize?client_id=${clientId}&scope=${scopes5}&redirect_uri=${callbackUrl}&state=${Math.random().toString(36).substring(7)}`;
const shopifyAuthUrl = `https://${platform.domain}/admin/oauth/authorize?client_id=${clientId}&scope=${scopes5}&redirect_uri=${encodeURIComponent(callbackUrl)}&state=${encodeURIComponent(state)}`;
return { authUrl: shopifyAuthUrl };
}
async function oAuthCallbackFunction4({
@@ -3213,11 +3215,11 @@ async function getShopWebhooks({ platform }) {
args: {}
});
}
async function handleShopOAuth({ platform, callbackUrl }) {
async function handleShopOAuth({ platform, callbackUrl, state }) {
return executeShopAdapterFunction({
platform,
functionName: "oAuthFunction",
args: { callbackUrl }
args: { callbackUrl, state }
});
}
async function handleShopOAuthCallback({ platform, code, shop, state, appKey, appSecret, redirectUri }) {
@@ -7790,9 +7792,7 @@ function statelessSessions({
const authHeader = context.req.headers.authorization;
if (authHeader?.startsWith("Bearer ")) {
const accessToken = authHeader.replace("Bearer ", "");
console.log("\u{1F511} ACCESS TOKEN:", accessToken);
if (accessToken.startsWith("osp_")) {
console.log("\u{1F511} API KEY DETECTED, VALIDATING...");
try {
const apiKeys = await context.sudo().query.ApiKey.findMany({
where: { status: { equals: "active" } },
@@ -7807,7 +7807,6 @@ function statelessSessions({
user { id }
`
});
console.log("\u{1F511} CHECKING AGAINST", apiKeys.length, "ACTIVE API KEYS");
let matchingApiKey = null;
for (const apiKey of apiKeys) {
try {
@@ -7821,24 +7820,19 @@ function statelessSessions({
const isValid = await import_bcryptjs.default.compare(accessToken, fullApiKey.tokenSecret);
if (isValid) {
matchingApiKey = apiKey;
console.log("\u{1F511} FOUND MATCHING API KEY:", apiKey.id);
break;
}
} catch (error) {
console.log("\u{1F511} ERROR VERIFYING API KEY:", error);
continue;
}
}
if (!matchingApiKey) {
console.log("\u{1F511} NO MATCHING API KEY FOUND");
return;
}
if (matchingApiKey.status !== "active") {
console.log("\u{1F511} API KEY NOT ACTIVE:", matchingApiKey.status);
return;
}
if (matchingApiKey.expiresAt && /* @__PURE__ */ new Date() > new Date(matchingApiKey.expiresAt)) {
console.log("\u{1F511} API KEY EXPIRED");
await context.sudo().query.ApiKey.updateOne({
where: { id: matchingApiKey.id },
data: { status: "revoked" }
@@ -7857,17 +7851,13 @@ function statelessSessions({
}
}).catch(console.error);
if (matchingApiKey.user?.id) {
const session = {
return {
itemId: matchingApiKey.user.id,
listKey,
apiKeyScopes: matchingApiKey.scopes || []
// Attach scopes for permission checking
};
console.log("\u{1F511} RETURNING SESSION:", JSON.stringify(session, null, 2));
return session;
}
} catch (err) {
console.log("\u{1F511} API Key validation error:", err);
return;
}
}
File diff suppressed because one or more lines are too long
+51 -145
View File
@@ -5,66 +5,19 @@ import { handleChannelOAuthCallback } from '@/features/integrations/channel/lib/
import { getBaseUrl } from '@/features/dashboard/lib/getBaseUrl';
import crypto from 'crypto';
// Self-contained signed state parameters (industry standard)
const SECRET_KEY = process.env.OAUTH_STATE_SECRET || 'openship-oauth-secret-key';
async function generateOAuthState(platformId: string, type: 'shop' | 'channel'): Promise<string> {
// Create state payload with timestamp for expiry
const payload = {
platformId,
type,
timestamp: Date.now(),
nonce: crypto.randomBytes(16).toString('hex') // Small nonce for uniqueness
};
console.log('🟢 GENERATE: Creating signed state for:', payload);
// Create signature
const payloadString = JSON.stringify(payload);
const signature = crypto.createHmac('sha256', SECRET_KEY).update(payloadString).digest('hex');
// Combine payload and signature
const signedState = {
payload: payloadString,
signature
};
// Encode as base64
const encodedState = Buffer.from(JSON.stringify(signedState)).toString('base64');
console.log('🟢 GENERATE: Generated signed state:', encodedState);
return encodedState;
}
export async function GET(request: NextRequest) {
try {
console.log('🚀 OAuth callback endpoint called');
console.log('🔍 Request URL:', request.url);
const { searchParams } = new URL(request.url);
// Get OAuth parameters
const code = searchParams.get('code');
const state = searchParams.get('state');
const shop = searchParams.get('shop'); // Domain from OpenFront
const shop = searchParams.get('shop');
const error = searchParams.get('error');
const errorDescription = searchParams.get('error_description');
// Check for simplified auto-create flow
const autoCreate = searchParams.get('auto_create') === 'true';
const appName = searchParams.get('app_name');
const clientId = searchParams.get('client_id');
console.log('🔍 OAuth callback parameters:');
console.log(' - code:', code ? `${code.substring(0, 10)}...` : 'MISSING');
console.log(' - state:', state ? `${state.substring(0, 20)}...` : 'MISSING');
console.log(' - shop:', shop);
console.log(' - error:', error);
console.log(' - errorDescription:', errorDescription);
console.log(' - autoCreate:', autoCreate);
console.log(' - appName:', appName);
console.log(' - clientId:', clientId);
// Handle OAuth errors
if (error) {
console.error('OAuth error:', error, errorDescription);
@@ -73,95 +26,81 @@ export async function GET(request: NextRequest) {
{ status: 400 }
);
}
if (!code || !state) {
return NextResponse.json(
{ error: 'Missing required parameters: code or state' },
{ status: 400 }
);
}
console.log('🔵 CALLBACK: Received state parameter:', state);
// Simplified auto-create flow is handled by marketplace flow instead
// Decode and validate state (handle both base64 and JSON formats)
let signedState;
try {
// First try to parse as direct JSON (marketplace flow from OpenFront)
try {
signedState = JSON.parse(state);
console.log('🔵 CALLBACK: Parsed state as direct JSON:', signedState);
} catch {
// If that fails, try base64 decoding (original flow from OpenShip)
const decoded = Buffer.from(state, 'base64').toString();
console.log('🔵 CALLBACK: Decoded state string from base64:', decoded);
signedState = JSON.parse(decoded);
console.log('🔵 CALLBACK: Parsed signed state from base64:', signedState);
}
} catch (e) {
console.error('🔴 CALLBACK: Failed to decode state:', e);
console.error('Failed to decode state:', e);
return NextResponse.json(
{ error: 'Invalid state parameter' },
{ status: 400 }
);
}
// Handle different state formats
let stateData;
if (signedState.type === 'marketplace') {
// Marketplace flow - state is already the data we need
console.log('🔵 CALLBACK: Detected marketplace flow');
stateData = signedState;
} else {
// Original OpenShip flow - has payload and signature
console.log('🔵 CALLBACK: Detected original OpenShip flow');
const { payload, signature } = signedState;
// Verify signature
const expectedSignature = crypto.createHmac('sha256', SECRET_KEY).update(payload).digest('hex');
if (signature !== expectedSignature) {
console.error('🔴 CALLBACK: Invalid state signature');
console.error('Invalid state signature');
return NextResponse.json(
{ error: 'Invalid state signature' },
{ status: 400 }
);
}
// Parse verified payload
try {
stateData = JSON.parse(payload);
console.log('🔵 CALLBACK: Verified state data:', stateData);
} catch (e) {
console.error('🔴 CALLBACK: Failed to parse payload:', e);
console.error('Failed to parse payload:', e);
return NextResponse.json(
{ error: 'Invalid state payload' },
{ status: 400 }
);
}
}
console.log('🔵 CALLBACK: State verified successfully');
// Handle marketplace flow - exchange code for tokens then redirect
if (stateData.type === 'marketplace') {
console.log('🔵 CALLBACK: Processing marketplace flow - exchanging code for tokens');
// Create minimal platform object for adapter
const marketplacePlatform = {
domain: shop,
accessToken: '', // Will be set after OAuth exchange
accessToken: '',
appKey: stateData.client_id,
appSecret: stateData.client_secret,
oAuthCallbackFunction: stateData.adapter_slug // Use dynamic adapter slug
oAuthCallbackFunction: stateData.adapter_slug
};
// Determine app type for proper OAuth handler
const marketplaceAppType = stateData.app_type || 'shop'; // Default to shop for backward compatibility
const marketplaceAppType = stateData.app_type || 'shop';
// Exchange code for access token using appropriate OAuth handler
const tokenResult = marketplaceAppType === 'channel'
const tokenResult = marketplaceAppType === 'channel'
? await handleChannelOAuthCallback({
platform: marketplacePlatform,
code,
@@ -180,40 +119,24 @@ export async function GET(request: NextRequest) {
appSecret: stateData.client_secret,
redirectUri: `${await getBaseUrl()}/api/oauth/callback`
});
console.log('🔍 Token exchange result type:', typeof tokenResult);
console.log('🔍 Token exchange result:', typeof tokenResult === 'string' ? `${tokenResult.substring(0, 10)}...` : tokenResult);
// Handle both old string format and new object format
let accessToken, refreshToken, tokenExpiresAt;
if (typeof tokenResult === 'string') {
console.log('📝 Using legacy string token format');
accessToken = tokenResult;
} else {
console.log('📝 Using new object token format');
// OpenFront returns camelCase field names
accessToken = tokenResult.accessToken;
refreshToken = tokenResult.refreshToken;
tokenExpiresAt = tokenResult.tokenExpiresAt;
console.log('🔑 Token details:');
console.log(' - accessToken present:', !!accessToken);
console.log(' - refreshToken present:', !!refreshToken);
console.log(' - tokenExpiresAt present:', !!tokenExpiresAt);
}
const baseUrl = await getBaseUrl();
// Determine redirect URL based on app type
const redirectAppType = stateData.app_type || 'shop'; // Default to shop for backward compatibility
const redirectAppType = stateData.app_type || 'shop';
const endpoint = redirectAppType === 'channel' ? 'channels' : 'shops';
const createParam = redirectAppType === 'channel' ? 'showCreateChannel' : 'showCreateShop';
console.log('🎯 Building redirect URL:');
console.log(' - baseUrl:', baseUrl);
console.log(' - redirectAppType:', redirectAppType);
console.log(' - endpoint:', endpoint);
console.log(' - createParam:', createParam);
const redirectUrl = new URL(`${baseUrl}/dashboard/platform/${endpoint}`);
redirectUrl.searchParams.set(createParam, 'true');
redirectUrl.searchParams.set('domain', shop ?? '');
@@ -222,45 +145,35 @@ export async function GET(request: NextRequest) {
redirectUrl.searchParams.set('client_secret', stateData.client_secret);
redirectUrl.searchParams.set('app_name', stateData.app_name);
redirectUrl.searchParams.set('adapter_slug', stateData.adapter_slug);
console.log('🔑 Adding optional tokens to redirect:');
if (refreshToken) {
console.log(' - Adding refreshToken to URL params');
redirectUrl.searchParams.set('refreshToken', refreshToken);
} else {
console.log(' - No refreshToken to add');
}
if (tokenExpiresAt) {
console.log(' - Adding tokenExpiresAt to URL params:', tokenExpiresAt);
// tokenExpiresAt is already a string from OpenFront adapter
redirectUrl.searchParams.set('tokenExpiresAt', tokenExpiresAt);
} else {
console.log(' - No tokenExpiresAt to add');
}
console.log('🎯 Final redirect URL:', redirectUrl.toString());
return NextResponse.redirect(redirectUrl.toString());
}
// Handle original OpenShip flow
const { platformId, type, timestamp } = stateData;
// Check if state is expired (10 minutes max)
const maxAge = 10 * 60 * 1000; // 10 minutes
const maxAge = 10 * 60 * 1000;
if (Date.now() - timestamp > maxAge) {
console.error('🔴 CALLBACK: State expired. Age:', Date.now() - timestamp, 'ms');
console.error('State expired');
return NextResponse.json(
{ error: 'State expired' },
{ status: 400 }
);
}
// Fetch the platform based on type
let platform;
let accessToken;
const baseUrl = await getBaseUrl();
if (type === 'shop') {
platform = await keystoneContext.sudo().query.ShopPlatform.findOne({
where: { id: platformId },
@@ -272,11 +185,11 @@ export async function GET(request: NextRequest) {
oAuthCallbackFunction
`,
});
if (!platform) {
return NextResponse.json({ error: 'Shop platform not found' }, { status: 404 });
}
// Exchange code for tokens
const tokenResult = await handleShopOAuthCallback({
platform,
@@ -285,25 +198,22 @@ export async function GET(request: NextRequest) {
state,
appKey: platform.appKey,
appSecret: platform.appSecret,
redirectUri: `${baseUrl}/api/oauth/callback`, // Single callback URL
redirectUri: `${baseUrl}/api/oauth/callback`,
});
// Handle both old string format and new object format for backward compatibility
// Handle both old string format and new object format
if (typeof tokenResult === 'string') {
// Legacy format - just access token
accessToken = tokenResult;
} else {
// New format - object with both tokens
accessToken = tokenResult.accessToken;
}
// Redirect to shops page with params
const redirectUrl = new URL(`${baseUrl}/dashboard/platform/shops`);
redirectUrl.searchParams.set('showCreateShop', 'true');
redirectUrl.searchParams.set('platform', platformId);
redirectUrl.searchParams.set('accessToken', accessToken);
if (typeof tokenResult === 'object') {
// Include additional token data for new implementations
if (tokenResult.refreshToken) {
redirectUrl.searchParams.set('refreshToken', tokenResult.refreshToken);
}
@@ -312,9 +222,9 @@ export async function GET(request: NextRequest) {
}
}
redirectUrl.searchParams.set('domain', shop ?? '');
return NextResponse.redirect(redirectUrl.toString());
} else if (type === 'channel') {
platform = await keystoneContext.sudo().query.ChannelPlatform.findOne({
where: { id: platformId },
@@ -326,11 +236,11 @@ export async function GET(request: NextRequest) {
oAuthCallbackFunction
`,
});
if (!platform) {
return NextResponse.json({ error: 'Channel platform not found' }, { status: 404 });
}
// Exchange code for access token
const channelTokenResult = await handleChannelOAuthCallback({
platform,
@@ -339,25 +249,22 @@ export async function GET(request: NextRequest) {
state,
appKey: platform.appKey,
appSecret: platform.appSecret,
redirectUri: `${baseUrl}/api/oauth/callback`, // Single callback URL
redirectUri: `${baseUrl}/api/oauth/callback`,
});
// Handle both old string format and new object format for backward compatibility
// Handle both old string format and new object format
if (typeof channelTokenResult === 'string') {
// Legacy format - just access token
accessToken = channelTokenResult;
} else {
// New format - object with both tokens
accessToken = channelTokenResult.accessToken;
}
// Redirect to channels page with params
const redirectUrl = new URL(`${baseUrl}/dashboard/platform/channels`);
redirectUrl.searchParams.set('showCreateChannel', 'true');
redirectUrl.searchParams.set('platform', platformId);
redirectUrl.searchParams.set('accessToken', accessToken);
if (typeof channelTokenResult === 'object') {
// Include additional token data for new implementations
if (channelTokenResult.refreshToken) {
redirectUrl.searchParams.set('refreshToken', channelTokenResult.refreshToken);
}
@@ -366,25 +273,24 @@ export async function GET(request: NextRequest) {
}
}
redirectUrl.searchParams.set('domain', shop ?? '');
return NextResponse.redirect(redirectUrl.toString());
} else {
return NextResponse.json(
{ error: 'Invalid platform type' },
{ status: 400 }
);
}
} catch (error) {
console.error('OAuth callback error:', error);
return NextResponse.json(
{
error: 'OAuth callback failed',
details: error instanceof Error ? error.message : 'Unknown error'
{
error: 'OAuth callback failed',
details: error instanceof Error ? error.message : 'Unknown error'
},
{ status: 500 }
);
}
}
@@ -234,6 +234,7 @@ function useBlockPopover(
popoverProps: mergeProps(overlayProps, positionProps),
underlayProps,
updatePosition,
triggerAnchorPoint: { x: 0, y: 0 },
}
}
@@ -83,11 +83,11 @@ export async function getChannelWebhooks({ platform }: { platform: any }) {
});
}
export async function handleChannelOAuth({ platform, callbackUrl }: { platform: any; callbackUrl: string }) {
export async function handleChannelOAuth({ platform, callbackUrl, state }: { platform: any; callbackUrl: string; state: string }) {
return executeChannelAdapterFunction({
platform,
functionName: "oAuthFunction",
args: { callbackUrl },
args: { callbackUrl, state },
});
}
+106 -309
View File
@@ -37,129 +37,85 @@ interface DeleteWebhookArgs {
// Helper function to get fresh access token with proper OAuth 2.0 flow
const getFreshAccessToken = async (platform: OpenFrontPlatform) => {
console.log('🔄 [OpenFront Channel] getFreshAccessToken called');
console.log('🔄 [OpenFront Channel] Platform domain:', platform.domain);
// Get channel with OAuth credentials from database
const channels = await keystoneContext.sudo().query.Channel.findMany({
where: {
where: {
domain: { equals: platform.domain },
accessToken: { equals: platform.accessToken }
},
query: 'id refreshToken tokenExpiresAt platform { appKey appSecret }'
});
if (!channels || channels.length === 0) {
console.log('⚠️ [OpenFront Channel] No matching channel found in database');
return platform.accessToken;
}
const channel = channels[0];
console.log('🔄 [OpenFront Channel] Found channel:', channel.id);
console.log('🔄 [OpenFront Channel] Has refresh token:', !!channel.refreshToken);
console.log('🔄 [OpenFront Channel] Token expires at:', channel.tokenExpiresAt);
console.log('🔄 [OpenFront Channel] Has OAuth credentials:', !!(channel.platform?.appKey && channel.platform?.appSecret));
console.log('🔄 [OpenFront Channel] Has refresh token:', !!channel.refreshToken);
// If we have a refresh token, check if we need to refresh
if (channel.refreshToken) {
console.log('🔄 [OpenFront Channel] Refresh token found, checking if refresh needed');
// Check if access token has expired (if we have expiry info)
let shouldRefresh = false;
if (channel.tokenExpiresAt) {
const expiresAt = typeof channel.tokenExpiresAt === 'string'
? new Date(channel.tokenExpiresAt)
const expiresAt = typeof channel.tokenExpiresAt === 'string'
? new Date(channel.tokenExpiresAt)
: channel.tokenExpiresAt;
const now = new Date();
shouldRefresh = expiresAt <= now;
console.log('🔄 [OpenFront Channel] Token expiry check:');
console.log('🔄 [OpenFront Channel] - Expires at:', expiresAt.toISOString());
console.log('🔄 [OpenFront Channel] - Current time:', now.toISOString());
console.log('🔄 [OpenFront Channel] - Should refresh:', shouldRefresh);
} else {
// If no expiry info, assume token needs refresh
shouldRefresh = true;
console.log('🔄 [OpenFront Channel] No expiry info found, assuming refresh needed');
}
if (shouldRefresh) {
console.log('🔄 [OpenFront Channel] Starting token refresh process...');
// Use refresh token to get new access token
const tokenUrl = `${platform.domain}/api/oauth/token`;
console.log('🔄 [OpenFront Channel] Token URL:', tokenUrl);
const formData = new URLSearchParams({
grant_type: "refresh_token",
refresh_token: channel.refreshToken,
client_id: channel.platform?.appKey || "",
client_secret: channel.platform?.appSecret || "",
});
const tokenUrl = `${platform.domain}/api/oauth/token`;
console.log('🔄 [OpenFront Channel] Refresh request params:');
console.log('🔄 [OpenFront Channel] - grant_type: refresh_token');
console.log('🔄 [OpenFront Channel] Making token refresh request');
console.log('🔄 [OpenFront Channel] - refresh_token:', channel.refreshToken ? "SET" : "NOT_SET");
console.log('🔄 [OpenFront Channel] Making refresh token request...');
const response = await fetch(tokenUrl, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: formData,
});
console.log('🔄 [OpenFront Channel] Refresh response status:', response.status);
console.log('🔄 [OpenFront Channel] Refresh response ok:', response.ok);
if (!response.ok) {
const errorText = await response.text();
console.error('🚨 [OpenFront Channel] Token refresh failed:', errorText);
console.error('🚨 [OpenFront Channel] Response status:', response.status);
console.error('🚨 [OpenFront Channel] Response statusText:', response.statusText);
throw new Error(`Failed to refresh access token: ${response.statusText} - ${errorText}`);
}
const tokenData = await response.json();
console.log('🔄 [OpenFront Channel] Token refresh response received');
console.log('🔄 [OpenFront Channel] - Has access_token:', !!tokenData.access_token);
console.log('🔄 [OpenFront Channel] - Has refresh_token:', !!tokenData.refresh_token);
console.log('🔄 [OpenFront Channel] - Expires in:', tokenData.expires_in, 'seconds');
const { access_token, refresh_token, expires_in } = tokenData;
// Update stored access token and expiry in database
console.log('🔄 [OpenFront Channel] Updating tokens in database...');
try {
console.log('🔄 [OpenFront Channel] Updating channel:', channel.id);
await keystoneContext.sudo().query.Channel.updateOne({
where: { id: channel.id },
data: {
accessToken: access_token,
...(refresh_token && { refreshToken: refresh_token }),
...(expires_in && { tokenExpiresAt: new Date(Date.now() + (expires_in * 1000)) })
}
const formData = new URLSearchParams({
grant_type: "refresh_token",
refresh_token: channel.refreshToken,
client_id: channel.platform?.appKey || "",
client_secret: channel.platform?.appSecret || "",
});
console.log('✅ [OpenFront Channel] Channel updated with new tokens:', channel.id);
} catch (error) {
console.error('🚨 [OpenFront Channel] Failed to update channel tokens in database:', error);
// Continue with the request even if database update fails
}
console.log('✅ [OpenFront Channel] Returning fresh access token');
return access_token;
const response = await fetch(tokenUrl, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: formData,
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to refresh access token: ${response.statusText} - ${errorText}`);
}
const tokenData = await response.json();
const { access_token, refresh_token, expires_in } = tokenData;
// Update stored access token and expiry in database
try {
await keystoneContext.sudo().query.Channel.updateOne({
where: { id: channel.id },
data: {
accessToken: access_token,
...(refresh_token && { refreshToken: refresh_token }),
...(expires_in && { tokenExpiresAt: new Date(Date.now() + (expires_in * 1000)) })
}
});
} catch (error) {
// Continue with the request even if database update fails
}
return access_token;
} else {
// Token hasn't expired yet, use existing one
console.log('✅ [OpenFront Channel] Token still valid, using existing access token');
return platform.accessToken;
}
}
// If no refresh token, just use the access token as-is
console.log('⚠️ [OpenFront Channel] No refresh token available, using existing access token');
return platform.accessToken;
};
@@ -308,8 +264,6 @@ export async function getProductFunction({
productId: string;
variantId?: string;
}) {
console.log("OpenFront Channel getProductFunction called with:", { platform: platform.domain, productId, variantId });
const openFrontClient = await createOpenFrontClient(platform);
const gqlQuery = gql`
@@ -388,12 +342,6 @@ export async function createPurchaseFunction({
shipping: any;
notes?: string;
}) {
console.log(`🛒 OpenFront Channel: Creating purchase with ${cartItems.length} items`);
console.log(`🚚 OpenFront Channel: Ship to: ${shipping?.firstName} ${shipping?.lastName}`);
console.log(`📦 OpenFront Channel: Full shipping data:`, JSON.stringify(shipping, null, 2));
console.log(`📦 OpenFront Channel: Platform domain:`, platform.domain);
console.log(`📦 OpenFront Channel: Cart items:`, JSON.stringify(cartItems, null, 2));
const openFrontClient = await createOpenFrontClient(platform);
try {
@@ -422,8 +370,6 @@ export async function createPurchaseFunction({
}
// Step 2: Create cart (following storefront flow)
console.log("🛒 [OpenFront Channel] Creating cart with region:", region.id);
const { createCart: cart } = await openFrontClient.request(gql`
mutation CreateCart($data: CartCreateInput!) {
createCart(data: $data) {
@@ -444,8 +390,6 @@ export async function createPurchaseFunction({
}
}) as any;
console.log("✅ [OpenFront Channel] Cart created with ID:", cart.id);
// Step 3: Add line items to cart (this computes unitPrice/total automatically)
const lineItemsToCreate = [];
for (const item of cartItems) {
@@ -455,8 +399,6 @@ export async function createPurchaseFunction({
});
}
console.log("📦 [OpenFront Channel] Adding", lineItemsToCreate.length, "line items to cart");
await openFrontClient.request(gql`
mutation AddLineItemsToCart($cartId: ID!, $data: CartUpdateInput!) {
updateActiveCart(cartId: $cartId, data: $data) {
@@ -472,11 +414,7 @@ export async function createPurchaseFunction({
}
}) as any;
console.log("✅ [OpenFront Channel] Line items added to cart");
// Step 4: Create addresses and add to cart
console.log("📍 [OpenFront Channel] Creating shipping address");
const { createAddress: shippingAddr } = await openFrontClient.request(gql`
mutation CreateAddress($data: AddressCreateInput!) {
createAddress(data: $data) {
@@ -500,8 +438,6 @@ export async function createPurchaseFunction({
}
}) as any;
console.log("✅ [OpenFront Channel] Shipping address created:", shippingAddr.id);
// Step 5: Update cart with addresses
await openFrontClient.request(gql`
mutation UpdateCartAddresses($cartId: ID!, $data: CartUpdateInput!) {
@@ -517,13 +453,7 @@ export async function createPurchaseFunction({
}
}) as any;
console.log("✅ [OpenFront Channel] Cart updated with addresses");
// Step 6: Complete cart to create order (like storefront placeOrder)
console.log("🎯 [OpenFront Channel] Completing cart to create order");
console.log("🎯 [OpenFront Channel] Calling completeActiveCart for cart:", cart.id);
const completeResult = await openFrontClient.request(gql`
mutation CompleteActiveCart($cartId: ID!) {
completeActiveCart(cartId: $cartId)
@@ -532,16 +462,11 @@ export async function createPurchaseFunction({
cartId: cart.id
}) as any;
console.log("🔍 [OpenFront Channel] completeActiveCart result:", JSON.stringify(completeResult, null, 2));
const order = completeResult.completeActiveCart;
if (!order?.id) {
throw new Error("Failed to complete cart - no order created");
}
console.log("🔍 [OpenFront Channel] COMPLETE ORDER RESULT:", JSON.stringify(order, null, 2));
console.log("🎉 [OpenFront Channel] Order created successfully:", order.id);
// Process line items from cart items (since order doesn't include line items)
const processedLineItems = cartItems.map((item: any) => ({
id: item.variantId,
@@ -561,21 +486,6 @@ export async function createPurchaseFunction({
};
} catch (error: any) {
console.error('🚨 OpenFront Channel: Purchase creation failed:', error);
if (error.response?.errors) {
console.error('🚨 GraphQL Errors:');
error.response.errors.forEach((err: any, i: number) => {
console.error(` ${i + 1}. ${err.message}`);
if (err.path) {
console.error(` Path: ${err.path.join(' -> ')}`);
}
if (err.extensions) {
console.error(` Extensions:`, err.extensions);
}
});
}
// Return error response with detailed error info
return {
purchaseId: null,
@@ -605,130 +515,66 @@ export async function createWebhookFunction({
endpoint: string;
events: string[];
}) {
console.log('🪝 [OpenFront Channel] createWebhookFunction called');
console.log('🪝 [OpenFront Channel] Platform domain:', platform.domain);
console.log('🪝 [OpenFront Channel] Endpoint URL:', endpoint);
console.log('🪝 [OpenFront Channel] Events received:', events);
console.log('🪝 [OpenFront Channel] Events type:', typeof events, 'Array?', Array.isArray(events));
// Check if ORDER_CANCELLED is requested - OpenFront doesn't support this yet
if (events.includes('ORDER_CANCELLED')) {
console.error('❌ OpenFront does not support ORDER_CANCELLED webhooks yet');
throw new Error('OpenFront does not support ORDER_CANCELLED webhooks. Only TRACKING_CREATED webhooks are currently supported.');
}
// Map Openship channel events to OpenFront events
// Map Openship channel events to OpenFront events
const eventMap: Record<string, string> = {
ORDER_CREATED: "order.created",
TRACKING_CREATED: "fulfillment.created",
// ORDER_CANCELLED: "order.cancelled", // Not supported yet
};
const openFrontEvents = events.map(event => {
const mapped = eventMap[event] || event;
console.log(`🪝 [OpenFront Channel] Mapping: ${event} -> ${mapped}`);
return mapped;
});
console.log('🪝 [OpenFront Channel] Final mapped events array:', openFrontEvents);
console.log('🪝 [OpenFront Channel] Joined events string:', openFrontEvents.join(", "));
const openFrontEvents = events.map(event => eventMap[event] || event);
try {
console.log('🪝 [OpenFront Channel] Creating OpenFront GraphQL client...');
const openFrontClient = await createOpenFrontClient(platform);
console.log('✅ [OpenFront Channel] GraphQL client created successfully');
const openFrontClient = await createOpenFrontClient(platform);
// Get current user from the access token
console.log('🪝 [OpenFront Channel] Fetching current user...');
const getUserQuery = gql`
query GetCurrentUser {
authenticatedItem {
... on User {
id
email
orderWebhookUrl
}
}
}
`;
console.log('🪝 [OpenFront Channel] Executing getUserQuery...');
const { authenticatedItem: user } = await openFrontClient.request(getUserQuery) as any;
console.log('🪝 [OpenFront Channel] getUserQuery result:', JSON.stringify(user, null, 2));
if (!user) {
console.error('🚨 [OpenFront Channel] User not authenticated - authenticatedItem is null/undefined');
throw new Error('User not authenticated');
}
console.log('✅ [OpenFront Channel] User authenticated successfully');
console.log('🪝 [OpenFront Channel] User ID:', user.id);
console.log('🪝 [OpenFront Channel] User email:', user.email);
console.log('🪝 [OpenFront Channel] Current webhook URL exists:', !!user.orderWebhookUrl);
// Update the user's webhook URL using updateActiveUser (bypasses access restrictions)
console.log('🪝 [OpenFront Channel] Updating user webhook URL...');
const updateUserMutation = gql`
mutation UpdateActiveUserWebhookUrl($data: UserUpdateProfileInput!) {
updateActiveUser(data: $data) {
// Get current user from the access token
const getUserQuery = gql`
query GetCurrentUser {
authenticatedItem {
... on User {
id
email
orderWebhookUrl
}
}
`;
}
`;
console.log('🪝 [OpenFront Channel] Update mutation variables:');
console.log('🪝 [OpenFront Channel] - User ID:', user.id);
console.log('🪝 [OpenFront Channel] - Setting new webhook URL');
const { authenticatedItem: user } = await openFrontClient.request(getUserQuery) as any;
const result = await openFrontClient.request(updateUserMutation, {
data: { orderWebhookUrl: endpoint }
}) as any;
if (!user) {
throw new Error('User not authenticated');
}
console.log('🪝 [OpenFront Channel] Update mutation result:', JSON.stringify(result, null, 2));
const updatedUser = result.updateActiveUser;
console.log('✅ [OpenFront Channel] User webhook URL updated successfully');
console.log('🪝 [OpenFront Channel] Updated user ID:', updatedUser.id);
console.log('🪝 [OpenFront Channel] Webhook URL updated successfully');
const webhookResponse = {
webhooks: [{
id: `user-${updatedUser.id}`,
callbackUrl: updatedUser.orderWebhookUrl,
topic: openFrontEvents.join(", "), // Join array into comma-separated string
format: "JSON",
createdAt: new Date().toISOString()
}],
webhookId: `user-${updatedUser.id}`
};
console.log('🪝 [OpenFront Channel] Final webhook response:', JSON.stringify(webhookResponse, null, 2));
return webhookResponse;
} catch (error: any) {
console.error('🚨 [OpenFront Channel] createWebhookFunction failed:', error);
console.error('🚨 [OpenFront Channel] Error message:', error.message);
console.error('🚨 [OpenFront Channel] Error stack:', error.stack);
if (error.response) {
console.error('🚨 [OpenFront Channel] GraphQL response:', JSON.stringify(error.response, null, 2));
if (error.response.errors) {
console.error('🚨 [OpenFront Channel] GraphQL errors:');
error.response.errors.forEach((err: any, i: number) => {
console.error(`🚨 [OpenFront Channel] ${i + 1}. ${err.message}`);
if (err.path) {
console.error(`🚨 [OpenFront Channel] Path: ${err.path.join(' -> ')}`);
}
if (err.extensions) {
console.error(`🚨 [OpenFront Channel] Extensions:`, err.extensions);
}
});
// Update the user's webhook URL using updateActiveUser (bypasses access restrictions)
const updateUserMutation = gql`
mutation UpdateActiveUserWebhookUrl($data: UserUpdateProfileInput!) {
updateActiveUser(data: $data) {
id
orderWebhookUrl
}
}
throw error;
}
`;
const result = await openFrontClient.request(updateUserMutation, {
data: { orderWebhookUrl: endpoint }
}) as any;
const updatedUser = result.updateActiveUser;
return {
webhooks: [{
id: `user-${updatedUser.id}`,
callbackUrl: updatedUser.orderWebhookUrl,
topic: openFrontEvents.join(", "),
format: "JSON",
createdAt: new Date().toISOString()
}],
webhookId: `user-${updatedUser.id}`
};
}
// Function to delete webhook - clear user's webhook URL
@@ -739,65 +585,29 @@ export async function deleteWebhookFunction({
platform: OpenFrontPlatform;
webhookId: string;
}) {
console.log('🗑️ OpenFront deleteWebhookFunction called:', { webhookId, platform: platform.domain });
try {
console.log('🗑️ [OpenFront Channel] Creating OpenFront GraphQL client...');
const openFrontClient = await createOpenFrontClient(platform);
console.log('✅ [OpenFront Channel] GraphQL client created successfully');
const openFrontClient = await createOpenFrontClient(platform);
// Clear the user's webhook URL using updateActiveUser (same as createWebhookFunction)
console.log('🗑️ [OpenFront Channel] Clearing user webhook URL...');
const updateUserMutation = gql`
mutation ClearActiveUserWebhookUrl($data: UserUpdateProfileInput!) {
updateActiveUser(data: $data) {
id
orderWebhookUrl
}
}
`;
console.log('🗑️ [OpenFront Channel] Clearing webhook URL for user');
const result = await openFrontClient.request(updateUserMutation, {
data: { orderWebhookUrl: "" }
}) as any;
console.log('🗑️ [OpenFront Channel] Clear mutation result:', JSON.stringify(result, null, 2));
const updatedUser = result.updateActiveUser;
console.log('✅ [OpenFront Channel] User webhook URL cleared successfully');
console.log('🗑️ [OpenFront Channel] Updated user ID:', updatedUser.id);
return {
success: true,
result: updatedUser,
deletedWebhookSubscriptionId: webhookId
};
} catch (error: any) {
console.error('🚨 [OpenFront Channel] deleteWebhookFunction failed:', error);
console.error('🚨 [OpenFront Channel] Error message:', error.message);
console.error('🚨 [OpenFront Channel] Error stack:', error.stack);
if (error.response) {
console.error('🚨 [OpenFront Channel] GraphQL response:', JSON.stringify(error.response, null, 2));
if (error.response.errors) {
console.error('🚨 [OpenFront Channel] GraphQL errors:');
error.response.errors.forEach((err: any, i: number) => {
console.error(`🚨 [OpenFront Channel] ${i + 1}. ${err.message}`);
if (err.path) {
console.error(`🚨 [OpenFront Channel] Path: ${err.path.join(' -> ')}`);
}
if (err.extensions) {
console.error(`🚨 [OpenFront Channel] Extensions:`, err.extensions);
}
});
// Clear the user's webhook URL using updateActiveUser (same as createWebhookFunction)
const updateUserMutation = gql`
mutation ClearActiveUserWebhookUrl($data: UserUpdateProfileInput!) {
updateActiveUser(data: $data) {
id
orderWebhookUrl
}
}
throw error;
}
`;
const result = await openFrontClient.request(updateUserMutation, {
data: { orderWebhookUrl: "" }
}) as any;
const updatedUser = result.updateActiveUser;
return {
success: true,
result: updatedUser,
deletedWebhookSubscriptionId: webhookId
};
}
// Function to get webhooks - get user's webhook URL
@@ -900,11 +710,8 @@ export async function addTrackingFunction({
data: { status: "shipped" },
});
console.log(`📦 OpenFront Channel: Tracking added for order ${purchaseId}: ${trackingCompany} ${trackingNumber}`);
return fulfillmentResult;
} catch (error) {
console.error('OpenFront Channel: Failed to add tracking:', error);
throw error;
}
}
@@ -1008,8 +815,7 @@ export async function oAuthCallbackFunction({
if (!response.ok) {
const errorText = await response.text();
console.error("OpenFront OAuth error:", errorText);
throw new Error(`Failed to exchange OAuth code for access token: ${response.statusText}`);
throw new Error(`Failed to exchange OAuth code for access token: ${response.statusText} - ${errorText}`);
}
const { access_token, refresh_token, expires_in } = await response.json();
@@ -1033,10 +839,6 @@ export async function createTrackingWebhookHandler({
event: any;
headers: Record<string, string>;
}) {
console.log('📦 [OpenFront Channel] createTrackingWebhookHandler called');
console.log('📦 [OpenFront Channel] Event type:', event.event);
console.log('📦 [OpenFront Channel] Payload:', JSON.stringify(event, null, 2));
// Validate event type
if (event.event !== 'order.fulfilled') {
throw new Error(`Unsupported event type: ${event.event}`);
@@ -1056,16 +858,11 @@ export async function createTrackingWebhookHandler({
// Get the first shipping label for tracking info
const shippingLabel = fulfillment.shippingLabels[0];
if (!shippingLabel.trackingNumber || !shippingLabel.carrier) {
throw new Error('Missing tracking number or carrier in shipping label');
}
console.log('📦 [OpenFront Channel] Tracking info extracted:');
console.log('📦 [OpenFront Channel] - Order ID:', order.id);
console.log('📦 [OpenFront Channel] - Tracking Number:', shippingLabel.trackingNumber);
console.log('📦 [OpenFront Channel] - Carrier:', shippingLabel.carrier);
// Return tracking data in the expected format (matching Shopify pattern)
return {
fulfillment: {
+8 -10
View File
@@ -157,8 +157,6 @@ export async function getProductFunction({
productId: string;
variantId?: string;
}) {
console.log("CHANNEL getProductFunction called with:", { platform: platform.domain, productId, variantId });
const shopifyClient = new GraphQLClient(
`https://${platform.domain}/admin/api/graphql.json`,
{
@@ -197,15 +195,12 @@ export async function getProductFunction({
`;
const fullVariantId = `gid://shopify/ProductVariant/${variantId}`;
console.log("CHANNEL querying with variantId:", fullVariantId);
const variantResult = await shopifyClient.request(gqlQuery, {
variantId: fullVariantId,
}) as any;
const { productVariant } = variantResult;
console.log("CHANNEL productVariant result:", productVariant);
if (!productVariant) {
throw new Error("Product not found from Shopify channel");
}
@@ -540,20 +535,23 @@ export async function getWebhooksFunction({
export async function oAuthFunction({
platform,
callbackUrl,
state,
}: {
platform: ShopifyPlatform;
callbackUrl: string;
state: string;
}) {
// Use platform's appKey if available, otherwise fall back to env variable for backward compatibility
const clientId = platform.appKey || process.env.SHOPIFY_APP_KEY;
if (!clientId) {
throw new Error("Shopify OAuth requires appKey in platform config or SHOPIFY_APP_KEY environment variable");
}
const scopes = "read_products,write_products,read_orders,write_orders,read_inventory,write_inventory";
const shopifyAuthUrl = `https://${platform.domain}/admin/oauth/authorize?client_id=${clientId}&scope=${scopes}&redirect_uri=${callbackUrl}&state=${Math.random().toString(36).substring(7)}`;
// Use the signed state passed from the caller (generated by generateOAuthState)
const shopifyAuthUrl = `https://${platform.domain}/admin/oauth/authorize?client_id=${clientId}&scope=${scopes}&redirect_uri=${encodeURIComponent(callbackUrl)}&state=${encodeURIComponent(state)}`;
return { authUrl: shopifyAuthUrl };
}
+5 -13
View File
@@ -9,24 +9,16 @@ export async function generateOAuthState(platformId: string, type: 'shop' | 'cha
platformId,
type,
timestamp: Date.now(),
nonce: crypto.randomBytes(16).toString('hex') // Small nonce for uniqueness
nonce: crypto.randomBytes(16).toString('hex')
};
console.log('🟢 GENERATE: Creating signed state for:', payload);
// Create signature
const payloadString = JSON.stringify(payload);
const signature = crypto.createHmac('sha256', SECRET_KEY).update(payloadString).digest('hex');
// Combine payload and signature
const signedState = {
payload: payloadString,
signature
};
// Encode as base64
const encodedState = Buffer.from(JSON.stringify(signedState)).toString('base64');
console.log('🟢 GENERATE: Generated signed state:', encodedState);
return encodedState;
return Buffer.from(JSON.stringify(signedState)).toString('base64');
}
@@ -88,7 +88,6 @@ export function verifyWebhook(
// Add more platforms as needed
default:
console.warn(`Unknown platform for webhook verification: ${platform}`)
return false
}
}
+2 -2
View File
@@ -99,11 +99,11 @@ export async function getShopWebhooks({ platform }: { platform: any }) {
});
}
export async function handleShopOAuth({ platform, callbackUrl }: { platform: any; callbackUrl: string }) {
export async function handleShopOAuth({ platform, callbackUrl, state }: { platform: any; callbackUrl: string; state: string }) {
return executeShopAdapterFunction({
platform,
functionName: "oAuthFunction",
args: { callbackUrl },
args: { callbackUrl, state },
});
}
+1 -9
View File
@@ -129,7 +129,6 @@ const getFreshAccessToken = async (platform: OpenFrontPlatform) => {
}
});
} catch (error) {
console.error('Failed to update shop tokens in database:', error);
// Continue with the request even if database update fails
}
@@ -296,7 +295,6 @@ export async function getProductFunction({
productId: string;
variantId?: string;
}) {
console.log("OpenFront getProductFunction called with:", { platform: platform.domain, productId, variantId });
const openFrontClient = await createOpenFrontClient(platform);
@@ -411,7 +409,6 @@ export async function searchOrdersFunction({
searchEntry: string;
after?: string;
}) {
console.log("fuckkk")
const openFrontClient = await createOpenFrontClient(platform);
const gqlQuery = gql`
@@ -501,7 +498,6 @@ export async function searchOrdersFunction({
skip,
}) as any;
console.log("Orders from OpenFront:", orders);
// Transform orders to Openship format
const transformedOrders = orders.map((order: any) => {
@@ -549,7 +545,6 @@ export async function searchOrdersFunction({
};
});
console.log("Transformed orders:", transformedOrders);
const hasNextPage = skip + take < ordersCount;
const endCursor = hasNextPage ? Buffer.from((skip + take).toString()).toString('base64') : null;
@@ -601,7 +596,6 @@ export async function updateProductFunction({
if (price !== undefined) {
// Note: Price updates in OpenFront might require updating the Price model separately
// This is a simplified approach - you might need to adjust based on your schema
console.log(`Price update requested for variant ${variantId}: ${price}`);
// Actual price update would depend on how prices are structured in OpenFront
}
@@ -797,8 +791,7 @@ export async function oAuthCallbackFunction({
if (!response.ok) {
const errorText = await response.text();
console.error("OpenFront OAuth error:", errorText);
throw new Error(`Failed to exchange OAuth code for access token: ${response.statusText}`);
throw new Error(`Failed to exchange OAuth code for access token: ${response.statusText} - ${errorText}`);
}
const { access_token, refresh_token, expires_in } = await response.json();
@@ -824,7 +817,6 @@ export async function createOrderWebhookHandler({
// Verify webhook authenticity using OpenFront's signature
const signature = headers["x-openfront-webhook-signature"] || headers["X-OpenFront-Webhook-Signature"];
if (!signature) {
console.error("Missing webhook signature. Available headers:", Object.keys(headers));
throw new Error("Missing webhook signature");
}
+9 -21
View File
@@ -149,8 +149,6 @@ export async function getProductFunction({
productId: string;
variantId?: string;
}) {
console.log("SHOP getProductFunction called with:", { platform: platform.domain, productId, variantId });
const shopifyClient = new GraphQLClient(
`https://${platform.domain}/admin/api/graphql.json`,
{
@@ -189,14 +187,11 @@ export async function getProductFunction({
`;
const fullVariantId = `gid://shopify/ProductVariant/${variantId}`;
console.log("SHOP querying with variantId:", fullVariantId);
const { productVariant } = await shopifyClient.request(gqlQuery, {
variantId: fullVariantId,
}) as any;
console.log("SHOP productVariant result:", productVariant);
if (!productVariant) {
throw new Error("Product not found from Shopify");
}
@@ -411,20 +406,11 @@ export async function updateProductFunction({
}
`;
console.log('QUERYING FOR VARIANT ID:', variantId);
console.log('FULL GID:', `gid://shopify/ProductVariant/${variantId}`);
const variantData = await shopifyClient.request(getVariantWithInventoryQuery, {
id: `gid://shopify/ProductVariant/${variantId}`,
});
console.log('VARIANT DATA RECEIVED:', JSON.stringify(variantData, null, 2));
if (!(variantData as any).productVariant?.inventoryItem?.id) {
console.log('FAILING BECAUSE NO inventoryItem.id');
console.log('variantData structure:', variantData);
console.log('productVariant:', (variantData as any).productVariant);
console.log('inventoryItem:', (variantData as any).productVariant?.inventoryItem);
throw new Error("Unable to find inventory item for variant");
}
@@ -482,7 +468,6 @@ export async function updateProductFunction({
})
);
} catch (inventoryError) {
console.error("Error updating inventory:", inventoryError);
throw inventoryError;
}
}
@@ -672,20 +657,23 @@ export async function getWebhooksFunction({
export async function oAuthFunction({
platform,
callbackUrl,
state,
}: {
platform: ShopifyPlatform;
callbackUrl: string;
state: string;
}) {
// Use platform's appKey if available, otherwise fall back to env variable for backward compatibility
const clientId = platform.appKey || process.env.SHOPIFY_APP_KEY;
if (!clientId) {
throw new Error("Shopify OAuth requires appKey in platform config or SHOPIFY_APP_KEY environment variable");
}
const scopes = "read_products,write_products,read_orders,write_orders,read_inventory,write_inventory";
const shopifyAuthUrl = `https://${platform.domain}/admin/oauth/authorize?client_id=${clientId}&scope=${scopes}&redirect_uri=${callbackUrl}&state=${Math.random().toString(36).substring(7)}`;
// Use the signed state passed from the caller (generated by generateOAuthState)
const shopifyAuthUrl = `https://${platform.domain}/admin/oauth/authorize?client_id=${clientId}&scope=${scopes}&redirect_uri=${encodeURIComponent(callbackUrl)}&state=${encodeURIComponent(state)}`;
return { authUrl: shopifyAuthUrl };
}
@@ -789,7 +777,7 @@ export async function createOrderWebhookHandler({
(result as any).productVariant?.product?.images?.edges?.[0]?.node?.originalSrc ||
null;
} catch (error) {
console.warn(`Failed to fetch image for variant ${item.variant_id}:`, error instanceof Error ? error.message : 'Unknown error');
// Failed to fetch image, continue with null
}
return {
@@ -39,10 +39,6 @@ async function addToCart(
const [existingCartItem] = allCartItems;
if (existingCartItem) {
console.log(
`There are already ${existingCartItem.quantity}, increment by 1!`
);
await context.query.CartItem.updateOne({
where: { id: existingCartItem.id },
data: {
@@ -11,7 +11,6 @@ async function searchChannelProductsQuery(
}: { channelId: string; searchEntry: string; after?: string },
context: any,
) {
console.log("helllooooo")
const sudoContext = context.sudo();
// Fetch the channel using the provided channelId
+7 -19
View File
@@ -55,11 +55,9 @@ export function statelessSessions({
if (authHeader?.startsWith("Bearer ")) {
const accessToken = authHeader.replace("Bearer ", "");
console.log('🔑 ACCESS TOKEN:', accessToken);
// Try to validate as API key first
if (accessToken.startsWith("osp_")) {
console.log('🔑 API KEY DETECTED, VALIDATING...');
try {
// Get all active API keys and test the token against each one
const apiKeys = await context.sudo().query.ApiKey.findMany({
@@ -76,8 +74,6 @@ export function statelessSessions({
`,
});
console.log('🔑 CHECKING AGAINST', apiKeys.length, 'ACTIVE API KEYS');
let matchingApiKey = null;
// Test token against each API key using bcryptjs (same as Keystone's default KDF)
@@ -99,28 +95,23 @@ export function statelessSessions({
if (isValid) {
matchingApiKey = apiKey;
console.log('🔑 FOUND MATCHING API KEY:', apiKey.id);
break;
}
} catch (error) {
console.log('🔑 ERROR VERIFYING API KEY:', error);
// Continue to next API key if this one doesn't match
continue;
}
}
if (!matchingApiKey) {
console.log('🔑 NO MATCHING API KEY FOUND');
return; // API key not found or invalid
}
if (matchingApiKey.status !== 'active') {
console.log('🔑 API KEY NOT ACTIVE:', matchingApiKey.status);
return; // API key is inactive
}
if (matchingApiKey.expiresAt && new Date() > new Date(matchingApiKey.expiresAt)) {
console.log('🔑 API KEY EXPIRED');
// Auto-revoke expired keys
await context.sudo().query.ApiKey.updateOne({
where: { id: matchingApiKey.id },
@@ -145,16 +136,13 @@ export function statelessSessions({
// Return user session with API key scopes attached
if (matchingApiKey.user?.id) {
const session = {
itemId: matchingApiKey.user.id,
return {
itemId: matchingApiKey.user.id,
listKey,
apiKeyScopes: matchingApiKey.scopes || [] // Attach scopes for permission checking
apiKeyScopes: matchingApiKey.scopes || []
};
console.log('🔑 RETURNING SESSION:', JSON.stringify(session, null, 2));
return session;
}
} catch (err) {
console.log('🔑 API Key validation error:', err);
return;
}
}
@@ -148,14 +148,16 @@ export async function getChannelWebhooks({ platform }: { platform: Platform }) {
export async function handleChannelOAuth({
platform,
callbackUrl,
state,
}: {
platform: Platform;
callbackUrl: string;
state: string;
}) {
return executeChannelAdapterFunction({
platform,
functionName: 'oAuthFunction',
args: { callbackUrl },
args: { callbackUrl, state },
});
}
@@ -102,11 +102,11 @@ export async function getShopWebhooks({ platform }: any) {
});
}
export async function handleShopOAuth({ platform, callbackUrl }: any) {
export async function handleShopOAuth({ platform, callbackUrl, state }: any) {
return executeShopAdapterFunction({
platform,
functionName: "oAuthFunction",
args: { callbackUrl },
args: { callbackUrl, state },
});
}
@@ -160,7 +160,6 @@ ${uniqueViews.map(view => ` "${view}"`).join(',\n')}
];`;
writeFileSync(outputPath, fileContent);
console.log(`Generated view order with ${uniqueViews.length} field types: ${uniqueViews.join(', ')}`);
}
function extractUniqueViews(jsonData: any) {
@@ -100,18 +100,18 @@ export async function initiateChannelOAuthFlow(platformId: string, domain: strin
// Generate state parameter with platform info
const state = await generateOAuthState(platformId, 'channel');
// The platform object needs to have the domain from user input
const platformWithDomain = {
...platform,
domain: domain, // Use the domain entered by the user
state: state, // Pass state to OAuth function
};
// Call the OAuth function to get the auth URL - use platform's callbackUrl
// Call the OAuth function to get the auth URL - pass state as separate argument
const result = await handleChannelOAuth({
platform: platformWithDomain,
callbackUrl: platform.callbackUrl || `${process.env.NEXT_PUBLIC_URL || 'http://localhost:3000'}/api/oauth/callback`
callbackUrl: platform.callbackUrl || `${process.env.NEXT_PUBLIC_URL || 'http://localhost:3000'}/api/oauth/callback`,
state: state,
});
// Redirect to the OAuth URL
@@ -3,9 +3,9 @@ import React, { useMemo, useState } from "react";
import { useList } from "@/features/dashboard/hooks/useAdminMeta";
import { useCreateItem } from "@/features/dashboard/utils/useCreateItem";
import { enhanceFields } from "@/features/dashboard/utils/enhanceFields";
import { useRouter } from "next/navigation";
import { useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import {
Dialog,
DialogContent,
@@ -40,7 +40,6 @@ const channelAdapters = {
export function CreatePlatform({ trigger }: { trigger: React.ReactNode }) {
const [selectedPlatform, setSelectedPlatform] = useState<string | undefined>(undefined);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const router = useRouter();
const queryClient = useQueryClient();
const { list } = useList('ChannelPlatform');
@@ -73,15 +72,28 @@ export function CreatePlatform({ trigger }: { trigger: React.ReactNode }) {
const keysToUpdateTemplate = ["name", "appKey", "appSecret"];
const handlePlatformActivation = async () => {
if (!createItem) return
if (!createItem) return;
const item = await createItem.create()
if (item?.id) {
setIsDialogOpen(false);
// Refresh the page to show the new platform
await queryClient.invalidateQueries({
queryKey: ['lists', 'ChannelPlatform', 'items']
});
try {
const item = await createItem.create();
if (item?.id) {
toast.success('Platform created successfully');
setIsDialogOpen(false);
// Refresh the page to show the new platform
await queryClient.invalidateQueries({
queryKey: ['lists', 'ChannelPlatform', 'items'],
exact: false
});
} else {
// Creation returned but no item - likely a validation error
const errorMessage = createItem.error?.graphQLErrors?.[0]?.message
|| createItem.error?.message
|| 'Failed to create platform';
toast.error(errorMessage);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'An error occurred while creating the platform';
toast.error(errorMessage);
}
};
@@ -116,8 +128,7 @@ export function CreatePlatform({ trigger }: { trigger: React.ReactNode }) {
} else {
// Use template slug for other platforms (shopify, etc.)
const templateValue = { kind: 'create' as const, inner: { kind: 'value' as const, value: value } };
const emptyValue = { kind: 'create' as const, inner: { kind: 'value' as const, value: "" } };
functionValues = {
createPurchaseFunction: templateValue,
getWebhooksFunction: templateValue,
@@ -127,9 +138,9 @@ export function CreatePlatform({ trigger }: { trigger: React.ReactNode }) {
getProductFunction: templateValue,
cancelPurchaseWebhookHandler: templateValue,
createTrackingWebhookHandler: templateValue,
// Leave OAuth functions empty for Shopify since users likely don't have app keys yet
oAuthFunction: value === 'shopify' ? emptyValue : templateValue,
oAuthCallbackFunction: value === 'shopify' ? emptyValue : templateValue,
// OAuth functions should use the template slug so the adapter can be resolved
oAuthFunction: templateValue,
oAuthCallbackFunction: templateValue,
};
}
+12 -50
View File
@@ -17,17 +17,6 @@ export interface CreateShopInput {
}
export async function createShop(data: CreateShopInput) {
console.log('🏪 createShop action called with data:');
console.log(' - name:', data.name);
console.log(' - domain:', data.domain);
console.log(' - accessToken present:', !!data.accessToken);
console.log(' - refreshToken present:', !!data.refreshToken);
console.log(' - refreshToken value:', data.refreshToken ? `${data.refreshToken.substring(0, 10)}...` : 'MISSING');
console.log(' - tokenExpiresAt present:', !!data.tokenExpiresAt);
console.log(' - tokenExpiresAt value:', data.tokenExpiresAt);
console.log(' - platformId:', data.platformId);
console.log(' - platform.create present:', !!data.platform?.create);
const mutation = `
mutation CreateShop($data: ShopCreateInput!) {
createShop(data: $data) {
@@ -49,14 +38,14 @@ export async function createShop(data: CreateShopInput) {
let platformData;
if (data.platformId) {
// Existing platform - connect by ID
console.log('🔗 Using existing platform ID:', data.platformId);
// Using existing platform ID
platformData = { connect: { id: data.platformId } };
} else if (data.platform?.create) {
// Inline platform creation
console.log('📦 Creating platform inline:', data.platform.create);
// Creating platform inline
platformData = { create: data.platform.create };
} else {
console.log('❌ No platform connection method provided');
// No platform connection method provided
throw new Error('Either platformId or platform.create must be provided');
}
@@ -77,51 +66,24 @@ export async function createShop(data: CreateShopInput) {
}
};
console.log('🔍 Building GraphQL variables:');
console.log(' - Base variables:', JSON.stringify(variables, null, 2));
if (data.refreshToken) {
console.log('🔑 Adding refreshToken to variables:', data.refreshToken ? `${data.refreshToken.substring(0, 10)}...` : 'MISSING');
variables.data.refreshToken = data.refreshToken;
} else {
console.log('❌ No refreshToken to add');
}
if (data.tokenExpiresAt) {
console.log('⏰ Adding tokenExpiresAt to variables:', data.tokenExpiresAt);
variables.data.tokenExpiresAt = data.tokenExpiresAt.toISOString();
} else {
console.log('❌ No tokenExpiresAt to add');
}
variables.data.platform = platformData;
console.log('🚀 Final GraphQL variables being sent:');
console.log(JSON.stringify(variables, null, 2));
const response = await keystoneClient(mutation, variables);
console.log('📨 GraphQL response received:');
console.log(' - success:', response.success);
console.log(' - data:', JSON.stringify(response.data, null, 2));
console.log(' - error:', response.error);
if (response.success && response.data?.createShop) {
console.log('✅ Shop created successfully:');
console.log(' - ID:', response.data.createShop.id);
console.log(' - Name:', response.data.createShop.name);
console.log(' - Domain:', response.data.createShop.domain);
console.log(' - Has accessToken:', !!response.data.createShop.accessToken);
console.log(' - Has refreshToken:', !!response.data.createShop.refreshToken);
console.log(' - TokenExpiresAt:', response.data.createShop.tokenExpiresAt);
return { success: true, shop: response.data.createShop };
} else {
console.log('❌ Shop creation failed:');
console.log(' - Response success:', response.success);
console.log(' - Response error:', response.error);
console.log(' - Full response:', JSON.stringify(response, null, 2));
return {
success: false,
error: response.error || 'Failed to create shop'
return {
success: false,
error: response.error || 'Failed to create shop'
};
}
}
@@ -159,18 +121,18 @@ export async function initiateOAuthFlow(platformId: string, domain: string) {
// Generate state parameter with platform info
const state = await generateOAuthState(platformId, 'shop');
// The platform object needs to have the domain from user input
const platformWithDomain = {
...platform,
domain: domain, // Use the domain entered by the user
state: state, // Pass state to OAuth function
};
// Call the OAuth function to get the auth URL - use platform's callbackUrl
// Call the OAuth function to get the auth URL - pass state as separate argument
const result = await handleShopOAuth({
platform: platformWithDomain,
callbackUrl: platform.callbackUrl || `${process.env.NEXT_PUBLIC_URL || 'http://localhost:3000'}/api/oauth/callback`
callbackUrl: platform.callbackUrl || `${process.env.NEXT_PUBLIC_URL || 'http://localhost:3000'}/api/oauth/callback`,
state: state,
});
// Redirect to the OAuth URL
@@ -3,9 +3,9 @@ import React, { useMemo, useState } from "react";
import { useList } from "@/features/dashboard/hooks/useAdminMeta";
import { useCreateItem } from "@/features/dashboard/utils/useCreateItem";
import { enhanceFields } from "@/features/dashboard/utils/enhanceFields";
import { useRouter } from "next/navigation";
import { useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import {
Dialog,
DialogContent,
@@ -100,7 +100,6 @@ export function getFilteredProps(props: any, modifications: any[], defaultCollap
export function CreatePlatform({ trigger }: { trigger: React.ReactNode }) {
const [selectedPlatform, setSelectedPlatform] = useState<string | undefined>(undefined);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const router = useRouter();
const queryClient = useQueryClient();
const { list } = useList('ShopPlatform');
@@ -137,15 +136,28 @@ export function CreatePlatform({ trigger }: { trigger: React.ReactNode }) {
const keysToUpdateTemplate = ["name", "appKey", "appSecret"];
const handlePlatformActivation = async () => {
if (!createItem) return
if (!createItem) return;
const item = await createItem.create()
if (item?.id) {
setIsDialogOpen(false);
// Refresh the page to show the new platform
await queryClient.invalidateQueries({
queryKey: ['lists', 'ShopPlatform', 'items']
});
try {
const item = await createItem.create();
if (item?.id) {
toast.success('Platform created successfully');
setIsDialogOpen(false);
// Refresh the page to show the new platform
await queryClient.invalidateQueries({
queryKey: ['lists', 'ShopPlatform', 'items'],
exact: false
});
} else {
// Creation returned but no item - likely a validation error
const errorMessage = createItem.error?.graphQLErrors?.[0]?.message
|| createItem.error?.message
|| 'Failed to create platform';
toast.error(errorMessage);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'An error occurred while creating the platform';
toast.error(errorMessage);
}
};
@@ -184,11 +196,10 @@ export function CreatePlatform({ trigger }: { trigger: React.ReactNode }) {
} else {
// Use template slug for other platforms (shopify, etc.)
const templateValue = { kind: 'create' as const, inner: { kind: 'value' as const, value: value } };
const emptyValue = { kind: 'create' as const, inner: { kind: 'value' as const, value: "" } };
functionValues = {
orderLinkFunction: templateValue,
updateProductFunction: templateValue,
updateProductFunction: templateValue,
getWebhooksFunction: templateValue,
deleteWebhookFunction: templateValue,
createWebhookFunction: templateValue,
@@ -197,9 +208,9 @@ export function CreatePlatform({ trigger }: { trigger: React.ReactNode }) {
searchOrdersFunction: templateValue,
addTrackingFunction: templateValue,
addCartToPlatformOrderFunction: templateValue,
// Leave OAuth functions empty for Shopify since users likely don't have app keys yet
oAuthFunction: value === 'shopify' ? emptyValue : templateValue,
oAuthCallbackFunction: value === 'shopify' ? emptyValue : templateValue,
// OAuth functions should use the template slug so the adapter can be resolved
oAuthFunction: templateValue,
oAuthCallbackFunction: templateValue,
cancelOrderWebhookHandler: templateValue,
createOrderWebhookHandler: templateValue,
};
@@ -249,11 +249,7 @@ export const Webhooks = ({ shopId, shop }: { shopId: string; shop?: any }) => {
<div className="space-y-3">
{recommendedWebhooks.map((webhook: any) => {
const fullRecommendedUrl = (typeof window !== 'undefined' ? window.location.origin : '') + webhook.callbackUrl;
console.log('🔍 Checking webhook:', webhook.topic);
console.log('🔍 Full recommended URL:', fullRecommendedUrl);
console.log('🔍 Existing webhooks:', webhooks.map((w: any) => ({ topic: w.topic, callbackUrl: w.callbackUrl })));
const existingWebhook = webhooks.find(
(w: any) => {
const existingTopic = Array.isArray(w.topic) ? w.topic[0] : w.topic;
+4462 -2552
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -75,9 +75,9 @@
"next-themes": "^0.4.6",
"nodemailer": "^7.0.5",
"pluralize": "^8.0.0",
"react": "^19.0.0",
"react": "^19.2.1",
"react-day-picker": "^9.7.0",
"react-dom": "^19.0.0",
"react-dom": "^19.2.1",
"react-hook-form": "^7.54.1",
"react-markdown": "^10.1.0",
"react-resizable-panels": "^3.0.2",
@@ -120,7 +120,7 @@
"node": ">=20.0.0"
},
"overrides": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react": "^19.2.1",
"react-dom": "^19.2.1"
}
}