From 40da4865bce62dd862fb46483b8a0dfe0d80052e Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 2 Oct 2024 16:49:33 +0200 Subject: [PATCH 1/2] feat(frontend): add confirmation before force log out --- Foxnouns.Frontend/app/lib/request.server.ts | 4 +-- .../app/routes/settings._index/route.tsx | 20 +++++++----- .../app/routes/settings.auth/route.tsx | 2 +- .../routes/settings.force-log-out/route.tsx | 29 +++++++++++++++++ .../app/routes/settings/route.tsx | 31 +++++++++++-------- Foxnouns.Frontend/public/locales/en.json | 3 +- 6 files changed, 65 insertions(+), 24 deletions(-) diff --git a/Foxnouns.Frontend/app/lib/request.server.ts b/Foxnouns.Frontend/app/lib/request.server.ts index 562666d..7da3e84 100644 --- a/Foxnouns.Frontend/app/lib/request.server.ts +++ b/Foxnouns.Frontend/app/lib/request.server.ts @@ -1,5 +1,5 @@ import { parse as parseCookie, serialize as serializeCookie } from "cookie"; -import { API_BASE, INTERNAL_API_BASE } from "~/env.server"; +import { INTERNAL_API_BASE } from "~/env.server"; import { ApiError, ErrorCode } from "./api/error"; import { tokenCookieName } from "~/lib/utils"; @@ -16,7 +16,7 @@ export async function baseRequest( path: string, params: RequestParams = {}, ): Promise { - const base = params.isInternal ? INTERNAL_API_BASE + "/internal" : API_BASE + "/v2"; + const base = params.isInternal ? INTERNAL_API_BASE + "/internal" : INTERNAL_API_BASE + "/v2"; const url = `${base}${path}`; const resp = await fetch(url, { diff --git a/Foxnouns.Frontend/app/routes/settings._index/route.tsx b/Foxnouns.Frontend/app/routes/settings._index/route.tsx index 5b90851..88655fc 100644 --- a/Foxnouns.Frontend/app/routes/settings._index/route.tsx +++ b/Foxnouns.Frontend/app/routes/settings._index/route.tsx @@ -1,6 +1,13 @@ import { Button, Form, InputGroup, Table } from "react-bootstrap"; import { useTranslation } from "react-i18next"; -import { Form as RemixForm, useActionData, useFetcher, useRouteLoaderData } from "@remix-run/react"; +import { + Form as RemixForm, + Link, + Outlet, + useActionData, + useFetcher, + useRouteLoaderData, +} from "@remix-run/react"; import { loader as settingsLoader } from "../settings/route"; import { loader as rootLoader } from "../../root"; import { DateTime } from "luxon"; @@ -43,12 +50,12 @@ export default function SettingsIndex() { const actionData = useActionData(); const { meta } = useRouteLoaderData("root")!; const { t } = useTranslation(); - const fetcher = useFetcher(); const createdAt = idTimestamp(user.id); return ( <> +
@@ -81,11 +88,10 @@ export default function SettingsIndex() {

{t("settings.general.log-out-everywhere")}

{t("settings.general.log-out-everywhere-hint")}

- - - + {/* @ts-expect-error as=Link */} +

{t("settings.general.table-header")}

diff --git a/Foxnouns.Frontend/app/routes/settings.auth/route.tsx b/Foxnouns.Frontend/app/routes/settings.auth/route.tsx index 22d2fcd..a8ca303 100644 --- a/Foxnouns.Frontend/app/routes/settings.auth/route.tsx +++ b/Foxnouns.Frontend/app/routes/settings.auth/route.tsx @@ -44,7 +44,7 @@ function EmailSettings({ user }: { user: MeUser }) { )} {emails.length < 3 && (

- {/* @ts-expect-error using as=Link causes an error here, even though it runs completely fine */} + {/* @ts-expect-error as=Link */} + {/* @ts-expect-error as=Link */} + + + + + ); +} diff --git a/Foxnouns.Frontend/app/routes/settings/route.tsx b/Foxnouns.Frontend/app/routes/settings/route.tsx index 3ef35fd..9d8247a 100644 --- a/Foxnouns.Frontend/app/routes/settings/route.tsx +++ b/Foxnouns.Frontend/app/routes/settings/route.tsx @@ -30,30 +30,35 @@ export default function SettingsLayout() { const { t } = useTranslation(); const { pathname } = useLocation(); + const isActive = (matches: string[] | string, startsWith: boolean = false) => + startsWith + ? typeof matches === "string" + ? pathname.startsWith(matches) + : matches.some((m) => pathname.startsWith(m)) + : typeof matches === "string" + ? matches === pathname + : matches.includes(pathname); + return ( <>

diff --git a/Foxnouns.Frontend/public/locales/en.json b/Foxnouns.Frontend/public/locales/en.json index 5a25098..c834e95 100644 --- a/Foxnouns.Frontend/public/locales/en.json +++ b/Foxnouns.Frontend/public/locales/en.json @@ -105,7 +105,8 @@ "member-list-hidden": "Member list hidden?", "custom-preferences": "Custom preferences", "role": "Account role", - "username-update-error": "Could not update your username as the new username is invalid:\n{{message}}" + "username-update-error": "Could not update your username as the new username is invalid:\n{{message}}", + "log-out-everywhere-confirm": "Are you sure you want to log out everywhere?\nPlease double check your authentication methods before doing so, as it might lock you out of your account." }, "auth": { "title": "Authentication", From 567e7941543fe830c083beadd876544f5d9406f6 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 2 Oct 2024 21:05:52 +0200 Subject: [PATCH 2/2] feat(frontend): hide everything email related if it's disabled on the backend --- .../Authentication/AuthController.cs | 4 +- .../Authentication/EmailAuthController.cs | 4 ++ Foxnouns.Frontend/app/lib/api/auth.ts | 1 + .../app/routes/auth.log-in/route.tsx | 62 ++++++++++--------- .../app/routes/settings.auth/route.tsx | 15 ++--- 5 files changed, 48 insertions(+), 38 deletions(-) diff --git a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs index 53aa171..1a737eb 100644 --- a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs @@ -39,10 +39,10 @@ public class AuthController( + $"&prompt=none&state={state}" + $"&redirect_uri={HttpUtility.UrlEncode($"{config.BaseUrl}/auth/callback/discord")}"; - return Ok(new UrlsResponse(discord, null, null)); + return Ok(new UrlsResponse(config.EmailAuth.Enabled, discord, null, null)); } - private record UrlsResponse(string? Discord, string? Google, string? Tumblr); + private record UrlsResponse(bool EmailEnabled, string? Discord, string? Google, string? Tumblr); public record AuthResponse( UserRendererService.UserResponse User, diff --git a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs index 937ab3a..7e3706e 100644 --- a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs @@ -100,6 +100,8 @@ public class EmailAuthController( [FromBody] CompleteRegistrationRequest req ) { + CheckRequirements(); + var email = await keyCacheService.GetKeyAsync($"email:{req.Ticket}"); if (email == null) throw new ApiError.BadRequest("Unknown ticket", "ticket", req.Ticket); @@ -185,6 +187,8 @@ public class EmailAuthController( [Authorize("*")] public async Task AddEmailAddressAsync([FromBody] AddEmailAddressRequest req) { + CheckRequirements(); + var emails = await db .AuthMethods.Where(m => m.UserId == CurrentUser!.Id && m.AuthType == AuthType.Email) .ToListAsync(); diff --git a/Foxnouns.Frontend/app/lib/api/auth.ts b/Foxnouns.Frontend/app/lib/api/auth.ts index a0d5bf1..0f8ce27 100644 --- a/Foxnouns.Frontend/app/lib/api/auth.ts +++ b/Foxnouns.Frontend/app/lib/api/auth.ts @@ -16,6 +16,7 @@ export type CallbackResponse = { }; export type AuthUrls = { + email_enabled: boolean; discord?: string; google?: string; tumblr?: string; diff --git a/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx b/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx index eadbaa9..aaffd80 100644 --- a/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx +++ b/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx @@ -11,7 +11,7 @@ import { useActionData, useLoaderData, } from "@remix-run/react"; -import { Form, Button, ButtonGroup, ListGroup, Row, Col } from "react-bootstrap"; +import { Form, Button, ButtonGroup, ListGroup } from "react-bootstrap"; import { useTranslation } from "react-i18next"; import i18n from "~/i18next.server"; import serverRequest, { getToken, writeCookie } from "~/lib/request.server"; @@ -78,33 +78,36 @@ export default function LoginPage() { return ( <> - - -

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

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

{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")}

@@ -124,8 +127,9 @@ export default function LoginPage() { )} - - +
+ {!urls.email_enabled &&
} + ); } diff --git a/Foxnouns.Frontend/app/routes/settings.auth/route.tsx b/Foxnouns.Frontend/app/routes/settings.auth/route.tsx index a8ca303..125f413 100644 --- a/Foxnouns.Frontend/app/routes/settings.auth/route.tsx +++ b/Foxnouns.Frontend/app/routes/settings.auth/route.tsx @@ -1,10 +1,12 @@ import i18n from "~/i18next.server"; import { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; -import { Link, useRouteLoaderData } from "@remix-run/react"; +import { Link, useLoaderData, useRouteLoaderData } from "@remix-run/react"; import { Button, ListGroup } from "react-bootstrap"; import { loader as settingsLoader } from "~/routes/settings/route"; import { useTranslation } from "react-i18next"; import { AuthMethod, MeUser } from "~/lib/api/user"; +import serverRequest from "~/lib/request.server"; +import { AuthUrls } from "~/lib/api/auth"; export const meta: MetaFunction = ({ data }) => { return [{ title: `${data?.meta.title || "Authentication"} • pronouns.cc` }]; @@ -12,17 +14,16 @@ export const meta: MetaFunction = ({ data }) => { export const loader = async ({ request }: LoaderFunctionArgs) => { const t = await i18n.getFixedT(request); - return { meta: { title: t("settings.auth.title") } }; + const urls = await serverRequest("POST", "/auth/urls", { isInternal: true }); + + return { urls, meta: { title: t("settings.auth.title") } }; }; export default function AuthSettings() { + const { urls } = useLoaderData(); const { user } = useRouteLoaderData("routes/settings")!; - return ( -
- -
- ); + return
{urls.email_enabled && }
; } function EmailSettings({ user }: { user: MeUser }) {