feat(frontend): create account from discord, better error alert
This commit is contained in:
		
							parent
							
								
									ff22530f0a
								
							
						
					
					
						commit
						103ba24555
					
				
					 4 changed files with 292 additions and 66 deletions
				
			
		|  | @ -1,11 +1,23 @@ | |||
| import { json, LoaderFunctionArgs } from "@remix-run/node"; | ||||
| import { type ApiError, ErrorCode } from "~/lib/api/error"; | ||||
| import { ActionFunctionArgs, json, redirect, LoaderFunctionArgs } from "@remix-run/node"; | ||||
| import { type ApiError, ErrorCode, firstErrorFor } from "~/lib/api/error"; | ||||
| import serverRequest, { writeCookie } from "~/lib/request.server"; | ||||
| import { CallbackResponse } from "~/lib/api/auth"; | ||||
| import { Form as RemixForm, Link, useLoaderData } from "@remix-run/react"; | ||||
| 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 from "react-bootstrap/Form"; | ||||
| import Button from "react-bootstrap/Button"; | ||||
| import ErrorAlert from "~/components/ErrorAlert"; | ||||
| import Alert from "react-bootstrap/Alert"; | ||||
| 
 | ||||
| export const shouldRevalidate: ShouldRevalidateFunction = ({ actionResult }) => { | ||||
| 	return !actionResult; | ||||
| }; | ||||
| 
 | ||||
| export const loader = async ({ request }: LoaderFunctionArgs) => { | ||||
| 	const url = new URL(request.url); | ||||
|  | @ -17,7 +29,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { | |||
| 		throw { status: 400, code: ErrorCode.BadRequest, message: "Missing code or state" } as ApiError; | ||||
| 
 | ||||
| 	const resp = await serverRequest<CallbackResponse>("POST", "/auth/discord/callback", { | ||||
| 		body: { code, state } | ||||
| 		body: { code, state }, | ||||
| 	}); | ||||
| 
 | ||||
| 	if (resp.has_account) { | ||||
|  | @ -25,9 +37,9 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { | |||
| 			{ hasAccount: true, user: resp.user!, ticket: null, remoteUser: null }, | ||||
| 			{ | ||||
| 				headers: { | ||||
| 					"Set-Cookie": writeCookie("pronounscc-token", resp.token!) | ||||
| 				} | ||||
| 			} | ||||
| 					"Set-Cookie": writeCookie("pronounscc-token", resp.token!), | ||||
| 				}, | ||||
| 			}, | ||||
| 		); | ||||
| 	} | ||||
| 
 | ||||
|  | @ -35,26 +47,62 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { | |||
| 		hasAccount: false, | ||||
| 		user: null, | ||||
| 		ticket: resp.ticket!, | ||||
| 		remoteUser: resp.remote_username! | ||||
| 		remoteUser: resp.remote_username!, | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| // TODO: action function
 | ||||
| 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/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<typeof loader>(); | ||||
| 	const actionData = useActionData<typeof action>(); | ||||
| 
 | ||||
| 	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 }}> | ||||
| 					<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>! | ||||
| 						Welcome back, <Link to={`/@${data.user!.username}`}>@{{ username }}</Link>! | ||||
| 					</Trans> | ||||
| 					<br /> | ||||
| 					{t("log-in.callback.redirect-hint")} | ||||
|  | @ -66,6 +114,7 @@ export default function DiscordCallbackPage() { | |||
| 	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.discord")}</Form.Label> | ||||
| 					<Form.Control type="text" readOnly={true} value={data.remoteUser!} /> | ||||
|  | @ -82,3 +131,34 @@ 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} />; | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue