feat(frontend): remove auth method

This commit is contained in:
sam 2025-02-11 14:21:40 +01:00
parent 373d97e70a
commit c47fc41437
Signed by: sam
GPG key ID: B4EF20DDE721CAA1
8 changed files with 121 additions and 6 deletions

View file

@ -0,0 +1,14 @@
<script lang="ts">
import { t } from "$lib/i18n";
type Props = { data?: { alertKey?: string }; key?: string };
let props: Props = $props();
let key = $derived(props.key ?? props.data?.alertKey);
</script>
{#if key}
<div class="alert alert-light">
{$t(key)}
</div>
{/if}

View file

@ -77,7 +77,16 @@
"log-in-sign-up-link": "Sign up with email",
"forgot-password-title": "Forgot password",
"reset-password-title": "Reset password",
"password-changed-hint": "Your password has been changed!"
"password-changed-hint": "Your password has been changed!",
"link-email-header": "Link a new email address",
"unlink-email-header": "Unlink email address",
"unlink-fediverse-header": "Unlink fediverse account",
"unlink-tumblr-header": "Unlink Tumblr account",
"unlink-google-header": "Unlink Google account",
"unlink-discord-header": "Unlink Discord account",
"unlink-confirmation-1": "Are you sure you want to unlink {{username}} from your account?",
"unlink-confirmation-2": "You will no longer be able to use this account to log in. Please make sure at least one of your other linked accounts is accessible before continuing.",
"unlink-button": "Unlink account"
},
"error": {
"bad-request-header": "Something was wrong with your input",
@ -310,5 +319,8 @@
"form": {
"optional": "(optional)",
"required": "Required"
},
"alert": {
"auth-method-remove-success": "Successfully unlinked account!"
}
}

View file

@ -9,9 +9,10 @@ export const setToken = (cookies: Cookies, token: string) =>
cookies.set(TOKEN_COOKIE_NAME, token, { path: "/" });
export const clearToken = (cookies: Cookies) => cookies.delete(TOKEN_COOKIE_NAME, { path: "/" });
// TODO: change this to something we actually clearly have the rights to use
export const DEFAULT_AVATAR = "https://pronouns.cc/default/512.webp";
export const DEFAULT_FLAG = "/unknown_flag.svg";
export const idTimestamp = (id: string) =>
DateTime.fromMillis(parseInt(id, 10) / (1 << 22) + 1_640_995_200_000);
export const alertKey = (url: URL): string | undefined =>
url.searchParams.has("alert") ? "alert." + url.searchParams.get("alert") : undefined;

View file

@ -1,11 +1,12 @@
import { apiRequest, fastRequest } from "$api";
import ApiError, { ErrorCode, type RawApiError } from "$api/error.js";
import type { AuthUrls } from "$api/models/auth";
import { alertKey } from "$lib";
import log from "$lib/log";
export const load = async ({ fetch }) => {
export const load = async ({ fetch, url }) => {
const urls = await apiRequest<AuthUrls>("POST", "/auth/urls", { fetch, isInternal: true });
return { urls };
return { urls, alertKey: alertKey(url) };
};
export const actions = {

View file

@ -1,6 +1,7 @@
<script lang="ts">
import AuthMethodList from "$components/settings/AuthMethodList.svelte";
import EmailSettings from "$components/settings/EmailSettings.svelte";
import UrlAlert from "$components/URLAlert.svelte";
import type { ActionData, PageData } from "./$types";
type Props = { data: PageData; form: ActionData };
@ -14,6 +15,8 @@
let fediAccounts = $derived(data.user.auth_methods.filter((m) => m.type === "FEDIVERSE"));
</script>
<UrlAlert {data} />
{#if data.urls.email_enabled}
<EmailSettings user={data.user} {canRemove} {max} {form} />
{/if}

View file

@ -8,7 +8,7 @@
</script>
<div class="mx-auto w-lg-75">
<h3>Link a new email address</h3>
<h3>{$t("auth.link-email-header")}</h3>
<FormStatusMarker {form} successMessage={$t("auth.check-inbox-for-link-hint")} />

View file

@ -0,0 +1,40 @@
import { fastRequest } from "$api";
import ApiError, { ErrorCode } from "$api/error.js";
import log from "$lib/log.js";
import { error, isRedirect, redirect } from "@sveltejs/kit";
export const load = async ({ parent, params }) => {
const data = await parent();
if (data.user.auth_methods.length < 2) {
error(403, {
message: "You cannot remove your last authentication method.",
status: 403,
code: ErrorCode.LastAuthMethod,
});
}
const authMethod = data.meUser!.auth_methods.find((m) => m.id === params.id);
if (!authMethod) {
error(404, {
message: "No authentication method with that ID found.",
status: 404,
code: ErrorCode.GenericApiError,
});
}
return { authMethod };
};
export const actions = {
default: async ({ params, fetch, cookies }) => {
try {
fastRequest("DELETE", "/auth/methods/" + params.id, { fetch, cookies, isInternal: true });
redirect(303, "/settings/auth?alert=auth-method-remove-success");
} catch (e) {
if (isRedirect(e)) throw e;
if (e instanceof ApiError) return { error: e.obj };
log.error("Could not remove auth method %s:", params.id, e);
throw e;
}
},
};

View file

@ -0,0 +1,44 @@
<script lang="ts">
import type { AuthType } from "$api/models";
import ErrorAlert from "$components/ErrorAlert.svelte";
import { t } from "$lib/i18n";
import type { ActionData, PageData } from "./$types";
type Props = { data: PageData; form: ActionData };
let { data, form }: Props = $props();
const unlinkHeader = (type: AuthType): string => {
switch (type) {
case "DISCORD":
return $t("auth.unlink-discord-header");
case "GOOGLE":
return $t("auth.unlink-google-header");
case "TUMBLR":
return $t("auth.unlink-tumblr-header");
case "FEDIVERSE":
return $t("auth.unlink-fediverse-header");
case "EMAIL":
return $t("auth.unlink-email-header");
}
};
</script>
<svelte:head>
<title>{unlinkHeader(data.authMethod.type)} • pronouns.cc</title>
</svelte:head>
<div class="mx-auto w-lg-75">
<h3>{unlinkHeader(data.authMethod.type)}</h3>
{#if form?.error}
<ErrorAlert error={form.error} />
{/if}
<p>
{$t("auth.unlink-confirmation-1", {
username: data.authMethod.remote_username || data.authMethod.remote_id,
})}
<strong>{$t("auth.unlink-confirmation-2")}</strong>
</p>
<form method="POST">
<button type="submit" class="btn btn-danger">{$t("auth.unlink-button")}</button>
</form>
</div>