@scayle/storefront-core
Version:
Collection of essential utilities to work with the Storefront API
364 lines (363 loc) • 10.9 kB
JavaScript
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" }
);