import type { ActionFunction, LoaderFunction, V2_MetaFunction, } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; import { useFetcher, useLoaderData, useNavigation } from "@remix-run/react"; import * as React from "react"; import { z } from "zod"; import { Button } from "~/components/Button"; import { Catcher } from "~/components/Catcher"; import { UserCombobox } from "~/components/Combobox"; import { Main } from "~/components/Main"; import { SubmitButton } from "~/components/SubmitButton"; import { db } from "~/db"; import { getUserId, isImpersonating, requireUserId, } from "~/modules/auth/user.server"; import { canPerformAdminActions } from "~/permissions"; import { dateToDatabaseTimestamp } from "~/utils/dates"; import { parseRequestFormData, validate, type SendouRouteHandle, } from "~/utils/remix"; import { makeTitle } from "~/utils/strings"; import { assertUnreachable } from "~/utils/types"; import { impersonateUrl, SEED_URL, STOP_IMPERSONATING_URL } from "~/utils/urls"; import { actualNumber } from "~/utils/zod"; export const meta: V2_MetaFunction = () => { return [{ title: makeTitle("Admin page") }]; }; const adminActionSchema = z.union([ z.object({ _action: z.literal("MIGRATE"), "old-user[value]": z.preprocess(actualNumber, z.number().positive()), "new-user[value]": z.preprocess(actualNumber, z.number().positive()), }), z.object({ _action: z.literal("REFRESH"), }), z.object({ _action: z.literal("FORCE_PATRON"), "user[value]": z.preprocess(actualNumber, z.number().positive()), patronTier: z.preprocess(actualNumber, z.number()), patronTill: z.string(), }), z.object({ _action: z.literal("VIDEO_ADDER"), "user[value]": z.preprocess(actualNumber, z.number().positive()), }), z.object({ _action: z.literal("LINK_PLAYER"), "user[value]": z.preprocess(actualNumber, z.number().positive()), playerId: z.preprocess(actualNumber, z.number().positive()), }), ]); export const action: ActionFunction = async ({ request }) => { const data = await parseRequestFormData({ request, schema: adminActionSchema, }); const user = await requireUserId(request); validate(canPerformAdminActions(user)); switch (data._action) { case "MIGRATE": { db.users.migrate({ oldUserId: data["old-user[value]"], newUserId: data["new-user[value]"], }); break; } case "REFRESH": { db.users.refreshPlusTiers(); break; } case "FORCE_PATRON": { db.users.forcePatron({ id: data["user[value]"], patronSince: dateToDatabaseTimestamp(new Date()), patronTier: data.patronTier, patronTill: dateToDatabaseTimestamp(new Date(data.patronTill)), }); break; } case "VIDEO_ADDER": { db.users.makeVideoAdder(data["user[value]"]); break; } case "LINK_PLAYER": { db.users.linkPlayer({ userId: data["user[value]"], playerId: data.playerId, }); break; } default: { assertUnreachable(data); } } return null; }; interface AdminPageLoaderData { isImpersonating: boolean; } export const loader: LoaderFunction = async ({ request }) => { const user = await getUserId(request); if (!canPerformAdminActions(user)) { return redirect("/"); } return json({ isImpersonating: await isImpersonating(request), }); }; export const handle: SendouRouteHandle = { navItemName: "admin", }; export default function AdminPage() { return (
{process.env.NODE_ENV !== "production" && }
); } function Impersonate() { const fetcher = useFetcher(); const [userId, setUserId] = React.useState(); const { isImpersonating } = useLoaderData(); return (

Impersonate user

setUserId(selected?.value ? Number(selected.value) : undefined) } />
{isImpersonating ? ( ) : null}
); } function MigrateUser() { const [oldUserId, setOldUserId] = React.useState(); const [newUserId, setNewUserId] = React.useState(); const navigation = useNavigation(); const fetcher = useFetcher(); const submitButtonText = navigation.state === "submitting" ? "Migrating..." : navigation.state === "loading" ? "Migrated!" : "Migrate"; return (

Migrate user data

setOldUserId(selected?.value ? Number(selected.value) : undefined) } />
setNewUserId(selected?.value ? Number(selected.value) : undefined) } />
{submitButtonText}
); } function LinkPlayer() { const fetcher = useFetcher(); return (

Link player

Link player
); } function GiveVideoAdder() { const fetcher = useFetcher(); return (

Give video adder

Add as video adder
); } function ForcePatron() { const fetcher = useFetcher(); return (

Force patron

Save
); } function RefreshPlusTiers() { const fetcher = useFetcher(); return (

Refresh Plus Tiers

Refresh
); } function Seed() { const fetcher = useFetcher(); return (

Seed

); } export const CatchBoundary = Catcher;