From 116d0577a7ee2203e24737fc953ffe8e37ebd6ea Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 11 Sep 2024 19:13:54 +0200 Subject: [PATCH] improve login page --- .../app/components/ErrorAlert.tsx | 38 +++++ Foxnouns.Frontend/app/lib/api/auth.ts | 6 + Foxnouns.Frontend/app/lib/request.server.ts | 2 + Foxnouns.Frontend/app/root.tsx | 70 +++++++++- .../app/routes/auth.log-in/route.tsx | 130 ++++++++++++++---- Foxnouns.Frontend/public/locales/en.json | 26 +++- 6 files changed, 238 insertions(+), 34 deletions(-) create mode 100644 Foxnouns.Frontend/app/components/ErrorAlert.tsx diff --git a/Foxnouns.Frontend/app/components/ErrorAlert.tsx b/Foxnouns.Frontend/app/components/ErrorAlert.tsx new file mode 100644 index 0000000..e30c516 --- /dev/null +++ b/Foxnouns.Frontend/app/components/ErrorAlert.tsx @@ -0,0 +1,38 @@ +import { TFunction } from "i18next"; +import Alert from "react-bootstrap/Alert"; +import { useTranslation } from "react-i18next"; +import { ApiError, ErrorCode } from "~/lib/api/error"; + +export default function ErrorAlert({ error }: { error: ApiError }) { + const { t } = useTranslation(); + + return ( + + {t("error.heading")} + {errorCodeDesc(t, error.code)} + + ); +} + +export const errorCodeDesc = (t: TFunction, code: ErrorCode) => { + switch (code) { + case ErrorCode.AuthenticationError: + return t("error.errors.authentication-error"); + case ErrorCode.AuthenticationRequired: + return t("error.errors.authentication-required"); + case ErrorCode.BadRequest: + return t("error.errors.bad-request"); + case ErrorCode.Forbidden: + return t("error.errors.forbidden"); + case ErrorCode.GenericApiError: + return t("error.errors.generic-error"); + case ErrorCode.InternalServerError: + return t("error.errors.internal-server-error"); + case ErrorCode.MemberNotFound: + return t("error.errors.member-not-found"); + case ErrorCode.UserNotFound: + return t("error.errors.user-not-found"); + } + + return t("error.errors.generic-error"); +}; diff --git a/Foxnouns.Frontend/app/lib/api/auth.ts b/Foxnouns.Frontend/app/lib/api/auth.ts index 8938c05..bda9925 100644 --- a/Foxnouns.Frontend/app/lib/api/auth.ts +++ b/Foxnouns.Frontend/app/lib/api/auth.ts @@ -5,3 +5,9 @@ export type AuthResponse = { token: string; expires_at: string; }; + +export type AuthUrls = { + discord?: string; + google?: string; + tumblr?: string; +}; diff --git a/Foxnouns.Frontend/app/lib/request.server.ts b/Foxnouns.Frontend/app/lib/request.server.ts index d7add9b..d6192ae 100644 --- a/Foxnouns.Frontend/app/lib/request.server.ts +++ b/Foxnouns.Frontend/app/lib/request.server.ts @@ -39,6 +39,8 @@ export default async function serverRequest( return (await resp.json()) as T; } +export const getToken = (req: Request) => getCookie(req, "pronounscc-token"); + export function getCookie(req: Request, cookieName: string): string | undefined { const header = req.headers.get("Cookie"); if (!header) return undefined; diff --git a/Foxnouns.Frontend/app/root.tsx b/Foxnouns.Frontend/app/root.tsx index d70be26..8452919 100644 --- a/Foxnouns.Frontend/app/root.tsx +++ b/Foxnouns.Frontend/app/root.tsx @@ -6,6 +6,8 @@ import { Scripts, ScrollRestoration, useLoaderData, + useRouteError, + useRouteLoaderData, } from "@remix-run/react"; import { LoaderFunctionArgs } from "@remix-run/node"; import { useChangeLanguage } from "remix-i18next/react"; @@ -20,6 +22,7 @@ import { ApiError, ErrorCode } from "./lib/api/error"; import "./app.scss"; import getLocalSettings from "./lib/settings.server"; import { LANGUAGE } from "~/env.server"; +import { errorCodeDesc } from "./components/ErrorAlert"; export const loader = async ({ request }: LoaderFunctionArgs) => { const meta = await serverRequest("GET", "/meta"); @@ -51,13 +54,29 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { }; export function Layout({ children }: { children: React.ReactNode }) { - const { settings, locale } = useLoaderData(); + const { locale, settings } = useRouteLoaderData("root") || { + meta: { + users: { + total: 0, + active_month: 0, + active_week: 0, + active_day: 0, + }, + members: 0, + version: "", + hash: "", + }, + }; const { i18n } = useTranslation(); - i18n.language = locale; - useChangeLanguage(locale); + i18n.language = locale || "en"; + useChangeLanguage(locale || "en"); return ( - + @@ -73,6 +92,49 @@ export function Layout({ children }: { children: React.ReactNode }) { ); } +export function ErrorBoundary() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const error: any = useRouteError(); + const { t } = useTranslation(); + + console.log(error); + + const errorElem = + "code" in error && "message" in error ? ( + + ) : ( + <>{t("error.errors.generic-error")} + ); + + return ( + + + + <>{t("error.title")} - pronouns.cc</> + + + + + + {errorElem} + + + + ); +} + +function ApiErrorElem({ error }: { error: ApiError }) { + const { t } = useTranslation(); + const errorDesc = errorCodeDesc(t, error.code); + + return ( + <> +

{t("error.heading")}

+

{errorDesc}

+ + ); +} + export default function App() { const { meta, meUser, settings } = useLoaderData(); diff --git a/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx b/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx index 4fd891f..3d0d387 100644 --- a/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx +++ b/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx @@ -1,11 +1,23 @@ -import { MetaFunction, json, LoaderFunctionArgs, ActionFunction, redirect } from "@remix-run/node"; -import { Form as RemixForm } from "@remix-run/react"; +import { + MetaFunction, + json, + LoaderFunctionArgs, + redirect, + ActionFunctionArgs, +} from "@remix-run/node"; +import { Form as RemixForm, useActionData, useLoaderData } from "@remix-run/react"; import Form from "react-bootstrap/Form"; import Button from "react-bootstrap/Button"; +import ButtonGroup from "react-bootstrap/ButtonGroup"; +import ListGroup from "react-bootstrap/ListGroup"; +import { Container, Row, Col } from "react-bootstrap"; import { useTranslation } from "react-i18next"; import i18n from "~/i18next.server"; -import serverRequest, { writeCookie } from "~/lib/request.server"; -import { AuthResponse } from "~/lib/api/auth"; +import serverRequest, { getToken, writeCookie } from "~/lib/request.server"; +import { AuthResponse, AuthUrls } from "~/lib/api/auth"; +import { ApiError, ErrorCode } from "~/lib/api/error"; +import ErrorAlert from "~/components/ErrorAlert"; +import { User } from "~/lib/api/user"; export const meta: MetaFunction = ({ data }) => { return [{ title: `${data?.meta.title || "Log in"} - pronouns.cc` }]; @@ -13,50 +25,110 @@ export const meta: MetaFunction = ({ data }) => { export const loader = async ({ request }: LoaderFunctionArgs) => { const t = await i18n.getFixedT(request); + const token = getToken(request); + if (token) { + try { + await serverRequest("GET", "/users/@me", { token }); + return redirect("/?err=already-logged-in", 303); + } catch (e) { + // ignore + } + } + + const urls = await serverRequest("POST", "/auth/urls"); return json({ meta: { title: t("log-in.title") }, + urls, }); }; -export const action: ActionFunction = async ({ request }) => { +export const action = async ({ request }: ActionFunctionArgs) => { const body = await request.formData(); const email = body.get("email") as string | null; const password = body.get("password") as string | null; console.log(email, password); - const resp = await serverRequest("POST", "/auth/email/login", { - body: { email, password }, - }); + try { + const resp = await serverRequest("POST", "/auth/email/login", { + body: { email, password }, + }); - return redirect("/", { - status: 303, - headers: { - "Set-Cookie": writeCookie("pronounscc-token", resp.token), - }, - }); + return redirect("/", { + status: 303, + headers: { + "Set-Cookie": writeCookie("pronounscc-token", resp.token), + }, + }); + } catch (e) { + return json({ error: e as ApiError }); + } }; export default function LoginPage() { const { t } = useTranslation(); + const { urls } = useLoaderData(); + const actionData = useActionData(); return ( - -
- - {t("log-in.email")} - - - - {t("log-in.password")} - - + + + +

{t("log-in.form-title")}

+ {actionData?.error && } + + + + {t("log-in.email")} + + + + {t("log-in.password")} + + - - - + + + + + +
+ + +

{t("log-in.3rd-party.title")}

+

{t("log-in.3rd-party.desc")}

+ + {urls.discord && ( + + {t("log-in.3rd-party.discord")} + + )} + {urls.google && ( + + {t("log-in.3rd-party.google")} + + )} + {urls.tumblr && ( + + {t("log-in.3rd-party.tumblr")} + + )} + + + + ); } + +function LoginError({ error }: { error: ApiError }) { + const { t } = useTranslation(); + + if (error.code !== ErrorCode.UserNotFound) return ; + + return <>{t("log-in.invalid-credentials")}; +} diff --git a/Foxnouns.Frontend/public/locales/en.json b/Foxnouns.Frontend/public/locales/en.json index 17b3948..d383266 100644 --- a/Foxnouns.Frontend/public/locales/en.json +++ b/Foxnouns.Frontend/public/locales/en.json @@ -1,4 +1,18 @@ { + "error": { + "heading": "An error occurred", + "errors": { + "authentication-error": "There was an error validating your credentials.", + "authentication-required": "You need to log in.", + "bad-request": "Server rejected your input, please check anything for errors.", + "forbidden": "You are not allowed to perform that action.", + "generic-error": "An unknown error occurred.", + "internal-server-error": "Server experienced an internal error, please try again later.", + "member-not-found": "Member not found, please check your spelling and try again.", + "user-not-found": "User not found, please check your spelling and try again." + }, + "title": "Error" + }, "navbar": { "view-profile": "View profile", "settings": "Settings", @@ -11,8 +25,18 @@ }, "log-in": { "title": "Log in", + "form-title": "Log in with email", "email": "Email address", "password": "Password", - "log-in-button": "Log in" + "log-in-button": "Log in", + "register-with-email": "Register with email", + "3rd-party": { + "title": "Log in with another service", + "desc": "If you prefer, you can also log in with one of these services:", + "discord": "Log in with Discord", + "google": "Log in with Google", + "tumblr": "Log in with Tumblr" + }, + "invalid-credentials": "Invalid email address or password, please check your spelling and try again." } }