UNPKG

@scayle/storefront-core

Version:

Collection of essential utilities to work with the Storefront API

364 lines (363 loc) 10.9 kB
import { decodeJwt } from "jose"; import { hasSession } from "../../types/index.mjs"; import { DEFAULT_WITH_LISTING, HttpStatusCode, HttpStatusMessage } from "../../constants/index.mjs"; import { FetchError } from "../../utils/fetch.mjs"; import { mergeBaskets, mergeWishlists } from "../../utils/user.mjs"; import { fetchUser } from "./user.mjs"; import { unwrap } from "../../utils/response.mjs"; import { getOAuthClient, OAuthRequestError } from "../../api/oauth.mjs"; import { ErrorResponse } from "../../errors/index.mjs"; import { defineRpcHandler } from "../../utils/index.mjs"; export const convertErrorForRpcCall = (error, httpStatuses) => { let fetchError; if (error instanceof FetchError) { fetchError = error; } else if (error instanceof OAuthRequestError && error.cause instanceof FetchError) { fetchError = error.cause; } if (fetchError && httpStatuses.includes(fetchError.response.status)) { return new ErrorResponse( fetchError.response.status, fetchError.response.statusText, fetchError.name ); } }; export async function postLogin(context, tokens) { const fetchUserResponse = await fetchUser( { accessToken: tokens.access_token }, context ); if (fetchUserResponse instanceof ErrorResponse) { return fetchUserResponse; } const user = await unwrap(fetchUserResponse); context.updateTokens({ accessToken: tokens.access_token, refreshToken: tokens.refresh_token }); context.updateUser(user); const { customerId } = decodeJwt(tokens.access_token); await Promise.all([ mergeBaskets( context.sessionId, await context.generateBasketKeyForUserId(customerId), DEFAULT_WITH_LISTING, context ), mergeWishlists( context.sessionId, await context.generateWishlistKeyForUserId(customerId), DEFAULT_WITH_LISTING, context ) ]); await context.createUserBoundSession(); await context.callHook?.( "storefront:afterLogin", { shopId: context.shopId, user, authenticationType: user.authentication?.type, accessToken: tokens.access_token }, context ); } export const oauthLogin = defineRpcHandler( async (login, context) => { if (!hasSession(context)) { return new ErrorResponse( HttpStatusCode.BAD_REQUEST, HttpStatusMessage.BAD_REQUEST, "No Session found" ); } const shopId = context.shopId; const client = getOAuthClient(context); if (!login.email || !login.password) { return new ErrorResponse( HttpStatusCode.BAD_REQUEST, HttpStatusMessage.BAD_REQUEST, "Login or password are missing" ); } try { const tokens = await client.login({ ...login, shop_id: shopId }); const postLoginResponse = await postLogin(context, tokens); if (postLoginResponse instanceof ErrorResponse) { return postLoginResponse; } return new Response(null, { status: HttpStatusCode.NO_CONTENT }); } catch (error) { context.log.error("OAuthClient.login failed", error); return convertErrorForRpcCall(error, [ HttpStatusCode.BAD_REQUEST, HttpStatusCode.NOT_FOUND ]) ?? new ErrorResponse( HttpStatusCode.INTERNAL_SERVER_ERROR, HttpStatusMessage.INTERNAL_SERVER_ERROR, "Login failed" ); } }, { method: "POST" } ); export const oauthRegister = defineRpcHandler( async (register, context) => { if (!hasSession(context)) { return new ErrorResponse( HttpStatusCode.BAD_REQUEST, HttpStatusMessage.BAD_REQUEST, "No Session found" ); } const shopId = context.shopId; const client = getOAuthClient(context); try { const tokens = await client.register({ ...register, shop_id: shopId }); const postLoginResponse = await postLogin(context, tokens); if (postLoginResponse instanceof ErrorResponse) { return postLoginResponse; } return new Response(null, { status: HttpStatusCode.NO_CONTENT }); } catch (error) { context.log.error("OAuthClient.register failed", error); return convertErrorForRpcCall(error, [ HttpStatusCode.BAD_REQUEST, HttpStatusCode.CONFLICT, HttpStatusCode.UNPROCESSABLE_CONTENT ]) ?? new ErrorResponse( HttpStatusCode.INTERNAL_SERVER_ERROR, HttpStatusMessage.INTERNAL_SERVER_ERROR, "Register failed" ); } }, { method: "POST" } ); export const oauthGuestLogin = defineRpcHandler( async (guest, context) => { if (!hasSession(context)) { return new ErrorResponse( HttpStatusCode.BAD_REQUEST, HttpStatusMessage.BAD_REQUEST, "No Session found" ); } const shopId = context.shopId; const client = getOAuthClient(context); try { const tokens = await client.guestLogin({ ...guest, shop_id: shopId }); const postLoginResponse = await postLogin(context, tokens); if (postLoginResponse instanceof ErrorResponse) { return postLoginResponse; } return new Response(null, { status: HttpStatusCode.NO_CONTENT }); } catch (error) { context.log.error("OAuthClient.guestLogin failed", error); return convertErrorForRpcCall(error, [ HttpStatusCode.BAD_REQUEST, HttpStatusCode.CONFLICT ]) ?? new ErrorResponse( HttpStatusCode.INTERNAL_SERVER_ERROR, HttpStatusMessage.INTERNAL_SERVER_ERROR, "Guest login failed" ); } }, { method: "POST" } ); export const refreshAccessToken = defineRpcHandler( async (context) => { if (!hasSession(context)) { return new ErrorResponse( HttpStatusCode.BAD_REQUEST, HttpStatusMessage.BAD_REQUEST, "No Session found" ); } const refreshToken = context.refreshToken; const client = getOAuthClient(context); if (!refreshToken) { return new ErrorResponse( HttpStatusCode.BAD_REQUEST, HttpStatusMessage.BAD_REQUEST, "No refresh token present" ); } try { const tokens = await client.refreshToken({ grant_type: "refresh_token", refresh_token: refreshToken }); context.updateTokens({ accessToken: tokens?.access_token, refreshToken: tokens?.refresh_token }); return { success: !!tokens }; } catch (error) { const response = convertErrorForRpcCall(error, [ HttpStatusCode.BAD_REQUEST, HttpStatusCode.UNAUTHORIZED ]); if (response && response.status === HttpStatusCode.UNAUTHORIZED) { context.log.debug( "Failed to refresh Checkout Token due to invalid refresh token. Deleting session" ); await context.destroySession(); } else { context.log.debug( "Failed to refresh Checkout Token for unknown reason." ); } if (response) { return response; } return { success: false }; } }, { method: "POST" } ); export const oauthRevokeToken = defineRpcHandler( async (context) => { if (!hasSession(context)) { return new ErrorResponse( HttpStatusCode.BAD_REQUEST, HttpStatusMessage.BAD_REQUEST, "No Session found" ); } const accessToken = context.accessToken; if (!accessToken) { return new ErrorResponse( HttpStatusCode.UNAUTHORIZED, HttpStatusMessage.UNAUTHORIZED, "No access token present" ); } const user = context.user; const client = getOAuthClient(context); await context.destroySession(); try { await client.revokeToken(context.shopId, accessToken); await context.callHook?.( "storefront:afterLogout", { shopId: context.shopId, user, authenticationType: user.authentication?.type }, context ); return { result: true }; } catch (error) { const response = convertErrorForRpcCall(error, [ HttpStatusCode.BAD_REQUEST, HttpStatusCode.UNAUTHORIZED, HttpStatusCode.NOT_FOUND ]); if (response) { return response; } return { result: false }; } }, { method: "POST" } ); export const oauthForgetPassword = defineRpcHandler( async ({ email }, context) => { if (!hasSession(context)) { return new ErrorResponse( HttpStatusCode.BAD_REQUEST, HttpStatusMessage.BAD_REQUEST, "No Session found" ); } const shopId = context.shopId; const client = getOAuthClient(context); try { if (!context.auth.resetPasswordUrl) { return new ErrorResponse( HttpStatusCode.INTERNAL_SERVER_ERROR, HttpStatusMessage.INTERNAL_SERVER_ERROR, "Missing password reset URL" ); } const resetUrl = new URL(context.auth.resetPasswordUrl); if (!resetUrl.searchParams.has("hash")) { resetUrl.searchParams.append("hash", "{hash}"); } else { resetUrl.searchParams.set("hash", "{hash}"); } await client.sendPasswordResetEmail({ email, reset_url: decodeURI(resetUrl.toString()), shop_id: shopId }); return { success: true }; } catch (error) { const response = convertErrorForRpcCall(error, [ HttpStatusCode.BAD_REQUEST, HttpStatusCode.NOT_FOUND, HttpStatusCode.UNAUTHORIZED, HttpStatusCode.FORBIDDEN ]); if (response) { return response; } return { success: false }; } }, { method: "POST" } ); export const updatePasswordByHash = defineRpcHandler( async (passwordHash, context) => { if (!hasSession(context)) { return new ErrorResponse( HttpStatusCode.BAD_REQUEST, HttpStatusMessage.BAD_REQUEST, "No Session found" ); } const shopId = context.shopId; const client = getOAuthClient(context); try { const tokens = await client.updatePasswordByHash({ ...passwordHash, shop_id: shopId }); context.updateTokens({ accessToken: tokens.access_token, refreshToken: tokens.refresh_token }); await context.createUserBoundSession(); } catch (error) { const response = convertErrorForRpcCall(error, [ HttpStatusCode.BAD_REQUEST, HttpStatusCode.UNAUTHORIZED, HttpStatusCode.NOT_ACCEPTABLE ]); if (response) { return response; } return new ErrorResponse( HttpStatusCode.INTERNAL_SERVER_ERROR, HttpStatusMessage.INTERNAL_SERVER_ERROR, "Error during update password by hash" ); } return new Response(null, { status: HttpStatusCode.NO_CONTENT }); }, { method: "POST" } );