Compare commits
	
		
			2 commits
		
	
	
		
			e030342358
			...
			567e794154
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 567e794154 | |||
| 40da4865bc | 
					 10 changed files with 113 additions and 62 deletions
				
			
		| 
						 | 
					@ -39,10 +39,10 @@ public class AuthController(
 | 
				
			||||||
                + $"&prompt=none&state={state}"
 | 
					                + $"&prompt=none&state={state}"
 | 
				
			||||||
                + $"&redirect_uri={HttpUtility.UrlEncode($"{config.BaseUrl}/auth/callback/discord")}";
 | 
					                + $"&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(
 | 
					    public record AuthResponse(
 | 
				
			||||||
        UserRendererService.UserResponse User,
 | 
					        UserRendererService.UserResponse User,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -100,6 +100,8 @@ public class EmailAuthController(
 | 
				
			||||||
        [FromBody] CompleteRegistrationRequest req
 | 
					        [FromBody] CompleteRegistrationRequest req
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					        CheckRequirements();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var email = await keyCacheService.GetKeyAsync($"email:{req.Ticket}");
 | 
					        var email = await keyCacheService.GetKeyAsync($"email:{req.Ticket}");
 | 
				
			||||||
        if (email == null)
 | 
					        if (email == null)
 | 
				
			||||||
            throw new ApiError.BadRequest("Unknown ticket", "ticket", req.Ticket);
 | 
					            throw new ApiError.BadRequest("Unknown ticket", "ticket", req.Ticket);
 | 
				
			||||||
| 
						 | 
					@ -185,6 +187,8 @@ public class EmailAuthController(
 | 
				
			||||||
    [Authorize("*")]
 | 
					    [Authorize("*")]
 | 
				
			||||||
    public async Task<IActionResult> AddEmailAddressAsync([FromBody] AddEmailAddressRequest req)
 | 
					    public async Task<IActionResult> AddEmailAddressAsync([FromBody] AddEmailAddressRequest req)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					        CheckRequirements();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var emails = await db
 | 
					        var emails = await db
 | 
				
			||||||
            .AuthMethods.Where(m => m.UserId == CurrentUser!.Id && m.AuthType == AuthType.Email)
 | 
					            .AuthMethods.Where(m => m.UserId == CurrentUser!.Id && m.AuthType == AuthType.Email)
 | 
				
			||||||
            .ToListAsync();
 | 
					            .ToListAsync();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,6 +16,7 @@ export type CallbackResponse = {
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type AuthUrls = {
 | 
					export type AuthUrls = {
 | 
				
			||||||
 | 
						email_enabled: boolean;
 | 
				
			||||||
	discord?: string;
 | 
						discord?: string;
 | 
				
			||||||
	google?: string;
 | 
						google?: string;
 | 
				
			||||||
	tumblr?: string;
 | 
						tumblr?: string;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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, {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,7 +11,7 @@ import {
 | 
				
			||||||
	useActionData,
 | 
						useActionData,
 | 
				
			||||||
	useLoaderData,
 | 
						useLoaderData,
 | 
				
			||||||
} from "@remix-run/react";
 | 
					} 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 { useTranslation } from "react-i18next";
 | 
				
			||||||
import i18n from "~/i18next.server";
 | 
					import i18n from "~/i18next.server";
 | 
				
			||||||
import serverRequest, { getToken, writeCookie } from "~/lib/request.server";
 | 
					import serverRequest, { getToken, writeCookie } from "~/lib/request.server";
 | 
				
			||||||
| 
						 | 
					@ -78,33 +78,36 @@ export default function LoginPage() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return (
 | 
						return (
 | 
				
			||||||
		<>
 | 
							<>
 | 
				
			||||||
			<Row>
 | 
								<div className="row">
 | 
				
			||||||
				<Col md className="mb-4">
 | 
									{!urls.email_enabled && <div className="col-lg-3"></div>}
 | 
				
			||||||
					<h2>{t("log-in.form-title")}</h2>
 | 
									{urls.email_enabled && (
 | 
				
			||||||
					{actionData?.error && <LoginError error={actionData.error} />}
 | 
										<div className="col col-md mb-4">
 | 
				
			||||||
					<RemixForm action="/auth/log-in" method="POST">
 | 
											<h2>{t("log-in.form-title")}</h2>
 | 
				
			||||||
						<Form as="div">
 | 
											{actionData?.error && <LoginError error={actionData.error} />}
 | 
				
			||||||
							<Form.Group className="mb-3" controlId="email">
 | 
											<RemixForm action="/auth/log-in" method="POST">
 | 
				
			||||||
								<Form.Label>{t("log-in.email")}</Form.Label>
 | 
												<Form as="div">
 | 
				
			||||||
								<Form.Control name="email" type="email" required />
 | 
													<Form.Group className="mb-3" controlId="email">
 | 
				
			||||||
							</Form.Group>
 | 
														<Form.Label>{t("log-in.email")}</Form.Label>
 | 
				
			||||||
							<Form.Group className="mb-3" controlId="password">
 | 
														<Form.Control name="email" type="email" required />
 | 
				
			||||||
								<Form.Label>{t("log-in.password")}</Form.Label>
 | 
													</Form.Group>
 | 
				
			||||||
								<Form.Control name="password" type="password" required />
 | 
													<Form.Group className="mb-3" controlId="password">
 | 
				
			||||||
							</Form.Group>
 | 
														<Form.Label>{t("log-in.password")}</Form.Label>
 | 
				
			||||||
 | 
														<Form.Control name="password" type="password" required />
 | 
				
			||||||
 | 
													</Form.Group>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							<ButtonGroup>
 | 
													<ButtonGroup>
 | 
				
			||||||
								<Button variant="primary" type="submit">
 | 
														<Button variant="primary" type="submit">
 | 
				
			||||||
									{t("log-in.log-in-button")}
 | 
															{t("log-in.log-in-button")}
 | 
				
			||||||
								</Button>
 | 
														</Button>
 | 
				
			||||||
								<Button as="a" href="/auth/register" variant="secondary">
 | 
														<Button as="a" href="/auth/register" variant="secondary">
 | 
				
			||||||
									{t("log-in.register-with-email")}
 | 
															{t("log-in.register-with-email")}
 | 
				
			||||||
								</Button>
 | 
														</Button>
 | 
				
			||||||
							</ButtonGroup>
 | 
													</ButtonGroup>
 | 
				
			||||||
						</Form>
 | 
												</Form>
 | 
				
			||||||
					</RemixForm>
 | 
											</RemixForm>
 | 
				
			||||||
				</Col>
 | 
										</div>
 | 
				
			||||||
				<Col md>
 | 
									)}
 | 
				
			||||||
 | 
									<div className="col col-md">
 | 
				
			||||||
					<h2>{t("log-in.3rd-party.title")}</h2>
 | 
										<h2>{t("log-in.3rd-party.title")}</h2>
 | 
				
			||||||
					<p>{t("log-in.3rd-party.desc")}</p>
 | 
										<p>{t("log-in.3rd-party.desc")}</p>
 | 
				
			||||||
					<ListGroup>
 | 
										<ListGroup>
 | 
				
			||||||
| 
						 | 
					@ -124,8 +127,9 @@ export default function LoginPage() {
 | 
				
			||||||
							</ListGroup.Item>
 | 
												</ListGroup.Item>
 | 
				
			||||||
						)}
 | 
											)}
 | 
				
			||||||
					</ListGroup>
 | 
										</ListGroup>
 | 
				
			||||||
				</Col>
 | 
									</div>
 | 
				
			||||||
			</Row>
 | 
									{!urls.email_enabled && <div className="col-lg-3"></div>}
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
		</>
 | 
							</>
 | 
				
			||||||
	);
 | 
						);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,10 +1,12 @@
 | 
				
			||||||
import i18n from "~/i18next.server";
 | 
					import i18n from "~/i18next.server";
 | 
				
			||||||
import { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
 | 
					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 { Button, ListGroup } from "react-bootstrap";
 | 
				
			||||||
import { loader as settingsLoader } from "~/routes/settings/route";
 | 
					import { loader as settingsLoader } from "~/routes/settings/route";
 | 
				
			||||||
import { useTranslation } from "react-i18next";
 | 
					import { useTranslation } from "react-i18next";
 | 
				
			||||||
import { AuthMethod, MeUser } from "~/lib/api/user";
 | 
					import { AuthMethod, MeUser } from "~/lib/api/user";
 | 
				
			||||||
 | 
					import serverRequest from "~/lib/request.server";
 | 
				
			||||||
 | 
					import { AuthUrls } from "~/lib/api/auth";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const meta: MetaFunction<typeof loader> = ({ data }) => {
 | 
					export const meta: MetaFunction<typeof loader> = ({ data }) => {
 | 
				
			||||||
	return [{ title: `${data?.meta.title || "Authentication"} • pronouns.cc` }];
 | 
						return [{ title: `${data?.meta.title || "Authentication"} • pronouns.cc` }];
 | 
				
			||||||
| 
						 | 
					@ -12,17 +14,16 @@ export const meta: MetaFunction<typeof loader> = ({ data }) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
 | 
					export const loader = async ({ request }: LoaderFunctionArgs) => {
 | 
				
			||||||
	const t = await i18n.getFixedT(request);
 | 
						const t = await i18n.getFixedT(request);
 | 
				
			||||||
	return { meta: { title: t("settings.auth.title") } };
 | 
						const urls = await serverRequest<AuthUrls>("POST", "/auth/urls", { isInternal: true });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return { urls, meta: { title: t("settings.auth.title") } };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function AuthSettings() {
 | 
					export default function AuthSettings() {
 | 
				
			||||||
 | 
						const { urls } = useLoaderData<typeof loader>();
 | 
				
			||||||
	const { user } = useRouteLoaderData<typeof settingsLoader>("routes/settings")!;
 | 
						const { user } = useRouteLoaderData<typeof settingsLoader>("routes/settings")!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return (
 | 
						return <div className="px-md-5">{urls.email_enabled && <EmailSettings user={user} />}</div>;
 | 
				
			||||||
		<div className="px-md-5">
 | 
					 | 
				
			||||||
			<EmailSettings user={user} />
 | 
					 | 
				
			||||||
		</div>
 | 
					 | 
				
			||||||
	);
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function EmailSettings({ user }: { user: MeUser }) {
 | 
					function EmailSettings({ user }: { user: MeUser }) {
 | 
				
			||||||
| 
						 | 
					@ -44,7 +45,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