import { ActionFunctionArgs, json, redirect, LoaderFunctionArgs, MetaFunction, } from "@remix-run/node"; import { type ApiError, ErrorCode, firstErrorFor } from "~/lib/api/error"; import serverRequest, { writeCookie } from "~/lib/request.server"; import { AuthResponse, CallbackResponse } from "~/lib/api/auth"; import { Form as RemixForm, Link, useActionData, useLoaderData, ShouldRevalidateFunction, } from "@remix-run/react"; import { Trans, useTranslation } from "react-i18next"; import { Form, Button, Alert } from "react-bootstrap"; import ErrorAlert from "~/components/ErrorAlert"; import i18n from "~/i18next.server"; export const meta: MetaFunction = ({ data }) => { return [{ title: `${data?.meta.title || "Log in"} • pronouns.cc` }]; }; export const shouldRevalidate: ShouldRevalidateFunction = ({ actionResult }) => { return !actionResult; }; export const loader = async ({ request }: LoaderFunctionArgs) => { const t = await i18n.getFixedT(request); const url = new URL(request.url); const code = url.searchParams.get("code"); const state = url.searchParams.get("state"); if (!code || !state) throw { status: 400, code: ErrorCode.BadRequest, message: "Missing code or state" } as ApiError; const resp = await serverRequest("POST", "/auth/discord/callback", { body: { code, state }, }); if (resp.has_account) { return json( { meta: { title: t("log-in.callback.title.discord-success") }, hasAccount: true, user: resp.user!, ticket: null, remoteUser: null, }, { headers: { "Set-Cookie": writeCookie("pronounscc-token", resp.token!), }, }, ); } return json({ meta: { title: t("log-in.callback.title.discord-register") }, hasAccount: false, user: null, ticket: resp.ticket!, remoteUser: resp.remote_username!, }); }; export const action = async ({ request }: ActionFunctionArgs) => { const data = await request.formData(); const username = data.get("username") as string | null; const ticket = data.get("ticket") as string | null; if (!username || !ticket) return json({ error: { status: 403, code: ErrorCode.BadRequest, message: "Invalid username or ticket", } as ApiError, user: null, }); try { const resp = await serverRequest("POST", "/auth/discord/register", { body: { username, ticket }, }); return redirect("/auth/welcome", { headers: { "Set-Cookie": writeCookie("pronounscc-token", resp.token), }, status: 303, }); } catch (e) { JSON.stringify(e); return json({ error: e as ApiError }); } }; export default function DiscordCallbackPage() { const { t } = useTranslation(); const data = useLoaderData(); const actionData = useActionData(); if (data.hasAccount) { const username = data.user!.username; return ( <>

{t("log-in.callback.success")}

{/* @ts-expect-error react-i18next handles interpolation here */} Welcome back, @{{ username }}!
{t("log-in.callback.redirect-hint")}

); } return (
{actionData?.error && } {t("log-in.callback.remote-username.discord")} {t("log-in.callback.username")}
); } function RegisterError({ error }: { error: ApiError }) { const { t } = useTranslation(); // TODO: maybe turn these messages into their own error codes? const ticketMessage = firstErrorFor(error, "ticket")?.message; const usernameMessage = firstErrorFor(error, "username")?.message; if (ticketMessage === "Invalid ticket") { return ( {t("error.heading")} Invalid ticket (it might have been too long since you logged in with Discord), please{" "} try again. ); } if (usernameMessage === "Username is already taken") { return ( {t("log-in.callback.invalid-username")} {t("log-in.callback.username-taken")} ); } return ; }