Move auth to modules

This commit is contained in:
Kalle
2022-06-11 11:21:06 +03:00
parent e5617d095e
commit 1c1d8dd6fe
17 changed files with 103 additions and 100 deletions

View File

@@ -1,14 +1,17 @@
import { Authenticator } from "remix-auth";
import { DiscordStrategy } from "./DiscordStrategy.server";
import type { LoggedInUser } from "./DiscordStrategy.server";
import { sessionStorage } from "./session.server";
import { authSessionStorage } from "./session.server";
export const DISCORD_AUTH_KEY = "discord";
export const SESSION_KEY = "user";
export const IMPERSONATED_SESSION_KEY = "impersonated_user";
export const authenticator = new Authenticator<LoggedInUser>(sessionStorage, {
sessionKey: SESSION_KEY,
});
export const authenticator = new Authenticator<LoggedInUser>(
authSessionStorage,
{
sessionKey: SESSION_KEY,
}
);
authenticator.use(new DiscordStrategy());

View File

@@ -0,0 +1,8 @@
export {
callbackLoader,
impersonateAction,
logInAction,
logOutAction,
} from "./routes.server";
export { getUser, requireUser } from "./user.server";

View File

@@ -0,0 +1,49 @@
import type { ActionFunction, LoaderFunction } from "@remix-run/node";
import { redirect } from "@remix-run/node";
import {
authenticator,
DISCORD_AUTH_KEY,
IMPERSONATED_SESSION_KEY,
} from "./authenticator.server";
import { authSessionStorage } from "./session.server";
export const callbackLoader: LoaderFunction = async ({ request }) => {
await authenticator.authenticate(DISCORD_AUTH_KEY, request, {
successRedirect: "/",
// TODO: should include query param that displays an error banner explaining that log in went wrong
// and where to get help for that
failureRedirect: "/",
});
throw new Response("Unknown authentication state", { status: 500 });
};
export const logOutAction: ActionFunction = async ({ request }) => {
await authenticator.logout(request, { redirectTo: "/" });
};
export const logInAction: ActionFunction = async ({ request }) => {
return authenticator.authenticate(DISCORD_AUTH_KEY, request);
};
export const impersonateAction: ActionFunction = async ({ request }) => {
if (process.env.NODE_ENV === "production") {
throw new Response(null, { status: 400 });
}
const session = await authSessionStorage.getSession(
request.headers.get("Cookie")
);
const url = new URL(request.url);
const rawId = url.searchParams.get("id");
const userId = Number(url.searchParams.get("id"));
if (!rawId || Number.isNaN(userId)) throw new Response(null, { status: 400 });
session.set(IMPERSONATED_SESSION_KEY, userId);
throw redirect("/", {
headers: { "Set-Cookie": await authSessionStorage.commitSession(session) },
});
};

View File

@@ -2,7 +2,7 @@ import { createCookieSessionStorage } from "@remix-run/node";
import invariant from "tiny-invariant";
invariant(process.env["SESSION_SECRET"]);
export const sessionStorage = createCookieSessionStorage({
export const authSessionStorage = createCookieSessionStorage({
cookie: {
name: "_session",
sameSite: "lax",

View File

@@ -0,0 +1,24 @@
import { db } from "~/db";
import { IMPERSONATED_SESSION_KEY, SESSION_KEY } from "./authenticator.server";
import { authSessionStorage } from "./session.server";
export async function getUser(request: Request) {
const session = await authSessionStorage.getSession(
request.headers.get("Cookie")
);
const userId =
session.get(IMPERSONATED_SESSION_KEY) ?? session.get(SESSION_KEY);
if (!userId) return;
return db.users.findByIdentifier(userId);
}
export async function requireUser(request: Request) {
const user = await getUser(request);
if (!user) throw new Response(null, { status: 401 });
return user;
}

View File

@@ -20,7 +20,7 @@ import layoutStyles from "~/styles/layout.css";
import resetStyles from "~/styles/reset.css";
import { Layout } from "./components/layout";
import type { UserWithPlusTier } from "./db/types";
import { getUser } from "./utils/remix";
import { getUser } from "./modules/auth";
export const unstable_shouldReload: ShouldReloadFunction = () => false;

View File

@@ -1,16 +1 @@
import type { LoaderFunction } from "@remix-run/node";
import {
authenticator,
DISCORD_AUTH_KEY,
} from "~/core/auth/authenticator.server";
export const loader: LoaderFunction = async ({ request }) => {
await authenticator.authenticate(DISCORD_AUTH_KEY, request, {
successRedirect: "/",
// TODO: should include query param that displays an error banner explaining that log in went wrong
// and where to get help for that
failureRedirect: "/",
});
throw new Response("Unknown authentication state", { status: 500 });
};
export { callbackLoader as loader } from "~/modules/auth";

View File

@@ -1,26 +1 @@
import type { ActionFunction } from "@remix-run/node";
import { redirect } from "@remix-run/node";
import { IMPERSONATED_SESSION_KEY } from "~/core/auth/authenticator.server";
import { sessionStorage } from "~/core/auth/session.server";
export const action: ActionFunction = async ({ request }) => {
if (process.env.NODE_ENV === "production") {
throw new Response(null, { status: 400 });
}
const session = await sessionStorage.getSession(
request.headers.get("Cookie")
);
const url = new URL(request.url);
const rawId = url.searchParams.get("id");
const userId = Number(url.searchParams.get("id"));
if (!rawId || Number.isNaN(userId)) throw new Response(null, { status: 400 });
session.set(IMPERSONATED_SESSION_KEY, userId);
throw redirect("/", {
headers: { "Set-Cookie": await sessionStorage.commitSession(session) },
});
};
export { impersonateAction as action } from "~/modules/auth";

View File

@@ -1,9 +1 @@
import {
authenticator,
DISCORD_AUTH_KEY,
} from "~/core/auth/authenticator.server";
import type { ActionFunction } from "@remix-run/node";
export const action: ActionFunction = async ({ request }) => {
return await authenticator.authenticate(DISCORD_AUTH_KEY, request);
};
export { logInAction as action } from "~/modules/auth";

View File

@@ -1,6 +1 @@
import type { ActionFunction } from "@remix-run/node";
import { authenticator } from "~/core/auth/authenticator.server";
export const action: ActionFunction = async ({ request }) => {
await authenticator.logout(request, { redirectTo: "/" });
};
export { logOutAction as action } from "~/modules/auth";

View File

@@ -20,18 +20,14 @@ import { db } from "~/db";
import type * as plusSuggestions from "~/db/models/plusSuggestions.server";
import type { PlusSuggestion } from "~/db/types";
import { useUser } from "~/hooks/useUser";
import { requireUser } from "~/modules/auth";
import {
canAddCommentToSuggestionFE,
canSuggestNewUserFE,
canDeleteComment,
} from "~/permissions";
import styles from "~/styles/plus.css";
import {
makeTitle,
parseRequestFormData,
requireUser,
validate,
} from "~/utils/remix";
import { makeTitle, parseRequestFormData, validate } from "~/utils/remix";
import { discordFullName } from "~/utils/strings";
import { actualNumber } from "~/utils/zod";

View File

@@ -9,12 +9,13 @@ import { PlUS_SUGGESTION_COMMENT_MAX_LENGTH } from "~/constants";
import { upcomingVoting } from "~/core/plus";
import { db } from "~/db";
import { useUser } from "~/hooks/useUser";
import { requireUser } from "~/modules/auth";
import {
canAddCommentToSuggestionBE,
canAddCommentToSuggestionFE,
} from "~/permissions";
import { atOrError } from "~/utils/arrays";
import { parseRequestFormData, requireUser, validate } from "~/utils/remix";
import { parseRequestFormData, validate } from "~/utils/remix";
import { PLUS_SUGGESTIONS_PAGE } from "~/utils/urls";
import { actualNumber } from "~/utils/zod";
import type { PlusSuggestionsLoaderData } from "../suggestions";

View File

@@ -25,7 +25,6 @@ import { actualNumber } from "~/utils/zod";
import {
badRequestIfFalsy,
parseRequestFormData,
requireUser,
validate,
} from "~/utils/remix";
import { upcomingVoting } from "~/core/plus";
@@ -33,6 +32,7 @@ import { db } from "~/db";
import type { UserWithPlusTier } from "~/db/types";
import { ErrorMessage } from "~/components/ErrorMessage";
import { atOrError } from "~/utils/arrays";
import { requireUser } from "~/modules/auth";
const commentActionSchema = z.object({
tier: z.preprocess(actualNumber, z.number().min(1).max(3)),

View File

@@ -10,12 +10,13 @@ import { db } from "~/db";
import type { PlusVotingResultByMonthYear } from "~/db/models/plusVotes.server";
import type { PlusVotingResult, UserWithPlusTier } from "~/db/types";
import { roundToTwoDecimalPlaces } from "~/utils/number";
import { getUser, makeTitle } from "~/utils/remix";
import { makeTitle } from "~/utils/remix";
import type { Unpacked } from "~/utils/types";
import styles from "~/styles/plus-history.css";
import { discordFullName } from "~/utils/strings";
import { userPage } from "~/utils/urls";
import clsx from "clsx";
import { getUser } from "~/modules/auth";
export const links: LinksFunction = () => {
return [{ rel: "stylesheet", href: styles }];

View File

@@ -9,8 +9,9 @@ import { Label } from "~/components/Label";
import { USER_BIO_MAX_LENGTH } from "~/constants";
import { db } from "~/db";
import type { User } from "~/db/types";
import { requireUser } from "~/modules/auth";
import styles from "~/styles/u-edit.css";
import { parseRequestFormData, requireUser } from "~/utils/remix";
import { parseRequestFormData } from "~/utils/remix";
import { falsyToNull } from "~/utils/zod";
import type { UserPageLoaderData } from "../u.$identifier";

View File

@@ -1,10 +1,4 @@
import { z } from "zod";
import {
IMPERSONATED_SESSION_KEY,
SESSION_KEY,
} from "~/core/auth/authenticator.server";
import { sessionStorage } from "~/core/auth/session.server";
import { db } from "~/db";
export function notFoundIfFalsy<T>(value: T | null | undefined): T {
if (!value) throw new Response(null, { status: 404 });
@@ -44,27 +38,6 @@ export async function parseRequestFormData<T extends z.ZodTypeAny>({
}
}
export async function requireUser(request: Request) {
const user = await getUser(request);
if (!user) throw new Response(null, { status: 401 });
return user;
}
export async function getUser(request: Request) {
const session = await sessionStorage.getSession(
request.headers.get("Cookie")
);
const userId =
session.get(IMPERSONATED_SESSION_KEY) ?? session.get(SESSION_KEY);
if (!userId) return;
return db.users.findByIdentifier(userId);
}
/** Asserts condition is truthy. Throws a new `Response` with status code 400 and given message if falsy. */
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- same format as TS docs: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions
export function validate(condition: any): asserts condition {