move to webapp

This commit is contained in:
m5r
2021-07-18 23:32:45 +08:00
parent 61c23ec9a7
commit a989125e6e
167 changed files with 26607 additions and 24066 deletions

View 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
View 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
View 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
View 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
View 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" });
}