Skip to content

Commit

Permalink
Exporting flow authentication method from remix package
Browse files Browse the repository at this point in the history
  • Loading branch information
paulomarg committed Jan 31, 2024
1 parent b36cc6d commit a91fe95
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import {adminClientFactory} from 'src/server/clients/admin';
import {BasicParams} from 'src/server/types';
import {ShopifyRestResources} from '@shopify/shopify-api';

export function authenticateFlowFactory(params: BasicParams) {
import {adminClientFactory} from '../../clients/admin';
import {BasicParams} from '../../types';

import type {AuthenticateFlow, FlowContext} from './types';

export function authenticateFlowFactory<
Resources extends ShopifyRestResources = ShopifyRestResources,
>(params: BasicParams): AuthenticateFlow<Resources> {
const {api, config, logger} = params;

return async function authenticate(request: Request) {
return async function authenticate(
request: Request,
): Promise<FlowContext<Resources>> {
logger.info('Authenticating flow request');

if (request.method !== 'POST') {
Expand All @@ -19,19 +27,26 @@ export function authenticateFlowFactory(params: BasicParams) {
}

const rawBody = await request.text();
const {valid} = await api.flow.validate({
const result = await api.flow.validate({
rawBody,
rawRequest: request,
});

if (!valid) {
if (!result.valid) {
logger.error('Received an invalid flow request', {reason: result.reason});

throw new Response(undefined, {
status: 400,
statusText: 'Bad Request',
});
}

const payload = JSON.parse(rawBody);

logger.debug('Flow request is valid, looking for an offline session', {
shop: payload.shopify_domain,
});

const sessionId = api.session.getOfflineId(payload.shopify_domain);
const session = await config.sessionStorage.loadSession(sessionId);

Expand All @@ -45,10 +60,12 @@ export function authenticateFlowFactory(params: BasicParams) {
});
}

logger.debug('Found a session for the flow request', {shop: session.shop});

return {
session,
payload,
admin: adminClientFactory({params, session}),
admin: adminClientFactory<Resources>({params, session}),
};
};
}
15 changes: 15 additions & 0 deletions packages/shopify-app-remix/src/server/authenticate/flow/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {Session, ShopifyRestResources} from '@shopify/shopify-api';

import type {AdminApiContext} from '../../clients';

export interface FlowContext<
Resources extends ShopifyRestResources = ShopifyRestResources,
> {
session: Session;
payload: any;
admin: AdminApiContext<Resources>;
}

export type AuthenticateFlow<
Resources extends ShopifyRestResources = ShopifyRestResources,
> = (request: Request) => Promise<FlowContext<Resources>>;
2 changes: 2 additions & 0 deletions packages/shopify-app-remix/src/server/shopify-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {unauthenticatedStorefrontContextFactory} from './unauthenticated/storefr
import {AuthCodeFlowStrategy} from './authenticate/admin/strategies/auth-code-flow';
import {TokenExchangeStrategy} from './authenticate/admin/strategies/token-exchange';
import {IdempotentPromiseHandler} from './authenticate/helpers/idempotent-promise-handler';
import {authenticateFlowFactory} from './authenticate/flow/authenticate';

/**
* Creates an object your app will use to interact with Shopify.
Expand Down Expand Up @@ -85,6 +86,7 @@ export function shopifyApp<
registerWebhooks: registerWebhooksFactory(params),
authenticate: {
admin: authStrategy,
flow: authenticateFlowFactory<Resources>(params),
public: authenticatePublicFactory<Config['future'], Resources>(params),
webhook: authenticateWebhookFactory<
Config['future'],
Expand Down
54 changes: 43 additions & 11 deletions packages/shopify-app-remix/src/server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import type {
RegisterWebhooksOptions,
} from './authenticate/webhooks/types';
import type {AuthenticatePublic} from './authenticate/public/types';
import type {AdminContext} from './authenticate/admin/types';
import type {AuthenticateAdmin} from './authenticate/admin/types';
import type {Unauthenticated} from './unauthenticated/types';
import {FutureFlagOptions, FutureFlags} from './future/flags';
import type {FutureFlagOptions, FutureFlags} from './future/flags';
import type {AuthenticateFlow} from './authenticate/flow/types';

export interface BasicParams<
Future extends FutureFlagOptions = FutureFlagOptions,
Expand Down Expand Up @@ -83,11 +84,6 @@ type Login = (request: Request) => Promise<LoginError | never>;

type AddDocumentResponseHeaders = (request: Request, headers: Headers) => void;

type AuthenticateAdmin<
Config extends AppConfigArg,
Resources extends ShopifyRestResources = ShopifyRestResources,
> = (request: Request) => Promise<AdminContext<Config, Resources>>;

type RestResourcesType<Config extends AppConfigArg> =
Config['restResources'] extends ShopifyRestResources
? Config['restResources']
Expand All @@ -109,6 +105,17 @@ interface Authenticate<Config extends AppConfigArg> {
* @example
* <caption>Authenticating a request for an embedded app.</caption>
* ```ts
* // /app/routes/**\/*.jsx
* import { LoaderFunctionArgs, json } from "@remix-run/node";
* import { authenticate } from "../../shopify.server";
*
* export async function loader({ request }: LoaderFunctionArgs) {
* const {admin, session, sessionToken, billing} = authenticate.admin(request);
*
* return json(await admin.rest.resources.Product.count({ session }));
* }
* ```
* ```ts
* // /app/shopify.server.ts
* import { LATEST_API_VERSION, shopifyApp } from "@shopify/shopify-app-remix/server";
* import { restResources } from "@shopify/shopify-api/rest/admin/2023-04";
Expand All @@ -120,19 +127,44 @@ interface Authenticate<Config extends AppConfigArg> {
* export default shopify;
* export const authenticate = shopify.authenticate;
* ```
*/
admin: AuthenticateAdmin<Config, RestResourcesType<Config>>;

/**
* Authenticate a Flow extension Request and get back an authenticated context, containing an admin context to access
* the API, and the payload of the request.
*
* If there is no session for the Request, this will return an HTTP 400 error.
*
* Note that this will always be a POST request.
*
* @example
* <caption>Authenticating a Flow extension request.</caption>
* ```ts
* // /app/routes/**\/*.jsx
* import { LoaderFunctionArgs, json } from "@remix-run/node";
* import { ActionFunctionArgs, json } from "@remix-run/node";
* import { authenticate } from "../../shopify.server";
*
* export async function loader({ request }: LoaderFunctionArgs) {
* const {admin, session, sessionToken, billing} = authenticate.admin(request);
* export async function action({ request }: ActionFunctionArgs) {
* const {admin, session, payload} = authenticate.flow(request);
*
* return json(await admin.rest.resources.Product.count({ session }));
* }
* ```
* ```ts
* // /app/shopify.server.ts
* import { LATEST_API_VERSION, shopifyApp } from "@shopify/shopify-app-remix/server";
* import { restResources } from "@shopify/shopify-api/rest/admin/2023-04";
*
* const shopify = shopifyApp({
* restResources,
* // ...etc
* });
* export default shopify;
* export const authenticate = shopify.authenticate;
* ```
*/
admin: AuthenticateAdmin<Config, RestResourcesType<Config>>;
flow: AuthenticateFlow<RestResourcesType<Config>>;

/**
* Authenticate a public request and get back a session token.
Expand Down

0 comments on commit a91fe95

Please sign in to comment.