Skip to content

Commit

Permalink
Move ensureInstalledOnShop into auth code flow strategy
Browse files Browse the repository at this point in the history
  • Loading branch information
rezaansyed committed Nov 14, 2023
1 parent 406ec00 commit 40ea175
Show file tree
Hide file tree
Showing 2 changed files with 149 additions and 131 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import {redirect} from '@remix-run/server-runtime';
import {
GraphqlQueryError,
HttpResponseError,
JwtPayload,
Session,
Shopify,
Expand Down Expand Up @@ -33,11 +31,9 @@ import type {
SessionContext,
} from './types';
import {
beginAuth,
createAdminApiContext,
redirectFactory,
redirectToShopifyOrAppRoot,
redirectWithExitIframe,
} from './helpers';
import {AuthCodeFlowStrategy} from './strategies/auth-code-flow';

Expand Down Expand Up @@ -122,7 +118,7 @@ export class AuthStrategy<

if (!sessionTokenHeader) {
await this.validateUrlParams(request);
await this.ensureInstalledOnShop(request);
await strategy.ensureInstalledOnShop(request, params);
await this.ensureAppIsEmbeddedIfRequired(request);
await this.ensureSessionTokenSearchParamIfRequired(request);
}
Expand Down Expand Up @@ -155,131 +151,6 @@ export class AuthStrategy<
}
}

private async getOfflineToken(request: Request) {
const {api, config, logger} = this;
const url = new URL(request.url);

const shop = url.searchParams.get('shop');

const offlineId = shop
? api.session.getOfflineId(shop)
: await api.session.getCurrentId({isOnline: false, rawRequest: request});

if (!offlineId) {
logger.info("Could not find a shop, can't authenticate request");
throw new Response(undefined, {
status: 400,
statusText: 'Bad Request',
});
}

return config.sessionStorage.loadSession(offlineId);
}

private async ensureInstalledOnShop(request: Request) {
const {api, config, logger} = this;
const url = new URL(request.url);

let shop = url.searchParams.get('shop');

// Ensure app is installed
logger.debug('Ensuring app is installed on shop', {shop});

const offlineSession = await this.getOfflineToken(request);
const isEmbedded = url.searchParams.get('embedded') === '1';

if (!offlineSession) {
logger.info("Shop hasn't installed app yet, redirecting to OAuth", {
shop,
});
if (isEmbedded) {
redirectWithExitIframe({api, config, logger}, request, shop!);
} else {
throw await beginAuth({api, config, logger}, request, false, shop!);
}
}

shop = shop || offlineSession.shop;

if (config.isEmbeddedApp && !isEmbedded) {
try {
logger.debug('Ensuring offline session is valid before embedding', {
shop,
});
await this.testSession(offlineSession);

logger.debug('Offline session is still valid, embedding app', {shop});
} catch (error) {
await this.handleInvalidOfflineSession(
error,
{api, logger, config},
request,
shop,
);
}
}
}

private async handleInvalidOfflineSession(
error: Error,
params: BasicParams,
request: Request,
shop: string,
) {
const {api, logger, config} = params;
if (error instanceof HttpResponseError) {
if (error.response.code === 401) {
logger.info('Shop session is no longer valid, redirecting to OAuth', {
shop,
});
throw await beginAuth({api, config, logger}, request, false, shop);
} else {
const message = JSON.stringify(error.response.body, null, 2);
logger.error(`Unexpected error during session validation: ${message}`, {
shop,
});

throw new Response(undefined, {
status: error.response.code,
statusText: error.response.statusText,
});
}
} else if (error instanceof GraphqlQueryError) {
const context: {[key: string]: string} = {shop};
if (error.response) {
context.response = JSON.stringify(error.response);
}

logger.error(
`Unexpected error during session validation: ${error.message}`,
context,
);

throw new Response(undefined, {
status: 500,
statusText: 'Internal Server Error',
});
}
}

private async testSession(session: Session): Promise<void> {
const {api} = this;

const client = new api.clients.Graphql({
session,
});

await client.query({
data: `#graphql
query shopifyAppShopName {
shop {
name
}
}
`,
});
}

private async ensureAppIsEmbeddedIfRequired(request: Request) {
const {api, logger, config} = this;
const url = new URL(request.url);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {
CookieNotFound,
GraphqlQueryError,
HttpResponseError,
InvalidHmacError,
InvalidOAuthError,
JwtPayload,
Session,
ShopifyRestResources,
} from '@shopify/shopify-api';

Expand Down Expand Up @@ -47,6 +49,88 @@ export class AuthCodeFlowStrategy<
return sessionContext!;
}

async ensureInstalledOnShop(request: Request, params: BasicParams) {
const {api, config, logger} = params;
const url = new URL(request.url);

let shop = url.searchParams.get('shop');

// Ensure app is installed
logger.debug('Ensuring app is installed on shop', {shop});

if (!(await this.hasValidOfflineId(request, params))) {
logger.info("Could not find a shop, can't authenticate request");
throw new Response(undefined, {
status: 400,
statusText: 'Bad Request',
});
}

const offlineSession = await this.getOfflineSession(request, params);
const isEmbedded = url.searchParams.get('embedded') === '1';

if (!offlineSession) {
logger.info("Shop hasn't installed app yet, redirecting to OAuth", {
shop,
});
if (isEmbedded) {
redirectWithExitIframe({api, config, logger}, request, shop!);
} else {
throw await beginAuth({api, config, logger}, request, false, shop!);
}
}

shop = shop || offlineSession.shop;

if (config.isEmbeddedApp && !isEmbedded) {
try {
logger.debug('Ensuring offline session is valid before embedding', {
shop,
});
await this.testSession(offlineSession, params);

logger.debug('Offline session is still valid, embedding app', {shop});
} catch (error) {
await this.handleInvalidOfflineSession(
error,
{api, logger, config},
request,
shop,
);
}
}
}

private async getOfflineSession(request: Request, params: BasicParams) {
const {api, config} = params;
const url = new URL(request.url);

const shop = url.searchParams.get('shop');

const offlineId = shop
? api.session.getOfflineId(shop)
: await api.session.getCurrentId({isOnline: false, rawRequest: request});

if (!offlineId) {
return null;
}

return config.sessionStorage.loadSession(offlineId);
}

private async hasValidOfflineId(request: Request, params: BasicParams) {
const {api} = params;
const url = new URL(request.url);

const shop = url.searchParams.get('shop');

const offlineId = shop
? api.session.getOfflineId(shop)
: await api.session.getCurrentId({isOnline: false, rawRequest: request});

return Boolean(offlineId);
}

private async handleBouncePageRoute(request: Request, params: BasicParams) {
const {config, logger} = params;
const url = new URL(request.url);
Expand Down Expand Up @@ -142,6 +226,27 @@ export class AuthCodeFlowStrategy<
}
}

private async testSession(
session: Session,
params: BasicParams,
): Promise<void> {
const {api} = params;

const client = new api.clients.Graphql({
session,
});

await client.query({
data: `#graphql
query shopifyAppShopName {
shop {
name
}
}
`,
});
}

private async oauthCallbackError(
params: BasicParams,
error: Error,
Expand Down Expand Up @@ -170,4 +275,46 @@ export class AuthCodeFlowStrategy<
statusText: 'Internal Server Error',
});
}

private async handleInvalidOfflineSession(
error: Error,
params: BasicParams,
request: Request,
shop: string,
) {
const {api, logger, config} = params;
if (error instanceof HttpResponseError) {
if (error.response.code === 401) {
logger.info('Shop session is no longer valid, redirecting to OAuth', {
shop,
});
throw await beginAuth({api, config, logger}, request, false, shop);
} else {
const message = JSON.stringify(error.response.body, null, 2);
logger.error(`Unexpected error during session validation: ${message}`, {
shop,
});

throw new Response(undefined, {
status: error.response.code,
statusText: error.response.statusText,
});
}
} else if (error instanceof GraphqlQueryError) {
const context: {[key: string]: string} = {shop};
if (error.response) {
context.response = JSON.stringify(error.response);
}

logger.error(
`Unexpected error during session validation: ${error.message}`,
context,
);

throw new Response(undefined, {
status: 500,
statusText: 'Internal Server Error',
});
}
}
}

0 comments on commit 40ea175

Please sign in to comment.