mirror of
https://github.com/openshiporg/openship.git
synced 2026-06-19 07:35:55 +00:00
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:
+11
-21
@@ -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
@@ -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 },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Generated
+4462
-2552
File diff suppressed because it is too large
Load Diff
+4
-4
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user