From 1c1d8dd6fefd7ef7fb6a7d3861ecf9b5989721c3 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Sat, 11 Jun 2022 11:21:06 +0300 Subject: [PATCH] Move auth to modules --- .../auth/DiscordStrategy.server.ts | 0 .../auth/authenticator.server.ts | 11 +++-- app/modules/auth/index.ts | 8 +++ app/modules/auth/routes.server.ts | 49 +++++++++++++++++++ app/{core => modules}/auth/session.server.ts | 2 +- app/modules/auth/user.server.ts | 24 +++++++++ app/root.tsx | 2 +- app/routes/auth/callback.tsx | 17 +------ app/routes/auth/impersonate.tsx | 27 +--------- app/routes/auth/index.tsx | 10 +--- app/routes/auth/logout.tsx | 7 +-- app/routes/plus/suggestions.tsx | 8 +-- .../suggestions/comment.$tier.$userId.tsx | 3 +- app/routes/plus/suggestions/new.tsx | 2 +- app/routes/plus/voting/results.tsx | 3 +- app/routes/u.$identifier/edit.tsx | 3 +- app/utils/remix.ts | 27 ---------- 17 files changed, 103 insertions(+), 100 deletions(-) rename app/{core => modules}/auth/DiscordStrategy.server.ts (100%) rename app/{core => modules}/auth/authenticator.server.ts (66%) create mode 100644 app/modules/auth/index.ts create mode 100644 app/modules/auth/routes.server.ts rename app/{core => modules}/auth/session.server.ts (84%) create mode 100644 app/modules/auth/user.server.ts diff --git a/app/core/auth/DiscordStrategy.server.ts b/app/modules/auth/DiscordStrategy.server.ts similarity index 100% rename from app/core/auth/DiscordStrategy.server.ts rename to app/modules/auth/DiscordStrategy.server.ts diff --git a/app/core/auth/authenticator.server.ts b/app/modules/auth/authenticator.server.ts similarity index 66% rename from app/core/auth/authenticator.server.ts rename to app/modules/auth/authenticator.server.ts index 8f92bd963..4d2978ae4 100644 --- a/app/core/auth/authenticator.server.ts +++ b/app/modules/auth/authenticator.server.ts @@ -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(sessionStorage, { - sessionKey: SESSION_KEY, -}); +export const authenticator = new Authenticator( + authSessionStorage, + { + sessionKey: SESSION_KEY, + } +); authenticator.use(new DiscordStrategy()); diff --git a/app/modules/auth/index.ts b/app/modules/auth/index.ts new file mode 100644 index 000000000..fbb21ebee --- /dev/null +++ b/app/modules/auth/index.ts @@ -0,0 +1,8 @@ +export { + callbackLoader, + impersonateAction, + logInAction, + logOutAction, +} from "./routes.server"; + +export { getUser, requireUser } from "./user.server"; diff --git a/app/modules/auth/routes.server.ts b/app/modules/auth/routes.server.ts new file mode 100644 index 000000000..22449611c --- /dev/null +++ b/app/modules/auth/routes.server.ts @@ -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) }, + }); +}; diff --git a/app/core/auth/session.server.ts b/app/modules/auth/session.server.ts similarity index 84% rename from app/core/auth/session.server.ts rename to app/modules/auth/session.server.ts index 978801d12..8c30cae85 100644 --- a/app/core/auth/session.server.ts +++ b/app/modules/auth/session.server.ts @@ -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", diff --git a/app/modules/auth/user.server.ts b/app/modules/auth/user.server.ts new file mode 100644 index 000000000..6e7b60a52 --- /dev/null +++ b/app/modules/auth/user.server.ts @@ -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; +} diff --git a/app/root.tsx b/app/root.tsx index 68b6321d7..65c3d4883 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -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; diff --git a/app/routes/auth/callback.tsx b/app/routes/auth/callback.tsx index d55a0d78d..fa00c3035 100644 --- a/app/routes/auth/callback.tsx +++ b/app/routes/auth/callback.tsx @@ -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"; diff --git a/app/routes/auth/impersonate.tsx b/app/routes/auth/impersonate.tsx index 85479118a..ffeca19e3 100644 --- a/app/routes/auth/impersonate.tsx +++ b/app/routes/auth/impersonate.tsx @@ -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"; diff --git a/app/routes/auth/index.tsx b/app/routes/auth/index.tsx index 786c15e66..bb77a9acd 100644 --- a/app/routes/auth/index.tsx +++ b/app/routes/auth/index.tsx @@ -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"; diff --git a/app/routes/auth/logout.tsx b/app/routes/auth/logout.tsx index 5ca101e35..41d7083c8 100644 --- a/app/routes/auth/logout.tsx +++ b/app/routes/auth/logout.tsx @@ -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"; diff --git a/app/routes/plus/suggestions.tsx b/app/routes/plus/suggestions.tsx index 27d1f9d9e..f2a597544 100644 --- a/app/routes/plus/suggestions.tsx +++ b/app/routes/plus/suggestions.tsx @@ -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"; diff --git a/app/routes/plus/suggestions/comment.$tier.$userId.tsx b/app/routes/plus/suggestions/comment.$tier.$userId.tsx index 4dd3a0029..69db18e8b 100644 --- a/app/routes/plus/suggestions/comment.$tier.$userId.tsx +++ b/app/routes/plus/suggestions/comment.$tier.$userId.tsx @@ -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"; diff --git a/app/routes/plus/suggestions/new.tsx b/app/routes/plus/suggestions/new.tsx index 7abeda24d..2c78ff2c9 100644 --- a/app/routes/plus/suggestions/new.tsx +++ b/app/routes/plus/suggestions/new.tsx @@ -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)), diff --git a/app/routes/plus/voting/results.tsx b/app/routes/plus/voting/results.tsx index dab66e220..954bf2470 100644 --- a/app/routes/plus/voting/results.tsx +++ b/app/routes/plus/voting/results.tsx @@ -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 }]; diff --git a/app/routes/u.$identifier/edit.tsx b/app/routes/u.$identifier/edit.tsx index b59aede84..723b44d6f 100644 --- a/app/routes/u.$identifier/edit.tsx +++ b/app/routes/u.$identifier/edit.tsx @@ -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"; diff --git a/app/utils/remix.ts b/app/utils/remix.ts index 14f9cc46a..27c239956 100644 --- a/app/utils/remix.ts +++ b/app/utils/remix.ts @@ -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(value: T | null | undefined): T { if (!value) throw new Response(null, { status: 404 }); @@ -44,27 +38,6 @@ export async function parseRequestFormData({ } } -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 {