mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-11 05:36:10 -05:00
Move auth to modules
This commit is contained in:
@@ -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());
|
||||
8
app/modules/auth/index.ts
Normal file
8
app/modules/auth/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export {
|
||||
callbackLoader,
|
||||
impersonateAction,
|
||||
logInAction,
|
||||
logOutAction,
|
||||
} from "./routes.server";
|
||||
|
||||
export { getUser, requireUser } from "./user.server";
|
||||
49
app/modules/auth/routes.server.ts
Normal file
49
app/modules/auth/routes.server.ts
Normal 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) },
|
||||
});
|
||||
};
|
||||
@@ -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",
|
||||
24
app/modules/auth/user.server.ts
Normal file
24
app/modules/auth/user.server.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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 }];
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user