move to webapp
This commit is contained in:
30
lib/__tests__/session-helpers.ts
Normal file
30
lib/__tests__/session-helpers.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import type { NextApiHandler } from "next";
|
||||
|
||||
import { withApiAuthRequired } from "../session-helpers";
|
||||
import { callApiHandler } from "../../jest/helpers";
|
||||
|
||||
describe("session-helpers", () => {
|
||||
describe("withApiAuthRequired", () => {
|
||||
const basicHandler: NextApiHandler = (req, res) =>
|
||||
res.status(200).end();
|
||||
|
||||
test("responds 401 to unauthenticated GET", async () => {
|
||||
const withAuthHandler = withApiAuthRequired(basicHandler);
|
||||
const { status } = await callApiHandler(withAuthHandler, {
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
expect(status).toBe(401);
|
||||
});
|
||||
|
||||
test("responds 200 to authenticated GET", async () => {
|
||||
const withAuthHandler = withApiAuthRequired(basicHandler);
|
||||
const { status } = await callApiHandler(withAuthHandler, {
|
||||
method: "GET",
|
||||
authentication: "auth0",
|
||||
});
|
||||
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
});
|
||||
});
|
12
lib/logger.ts
Normal file
12
lib/logger.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import pino from "pino";
|
||||
|
||||
const appLogger = pino({
|
||||
level: "debug",
|
||||
base: {
|
||||
env: process.env.NODE_ENV || "NODE_ENV not set",
|
||||
revision: process.env.VERCEL_GITHUB_COMMIT_SHA,
|
||||
},
|
||||
prettyPrint: true,
|
||||
});
|
||||
|
||||
export default appLogger;
|
184
lib/session-helpers.ts
Normal file
184
lib/session-helpers.ts
Normal file
@ -0,0 +1,184 @@
|
||||
import type {
|
||||
GetServerSideProps,
|
||||
GetServerSidePropsContext,
|
||||
GetServerSidePropsResult,
|
||||
NextApiHandler,
|
||||
NextApiRequest,
|
||||
NextApiResponse,
|
||||
} from "next";
|
||||
import type { User } from "@supabase/supabase-js";
|
||||
|
||||
import supabase from "../src/supabase/server";
|
||||
import appLogger from "./logger";
|
||||
import { setCookie } from "./utils/cookies";
|
||||
import { findCustomer } from "../src/database/customer";
|
||||
import { findCustomerPhoneNumber } from "../src/database/phone-number";
|
||||
|
||||
const logger = appLogger.child({ module: "session-helpers" });
|
||||
|
||||
type EmptyProps = Record<string, unknown>;
|
||||
|
||||
type SessionProps = {
|
||||
user: User;
|
||||
};
|
||||
|
||||
function hasProps<Props extends EmptyProps = EmptyProps>(
|
||||
result: GetServerSidePropsResult<Props>,
|
||||
): result is { props: Props } {
|
||||
return result.hasOwnProperty("props");
|
||||
}
|
||||
|
||||
export function withPageOnboardingRequired<Props extends EmptyProps = EmptyProps>(
|
||||
getServerSideProps?: GSSPWithSession<Props>,
|
||||
) {
|
||||
return withPageAuthRequired(
|
||||
async function wrappedGetServerSideProps(context, user) {
|
||||
if (context.req.cookies.hasDoneOnboarding !== "true") {
|
||||
try {
|
||||
const customer = await findCustomer(user.id);
|
||||
console.log("customer", customer);
|
||||
if (!customer.accountSid || !customer.authToken) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/welcome/step-two",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
/*if (!customer.paddleCustomerId || !customer.paddleSubscriptionId) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/welcome/step-one",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}*/
|
||||
try {
|
||||
await findCustomerPhoneNumber(user.id);
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/welcome/step-three",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
setCookie({
|
||||
req: context.req,
|
||||
res: context.res,
|
||||
name: "hasDoneOnboarding",
|
||||
value: "true",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("error", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (!getServerSideProps) {
|
||||
return {
|
||||
props: {} as Props,
|
||||
};
|
||||
}
|
||||
|
||||
return getServerSideProps(context, user);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
type GSSPWithSession<Props> = (
|
||||
context: GetServerSidePropsContext,
|
||||
user: User,
|
||||
) => GetServerSidePropsResult<Props> | Promise<GetServerSidePropsResult<Props>>;
|
||||
|
||||
export function withPageAuthRequired<Props extends EmptyProps = EmptyProps>(
|
||||
getServerSideProps?: GSSPWithSession<Props>,
|
||||
): GetServerSideProps<Omit<Props, "user"> & SessionProps> {
|
||||
return async function wrappedGetServerSideProps(context) {
|
||||
const redirectTo = `/auth/sign-in?redirectTo=${context.resolvedUrl}`;
|
||||
const userResponse = await supabase.auth.api.getUserByCookie(context.req);
|
||||
const user = userResponse.user!;
|
||||
if (userResponse.error) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: redirectTo,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!getServerSideProps) {
|
||||
return {
|
||||
props: { user } as Props & SessionProps,
|
||||
};
|
||||
}
|
||||
|
||||
const getServerSidePropsResult = await getServerSideProps(
|
||||
context,
|
||||
user,
|
||||
);
|
||||
if (!hasProps(getServerSidePropsResult)) {
|
||||
return getServerSidePropsResult;
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
...getServerSidePropsResult.props,
|
||||
user,
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
type ApiHandlerWithAuth<T> = (
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<T>,
|
||||
user: User,
|
||||
) => void | Promise<void>;
|
||||
|
||||
export function withApiAuthRequired<T = any>(
|
||||
handler: ApiHandlerWithAuth<T>,
|
||||
): NextApiHandler {
|
||||
return async function wrappedApiHandler(req, res) {
|
||||
const userResponse = await supabase.auth.api.getUserByCookie(req);
|
||||
if (userResponse.error) {
|
||||
logger.error(userResponse.error.message);
|
||||
return res.status(401).end();
|
||||
}
|
||||
|
||||
return handler(req, res, userResponse.user!);
|
||||
};
|
||||
}
|
||||
|
||||
export function withPageAuthNotRequired<Props extends EmptyProps = EmptyProps>(
|
||||
getServerSideProps?: GetServerSideProps<Props>,
|
||||
): GetServerSideProps<Props> {
|
||||
return async function wrappedGetServerSideProps(context) {
|
||||
let redirectTo: string;
|
||||
if (Array.isArray(context.query.redirectTo)) {
|
||||
redirectTo = context.query.redirectTo[0];
|
||||
} else {
|
||||
redirectTo = context.query.redirectTo ?? "/messages";
|
||||
}
|
||||
|
||||
const { user } = await supabase.auth.api.getUserByCookie(context.req);
|
||||
console.log("user", user);
|
||||
if (user !== null) {
|
||||
console.log("redirect");
|
||||
return {
|
||||
redirect: {
|
||||
destination: redirectTo,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
console.log("no redirect");
|
||||
if (getServerSideProps) {
|
||||
return getServerSideProps(context);
|
||||
}
|
||||
|
||||
return { props: {} as Props };
|
||||
};
|
||||
}
|
79
lib/utils/cookies.ts
Normal file
79
lib/utils/cookies.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import type { IncomingMessage, ServerResponse } from "http";
|
||||
import type { CookieSerializeOptions } from "cookie";
|
||||
import nookies from "nookies";
|
||||
|
||||
const defaultOptions: CookieSerializeOptions = {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
};
|
||||
|
||||
export function getCookies(req?: BaseParams["req"]) {
|
||||
const context = buildContext({ req });
|
||||
|
||||
return nookies.get(context);
|
||||
}
|
||||
|
||||
type SetCookieParams = BaseParams & {
|
||||
value: string;
|
||||
options?: CookieSerializeOptions;
|
||||
};
|
||||
|
||||
export function setCookie(params: SetCookieParams) {
|
||||
const { req, res, name, value } = params;
|
||||
const context = buildContext({ res });
|
||||
const options: CookieSerializeOptions = {
|
||||
...defaultOptions,
|
||||
...params.options,
|
||||
secure: isSecureEnvironment(req),
|
||||
};
|
||||
|
||||
return nookies.set(context, name, value, options);
|
||||
}
|
||||
|
||||
type DestroyCookieParams = BaseParams & {
|
||||
options?: CookieSerializeOptions;
|
||||
};
|
||||
|
||||
export function destroyCookie(params: DestroyCookieParams) {
|
||||
const { res, name } = params;
|
||||
const context = buildContext({ res });
|
||||
const options = Object.assign({}, defaultOptions, params.options);
|
||||
|
||||
return nookies.destroy(context, name, options);
|
||||
}
|
||||
|
||||
function isSecureEnvironment(req: IncomingMessage | null | undefined): boolean {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!req || !req.headers || !req.headers.host) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const host =
|
||||
(req.headers.host.indexOf(":") > -1 &&
|
||||
req.headers.host.split(":")[0]) ||
|
||||
req.headers.host;
|
||||
|
||||
return !["localhost", "127.0.0.1"].includes(host);
|
||||
}
|
||||
|
||||
type BaseParams = {
|
||||
req?: IncomingMessage | null;
|
||||
res?: ServerResponse | null;
|
||||
name: string;
|
||||
};
|
||||
|
||||
function buildContext({ req, res }: Pick<BaseParams, "req" | "res">) {
|
||||
if (req !== null && typeof req !== "undefined") {
|
||||
return { req };
|
||||
}
|
||||
|
||||
if (res !== null && typeof res !== "undefined") {
|
||||
return { res };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
7
lib/utils/hkdf.ts
Normal file
7
lib/utils/hkdf.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import hkdf from "futoin-hkdf";
|
||||
|
||||
const BYTE_LENGTH = 32;
|
||||
|
||||
export function encryption(secret: string) {
|
||||
return hkdf(secret, BYTE_LENGTH, { info: "JWE CEK", hash: "SHA-256" });
|
||||
}
|
Reference in New Issue
Block a user