feat: initial fediverse registration/login
This commit is contained in:
parent
5a22807410
commit
c4cb08cdc1
16 changed files with 467 additions and 111 deletions
|
@ -22,6 +22,7 @@ import ErrorAlert from "~/components/ErrorAlert";
|
|||
import i18n from "~/i18next.server";
|
||||
import { tokenCookieName } from "~/lib/utils";
|
||||
import { useEffect } from "react";
|
||||
import RegisterError from "~/components/RegisterError";
|
||||
|
||||
export const meta: MetaFunction<typeof loader> = ({ data }) => {
|
||||
return [{ title: `${data?.meta.title || "Log in"} • pronouns.cc` }];
|
||||
|
@ -163,34 +164,3 @@ export default function DiscordCallbackPage() {
|
|||
</RemixForm>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Alert variant="danger">
|
||||
<Alert.Heading as="h4">{t("error.heading")}</Alert.Heading>
|
||||
<Trans t={t} i18nKey={"log-in.callback.invalid-ticket"}>
|
||||
Invalid ticket (it might have been too long since you logged in with Discord), please{" "}
|
||||
<Link to="/auth/log-in">try again</Link>.
|
||||
</Trans>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (usernameMessage === "Username is already taken") {
|
||||
return (
|
||||
<Alert variant="danger">
|
||||
<Alert.Heading as="h4">{t("log-in.callback.invalid-username")}</Alert.Heading>
|
||||
{t("log-in.callback.username-taken")}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return <ErrorAlert error={error} />;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,163 @@
|
|||
import {
|
||||
ActionFunctionArgs,
|
||||
json,
|
||||
LoaderFunctionArgs,
|
||||
MetaFunction,
|
||||
redirect,
|
||||
} from "@remix-run/node";
|
||||
import i18n from "~/i18next.server";
|
||||
import { type ApiError, ErrorCode } from "~/lib/api/error";
|
||||
import serverRequest, { writeCookie } from "~/lib/request.server";
|
||||
import { AuthResponse, CallbackResponse } from "~/lib/api/auth";
|
||||
import { tokenCookieName } from "~/lib/utils";
|
||||
import {
|
||||
Link,
|
||||
ShouldRevalidateFunction,
|
||||
useActionData,
|
||||
useLoaderData,
|
||||
useNavigate,
|
||||
} from "@remix-run/react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { useEffect } from "react";
|
||||
import { Form as RemixForm } from "@remix-run/react/dist/components";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import RegisterError from "~/components/RegisterError";
|
||||
|
||||
export const meta: MetaFunction<typeof loader> = ({ data }) => {
|
||||
return [{ title: `${data?.meta.title || "Log in"} • pronouns.cc` }];
|
||||
};
|
||||
|
||||
export const shouldRevalidate: ShouldRevalidateFunction = ({ actionResult }) => {
|
||||
return !actionResult;
|
||||
};
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const t = await i18n.getFixedT(request);
|
||||
const url = new URL(request.url);
|
||||
|
||||
const code = url.searchParams.get("code");
|
||||
if (!code) throw { status: 400, code: ErrorCode.BadRequest, message: "Missing code" } as ApiError;
|
||||
|
||||
const resp = await serverRequest<CallbackResponse>("POST", "/auth/fediverse/callback", {
|
||||
body: { code, instance: params.instance! },
|
||||
isInternal: true,
|
||||
});
|
||||
|
||||
if (resp.has_account) {
|
||||
return json(
|
||||
{
|
||||
meta: { title: t("log-in.callback.title.fediverse-success") },
|
||||
hasAccount: true,
|
||||
user: resp.user!,
|
||||
ticket: null,
|
||||
remoteUser: null,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Set-Cookie": writeCookie(tokenCookieName, resp.token!),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return json({
|
||||
meta: { title: t("log-in.callback.title.fediverse-register") },
|
||||
hasAccount: false,
|
||||
user: null,
|
||||
instance: params.instance!,
|
||||
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<AuthResponse>("POST", "/auth/fediverse/register", {
|
||||
body: { username, ticket },
|
||||
isInternal: true,
|
||||
});
|
||||
|
||||
return redirect("/auth/welcome", {
|
||||
headers: {
|
||||
"Set-Cookie": writeCookie(tokenCookieName, resp.token),
|
||||
},
|
||||
status: 303,
|
||||
});
|
||||
} catch (e) {
|
||||
JSON.stringify(e);
|
||||
|
||||
return json({ error: e as ApiError });
|
||||
}
|
||||
};
|
||||
|
||||
export default function FediverseCallbackPage() {
|
||||
const { t } = useTranslation();
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const actionData = useActionData<typeof action>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
if (data.hasAccount) {
|
||||
navigate(`/@${data.user!.username}`);
|
||||
}
|
||||
}, 2000);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
if (data.hasAccount) {
|
||||
const username = data.user!.username;
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>{t("log-in.callback.success")}</h1>
|
||||
<p>
|
||||
<Trans
|
||||
t={t}
|
||||
i18nKey={"log-in.callback.success-link"}
|
||||
values={{ username: data.user!.username }}
|
||||
>
|
||||
{/* @ts-expect-error react-i18next handles interpolation here */}
|
||||
Welcome back, <Link to={`/@${data.user!.username}`}>@{{ username }}</Link>!
|
||||
</Trans>
|
||||
<br />
|
||||
{t("log-in.callback.redirect-hint")}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RemixForm method="POST">
|
||||
<Form as="div">
|
||||
{actionData?.error && <RegisterError error={actionData.error} />}
|
||||
<Form.Group className="mb-3" controlId="remote-username">
|
||||
<Form.Label>{t("log-in.callback.remote-username.fediverse")}</Form.Label>
|
||||
<Form.Control type="text" readOnly={true} value={data.remoteUser!} />
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-3" controlId="username">
|
||||
<Form.Label>{t("log-in.callback.username")}</Form.Label>
|
||||
<Form.Control name="username" type="text" required />
|
||||
</Form.Group>
|
||||
<input type="hidden" name="ticket" value={data.ticket!} />
|
||||
<Button variant="primary" type="submit">
|
||||
{t("log-in.callback.sign-up-button")}
|
||||
</Button>
|
||||
</Form>
|
||||
</RemixForm>
|
||||
);
|
||||
}
|
|
@ -126,6 +126,9 @@ export default function LoginPage() {
|
|||
{t("log-in.3rd-party.tumblr")}
|
||||
</ListGroup.Item>
|
||||
)}
|
||||
<ListGroup.Item action href="/auth/log-in/fediverse">
|
||||
{t("log-in.3rd-party.fediverse")}
|
||||
</ListGroup.Item>
|
||||
</ListGroup>
|
||||
</div>
|
||||
{!urls.email_enabled && <div className="col-lg-3"></div>}
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
import {
|
||||
LoaderFunctionArgs,
|
||||
json,
|
||||
MetaFunction,
|
||||
ActionFunctionArgs,
|
||||
redirect,
|
||||
} from "@remix-run/node";
|
||||
import i18n from "~/i18next.server";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Form as RemixForm, useActionData } from "@remix-run/react";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import serverRequest from "~/lib/request.server";
|
||||
import { ApiError, ErrorCode } from "~/lib/api/error";
|
||||
import ErrorAlert from "~/components/ErrorAlert";
|
||||
|
||||
export const meta: MetaFunction<typeof loader> = ({ data }) => {
|
||||
return [{ title: `${data?.meta.title || "Log in with a Fediverse account"} • pronouns.cc` }];
|
||||
};
|
||||
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
const t = await i18n.getFixedT(request);
|
||||
|
||||
return json({ meta: { title: t("log-in.fediverse.choose-title") } });
|
||||
};
|
||||
|
||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const body = await request.formData();
|
||||
const instance = body.get("instance") as string | null;
|
||||
if (!instance)
|
||||
return json({
|
||||
error: {
|
||||
status: 403,
|
||||
code: ErrorCode.BadRequest,
|
||||
message: "Invalid instance name",
|
||||
} as ApiError,
|
||||
});
|
||||
|
||||
try {
|
||||
const resp = await serverRequest<{ url: string }>(
|
||||
"GET",
|
||||
`/auth/fediverse?instance=${encodeURIComponent(instance)}`,
|
||||
{
|
||||
isInternal: true,
|
||||
},
|
||||
);
|
||||
|
||||
return redirect(resp.url);
|
||||
} catch (e) {
|
||||
return json({ error: e as ApiError });
|
||||
}
|
||||
};
|
||||
|
||||
export default function AuthFediversePage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const data = useActionData<typeof action>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>{t("log-in.fediverse.choose-form-title")}</h2>
|
||||
{data?.error && <ErrorAlert error={data.error} />}
|
||||
<RemixForm method="POST">
|
||||
<Form as="div">
|
||||
<Form.Group className="mb-3" controlId="instance">
|
||||
<Form.Label>{t("log-in.fediverse-instance-label")}</Form.Label>
|
||||
<Form.Control name="instance" type="text" />
|
||||
</Form.Group>
|
||||
<Button variant="primary" type="submit">
|
||||
{t("log-in.fediverse-log-in-button")}
|
||||
</Button>
|
||||
</Form>
|
||||
</RemixForm>
|
||||
</>
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue