feat(frontend): add confirmation before force log out
This commit is contained in:
		
							parent
							
								
									e030342358
								
							
						
					
					
						commit
						40da4865bc
					
				
					 6 changed files with 65 additions and 24 deletions
				
			
		| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
import { parse as parseCookie, serialize as serializeCookie } from "cookie";
 | 
					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 { ApiError, ErrorCode } from "./api/error";
 | 
				
			||||||
import { tokenCookieName } from "~/lib/utils";
 | 
					import { tokenCookieName } from "~/lib/utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,7 +16,7 @@ export async function baseRequest(
 | 
				
			||||||
	path: string,
 | 
						path: string,
 | 
				
			||||||
	params: RequestParams = {},
 | 
						params: RequestParams = {},
 | 
				
			||||||
): Promise<Response> {
 | 
					): Promise<Response> {
 | 
				
			||||||
	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 url = `${base}${path}`;
 | 
				
			||||||
	const resp = await fetch(url, {
 | 
						const resp = await fetch(url, {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,13 @@
 | 
				
			||||||
import { Button, Form, InputGroup, Table } from "react-bootstrap";
 | 
					import { Button, Form, InputGroup, Table } from "react-bootstrap";
 | 
				
			||||||
import { useTranslation } from "react-i18next";
 | 
					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 settingsLoader } from "../settings/route";
 | 
				
			||||||
import { loader as rootLoader } from "../../root";
 | 
					import { loader as rootLoader } from "../../root";
 | 
				
			||||||
import { DateTime } from "luxon";
 | 
					import { DateTime } from "luxon";
 | 
				
			||||||
| 
						 | 
					@ -43,12 +50,12 @@ export default function SettingsIndex() {
 | 
				
			||||||
	const actionData = useActionData<typeof action>();
 | 
						const actionData = useActionData<typeof action>();
 | 
				
			||||||
	const { meta } = useRouteLoaderData<typeof rootLoader>("root")!;
 | 
						const { meta } = useRouteLoaderData<typeof rootLoader>("root")!;
 | 
				
			||||||
	const { t } = useTranslation();
 | 
						const { t } = useTranslation();
 | 
				
			||||||
	const fetcher = useFetcher();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const createdAt = idTimestamp(user.id);
 | 
						const createdAt = idTimestamp(user.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return (
 | 
						return (
 | 
				
			||||||
		<>
 | 
							<>
 | 
				
			||||||
 | 
								<Outlet />
 | 
				
			||||||
			<div className="row">
 | 
								<div className="row">
 | 
				
			||||||
				<div className="col-md">
 | 
									<div className="col-md">
 | 
				
			||||||
					<RemixForm method="POST">
 | 
										<RemixForm method="POST">
 | 
				
			||||||
| 
						 | 
					@ -81,11 +88,10 @@ export default function SettingsIndex() {
 | 
				
			||||||
			<div>
 | 
								<div>
 | 
				
			||||||
				<h4>{t("settings.general.log-out-everywhere")}</h4>
 | 
									<h4>{t("settings.general.log-out-everywhere")}</h4>
 | 
				
			||||||
				<p>{t("settings.general.log-out-everywhere-hint")}</p>
 | 
									<p>{t("settings.general.log-out-everywhere-hint")}</p>
 | 
				
			||||||
				<fetcher.Form method="POST" action="/settings/force-log-out">
 | 
									{/* @ts-expect-error as=Link */}
 | 
				
			||||||
					<Button type="submit" variant="danger">
 | 
									<Button as={Link} variant="danger" to="/settings/force-log-out">
 | 
				
			||||||
						{t("settings.general.force-log-out-button")}
 | 
										{t("settings.general.force-log-out-button")}
 | 
				
			||||||
					</Button>
 | 
									</Button>
 | 
				
			||||||
				</fetcher.Form>
 | 
					 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			<h4 className="mt-2">{t("settings.general.table-header")}</h4>
 | 
								<h4 className="mt-2">{t("settings.general.table-header")}</h4>
 | 
				
			||||||
			<Table striped bordered hover>
 | 
								<Table striped bordered hover>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -44,7 +44,7 @@ function EmailSettings({ user }: { user: MeUser }) {
 | 
				
			||||||
			)}
 | 
								)}
 | 
				
			||||||
			{emails.length < 3 && (
 | 
								{emails.length < 3 && (
 | 
				
			||||||
				<p>
 | 
									<p>
 | 
				
			||||||
					{/* @ts-expect-error using as=Link causes an error here, even though it runs completely fine */}
 | 
										{/* @ts-expect-error as=Link */}
 | 
				
			||||||
					<Button variant="primary" as={Link} to="/settings/auth/add-email">
 | 
										<Button variant="primary" as={Link} to="/settings/auth/add-email">
 | 
				
			||||||
						{emails.length === 0
 | 
											{emails.length === 0
 | 
				
			||||||
							? t("settings.auth.form.add-first-email")
 | 
												? t("settings.auth.form.add-first-email")
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,9 @@
 | 
				
			||||||
import { ActionFunction, redirect } from "@remix-run/node";
 | 
					import { ActionFunction, redirect } from "@remix-run/node";
 | 
				
			||||||
import { fastRequest, getToken, writeCookie } from "~/lib/request.server";
 | 
					import { fastRequest, getToken, writeCookie } from "~/lib/request.server";
 | 
				
			||||||
import { tokenCookieName } from "~/lib/utils";
 | 
					import { tokenCookieName } from "~/lib/utils";
 | 
				
			||||||
 | 
					import { Button, Form } from "react-bootstrap";
 | 
				
			||||||
 | 
					import { useTranslation } from "react-i18next";
 | 
				
			||||||
 | 
					import { Form as RemixForm, Link } from "@remix-run/react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const action: ActionFunction = async ({ request }) => {
 | 
					export const action: ActionFunction = async ({ request }) => {
 | 
				
			||||||
	const token = getToken(request);
 | 
						const token = getToken(request);
 | 
				
			||||||
| 
						 | 
					@ -17,3 +20,29 @@ export const action: ActionFunction = async ({ request }) => {
 | 
				
			||||||
		headers: { "Set-Cookie": writeCookie(tokenCookieName, "token", 0) },
 | 
							headers: { "Set-Cookie": writeCookie(tokenCookieName, "token", 0) },
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const loader = () => {
 | 
				
			||||||
 | 
						return null;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function ForceLogoutPage() {
 | 
				
			||||||
 | 
						const { t } = useTranslation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return (
 | 
				
			||||||
 | 
							<>
 | 
				
			||||||
 | 
								<h4>{t("settings.general.log-out-everywhere")}</h4>
 | 
				
			||||||
 | 
								<p className="text-has-newline">{t("settings.general.log-out-everywhere-confirm")}</p>
 | 
				
			||||||
 | 
								<RemixForm method="POST">
 | 
				
			||||||
 | 
									<Form as="div">
 | 
				
			||||||
 | 
										<Button type="submit" variant="danger">
 | 
				
			||||||
 | 
											{t("yes")}
 | 
				
			||||||
 | 
										</Button>
 | 
				
			||||||
 | 
										{/* @ts-expect-error as=Link */}
 | 
				
			||||||
 | 
										<Button variant="link" as={Link} to="/settings">
 | 
				
			||||||
 | 
											{t("no")}
 | 
				
			||||||
 | 
										</Button>
 | 
				
			||||||
 | 
									</Form>
 | 
				
			||||||
 | 
								</RemixForm>
 | 
				
			||||||
 | 
							</>
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -30,30 +30,35 @@ export default function SettingsLayout() {
 | 
				
			||||||
	const { t } = useTranslation();
 | 
						const { t } = useTranslation();
 | 
				
			||||||
	const { pathname } = useLocation();
 | 
						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 (
 | 
						return (
 | 
				
			||||||
		<>
 | 
							<>
 | 
				
			||||||
			<Nav variant="pills" justify fill className="flex-column flex-md-row">
 | 
								<Nav variant="pills" justify fill className="flex-column flex-md-row">
 | 
				
			||||||
				<Nav.Link active={pathname === "/settings"} as={Link} to="/settings">
 | 
									<Nav.Link
 | 
				
			||||||
 | 
										active={isActive(["/settings", "/settings/force-log-out"])}
 | 
				
			||||||
 | 
										as={Link}
 | 
				
			||||||
 | 
										to="/settings"
 | 
				
			||||||
 | 
									>
 | 
				
			||||||
					{t("settings.nav.general-information")}
 | 
										{t("settings.nav.general-information")}
 | 
				
			||||||
				</Nav.Link>
 | 
									</Nav.Link>
 | 
				
			||||||
				<Nav.Link
 | 
									<Nav.Link active={isActive("/settings/profile", true)} as={Link} to="/settings/profile">
 | 
				
			||||||
					active={pathname.startsWith("/settings/profile")}
 | 
					 | 
				
			||||||
					as={Link}
 | 
					 | 
				
			||||||
					to="/settings/profile"
 | 
					 | 
				
			||||||
				>
 | 
					 | 
				
			||||||
					{t("settings.nav.profile")}
 | 
										{t("settings.nav.profile")}
 | 
				
			||||||
				</Nav.Link>
 | 
									</Nav.Link>
 | 
				
			||||||
				<Nav.Link
 | 
									<Nav.Link active={isActive("/settings/members", true)} as={Link} to="/settings/members">
 | 
				
			||||||
					active={pathname.startsWith("/settings/members")}
 | 
					 | 
				
			||||||
					as={Link}
 | 
					 | 
				
			||||||
					to="/settings/members"
 | 
					 | 
				
			||||||
				>
 | 
					 | 
				
			||||||
					{t("settings.nav.members")}
 | 
										{t("settings.nav.members")}
 | 
				
			||||||
				</Nav.Link>
 | 
									</Nav.Link>
 | 
				
			||||||
				<Nav.Link active={pathname.startsWith("/settings/auth")} as={Link} to="/settings/auth">
 | 
									<Nav.Link active={isActive("/settings/auth", true)} as={Link} to="/settings/auth">
 | 
				
			||||||
					{t("settings.nav.authentication")}
 | 
										{t("settings.nav.authentication")}
 | 
				
			||||||
				</Nav.Link>
 | 
									</Nav.Link>
 | 
				
			||||||
				<Nav.Link active={pathname === "/settings/export"} as={Link} to="/settings/export">
 | 
									<Nav.Link active={isActive("/settings/export")} as={Link} to="/settings/export">
 | 
				
			||||||
					{t("settings.nav.export")}
 | 
										{t("settings.nav.export")}
 | 
				
			||||||
				</Nav.Link>
 | 
									</Nav.Link>
 | 
				
			||||||
			</Nav>
 | 
								</Nav>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -105,7 +105,8 @@
 | 
				
			||||||
			"member-list-hidden": "Member list hidden?",
 | 
								"member-list-hidden": "Member list hidden?",
 | 
				
			||||||
			"custom-preferences": "Custom preferences",
 | 
								"custom-preferences": "Custom preferences",
 | 
				
			||||||
			"role": "Account role",
 | 
								"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": {
 | 
							"auth": {
 | 
				
			||||||
			"title": "Authentication",
 | 
								"title": "Authentication",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue