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/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..a3b5877 100644
--- a/Foxnouns.Frontend/app/root.tsx
+++ b/Foxnouns.Frontend/app/root.tsx
@@ -6,10 +6,11 @@ import {
Scripts,
ScrollRestoration,
useLoaderData,
+ useRouteError,
} from "@remix-run/react";
import { LoaderFunctionArgs } from "@remix-run/node";
import { useChangeLanguage } from "remix-i18next/react";
-import { useTranslation } from "react-i18next";
+import { Trans, useTranslation } from "react-i18next";
import serverRequest, { getCookie, writeCookie } from "./lib/request.server";
import Meta from "./lib/api/meta";
@@ -20,6 +21,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");
@@ -73,6 +75,47 @@ 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..7eccac5 100644
--- a/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx
+++ b/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx
@@ -1,11 +1,22 @@
-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, Link, useActionData } from "@remix-run/react";
import Form from "react-bootstrap/Form";
import Button from "react-bootstrap/Button";
+import Row from "react-bootstrap/Row";
+import Col from "react-bootstrap/Col";
import { useTranslation } from "react-i18next";
import i18n from "~/i18next.server";
-import serverRequest, { writeCookie } from "~/lib/request.server";
+import serverRequest, { getToken, writeCookie } from "~/lib/request.server";
import { AuthResponse } 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 +24,85 @@ 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 });
+
+ throw redirect("/?err=already-logged-in", 303);
+ } catch (e) {
+ // ignore
+ }
+ }
return json({
meta: { title: t("log-in.title") },
});
};
-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 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")}
+
+
);
}
+
+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..5bcf161 100644
--- a/Foxnouns.Frontend/public/locales/en.json
+++ b/Foxnouns.Frontend/public/locales/en.json
@@ -1,18 +1,35 @@
{
- "navbar": {
- "view-profile": "View profile",
- "settings": "Settings",
- "log-out": "Log out",
- "log-in": "Log in or sign up",
- "theme": "Theme",
- "theme-auto": "Automatic",
- "theme-dark": "Dark",
- "theme-light": "Light"
- },
- "log-in": {
- "title": "Log in",
- "email": "Email address",
- "password": "Password",
- "log-in-button": "Log in"
- }
+ "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."
+ }
+ },
+ "navbar": {
+ "view-profile": "View profile",
+ "settings": "Settings",
+ "log-out": "Log out",
+ "log-in": "Log in or sign up",
+ "theme": "Theme",
+ "theme-auto": "Automatic",
+ "theme-dark": "Dark",
+ "theme-light": "Light"
+ },
+ "log-in": {
+ "title": "Log in",
+ "form-title": "",
+ "email": "Email address",
+ "password": "Password",
+ "log-in-button": "Log in",
+ "register-with-email": "Register with email",
+ "3rd-party-title": "Log in with another account",
+ "invalid-credentials": "Invalid email address or password, please check your spelling and try again."
+ }
}