From be34c4c77e436f19ff8f94b5949e96762471d5fd Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 10 Sep 2024 21:24:40 +0200 Subject: [PATCH] feat(frontend): working email login --- Foxnouns.Backend/Services/AuthService.cs | 6 +- .../app/components/nav/Navbar.tsx | 2 +- Foxnouns.Frontend/app/lib/api/auth.ts | 7 +++ Foxnouns.Frontend/app/root.tsx | 7 +-- .../app/routes/$username/route.tsx | 4 +- .../app/routes/auth.log-in/route.tsx | 62 +++++++++++++++++++ Foxnouns.Frontend/i18next-parser.config.js | 4 +- Foxnouns.Frontend/public/locales/en.json | 6 ++ 8 files changed, 85 insertions(+), 13 deletions(-) create mode 100644 Foxnouns.Frontend/app/lib/api/auth.ts create mode 100644 Foxnouns.Frontend/app/routes/auth.log-in/route.tsx diff --git a/Foxnouns.Backend/Services/AuthService.cs b/Foxnouns.Backend/Services/AuthService.cs index decb240..22a28d2 100644 --- a/Foxnouns.Backend/Services/AuthService.cs +++ b/Foxnouns.Backend/Services/AuthService.cs @@ -81,11 +81,11 @@ public class AuthService(IClock clock, DatabaseContext db, ISnowflakeGenerator s var user = await db.Users.FirstOrDefaultAsync(u => u.AuthMethods.Any(a => a.AuthType == AuthType.Email && a.RemoteId == email), ct); if (user == null) - throw new ApiError.NotFound("No user with that email address found, or password is incorrect"); + throw new ApiError.NotFound("No user with that email address found, or password is incorrect", ErrorCode.UserNotFound); var pwResult = await Task.Run(() => _passwordHasher.VerifyHashedPassword(user, user.Password!, password), ct); - if (pwResult == PasswordVerificationResult.Failed) - throw new ApiError.NotFound("No user with that email address found, or password is incorrect"); + if (pwResult == PasswordVerificationResult.Failed) // TODO: this seems to fail on some valid passwords? + throw new ApiError.NotFound("No user with that email address found, or password is incorrect", ErrorCode.UserNotFound); if (pwResult == PasswordVerificationResult.SuccessRehashNeeded) { user.Password = await Task.Run(() => _passwordHasher.HashPassword(user, password), ct); diff --git a/Foxnouns.Frontend/app/components/nav/Navbar.tsx b/Foxnouns.Frontend/app/components/nav/Navbar.tsx index 75f7d7d..dbccac7 100644 --- a/Foxnouns.Frontend/app/components/nav/Navbar.tsx +++ b/Foxnouns.Frontend/app/components/nav/Navbar.tsx @@ -36,7 +36,7 @@ export default function MainNavbar({ ) : ( - + {t("navbar.log-in")} ); diff --git a/Foxnouns.Frontend/app/lib/api/auth.ts b/Foxnouns.Frontend/app/lib/api/auth.ts new file mode 100644 index 0000000..8938c05 --- /dev/null +++ b/Foxnouns.Frontend/app/lib/api/auth.ts @@ -0,0 +1,7 @@ +import { User } from "~/lib/api/user"; + +export type AuthResponse = { + user: User; + token: string; + expires_at: string; +}; diff --git a/Foxnouns.Frontend/app/root.tsx b/Foxnouns.Frontend/app/root.tsx index b8b9d09..d70be26 100644 --- a/Foxnouns.Frontend/app/root.tsx +++ b/Foxnouns.Frontend/app/root.tsx @@ -7,8 +7,7 @@ import { ScrollRestoration, useLoaderData, } from "@remix-run/react"; -import { LoaderFunction } from "@remix-run/node"; -import SSRProvider from "react-bootstrap/SSRProvider"; +import { LoaderFunctionArgs } from "@remix-run/node"; import { useChangeLanguage } from "remix-i18next/react"; import { useTranslation } from "react-i18next"; @@ -22,7 +21,7 @@ import "./app.scss"; import getLocalSettings from "./lib/settings.server"; import { LANGUAGE } from "~/env.server"; -export const loader: LoaderFunction = async ({ request }) => { +export const loader = async ({ request }: LoaderFunctionArgs) => { const meta = await serverRequest("GET", "/meta"); const token = getCookie(request, "pronounscc-token"); @@ -66,7 +65,7 @@ export function Layout({ children }: { children: React.ReactNode }) { - {children} + {children} diff --git a/Foxnouns.Frontend/app/routes/$username/route.tsx b/Foxnouns.Frontend/app/routes/$username/route.tsx index a26fb49..418b3e8 100644 --- a/Foxnouns.Frontend/app/routes/$username/route.tsx +++ b/Foxnouns.Frontend/app/routes/$username/route.tsx @@ -1,4 +1,4 @@ -import { json, LoaderFunction, MetaFunction } from "@remix-run/node"; +import { json, LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { redirect, useLoaderData } from "@remix-run/react"; import { User } from "~/lib/api/user"; import serverRequest from "~/lib/request.server"; @@ -9,7 +9,7 @@ export const meta: MetaFunction = ({ data }) => { return [{ title: `@${user.username} - pronouns.cc` }]; }; -export const loader: LoaderFunction = async ({ params }) => { +export const loader = async ({ params }: LoaderFunctionArgs) => { let username = params.username!; if (!username.startsWith("@")) throw redirect(`/@${username}`); username = username.substring("@".length); diff --git a/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx b/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx new file mode 100644 index 0000000..4fd891f --- /dev/null +++ b/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx @@ -0,0 +1,62 @@ +import { MetaFunction, json, LoaderFunctionArgs, ActionFunction, redirect } from "@remix-run/node"; +import { Form as RemixForm } from "@remix-run/react"; +import Form from "react-bootstrap/Form"; +import Button from "react-bootstrap/Button"; +import { useTranslation } from "react-i18next"; +import i18n from "~/i18next.server"; +import serverRequest, { writeCookie } from "~/lib/request.server"; +import { AuthResponse } from "~/lib/api/auth"; + +export const meta: MetaFunction = ({ data }) => { + return [{ title: `${data?.meta.title || "Log in"} - pronouns.cc` }]; +}; + +export const loader = async ({ request }: LoaderFunctionArgs) => { + const t = await i18n.getFixedT(request); + + return json({ + meta: { title: t("log-in.title") }, + }); +}; + +export const action: ActionFunction = async ({ request }) => { + 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 }, + }); + + return redirect("/", { + status: 303, + headers: { + "Set-Cookie": writeCookie("pronounscc-token", resp.token), + }, + }); +}; + +export default function LoginPage() { + const { t } = useTranslation(); + + return ( + +
+ + {t("log-in.email")} + + + + {t("log-in.password")} + + + + +
+
+ ); +} diff --git a/Foxnouns.Frontend/i18next-parser.config.js b/Foxnouns.Frontend/i18next-parser.config.js index 41d6da6..a1e6625 100644 --- a/Foxnouns.Frontend/i18next-parser.config.js +++ b/Foxnouns.Frontend/i18next-parser.config.js @@ -1,5 +1,3 @@ -import i18n from "./app/i18n.js"; - export default { - locales: i18n.supportedLngs, + locales: ["en"], }; diff --git a/Foxnouns.Frontend/public/locales/en.json b/Foxnouns.Frontend/public/locales/en.json index f576541..17b3948 100644 --- a/Foxnouns.Frontend/public/locales/en.json +++ b/Foxnouns.Frontend/public/locales/en.json @@ -8,5 +8,11 @@ "theme-auto": "Automatic", "theme-dark": "Dark", "theme-light": "Light" + }, + "log-in": { + "title": "Log in", + "email": "Email address", + "password": "Password", + "log-in-button": "Log in" } }