feat: add email to existing account, change password
This commit is contained in:
		
							parent
							
								
									77c3047b1e
								
							
						
					
					
						commit
						1cf2619393
					
				
					 13 changed files with 227 additions and 20 deletions
				
			
		|  | @ -8,7 +8,7 @@ | |||
| 	let name = $derived( | ||||
| 		method.type === "EMAIL" ? method.remote_id : (method.remote_username ?? method.remote_id), | ||||
| 	); | ||||
| 	let showId = $derived(method.type !== "FEDIVERSE"); | ||||
| 	let showId = $derived(method.type !== "EMAIL"); | ||||
| </script> | ||||
| 
 | ||||
| <div class="list-group-item"> | ||||
|  |  | |||
|  | @ -0,0 +1,73 @@ | |||
| <script lang="ts"> | ||||
| 	import type { RawApiError } from "$api/error"; | ||||
| 	import type { MeUser } from "$api/models"; | ||||
| 	import FormStatusMarker from "$components/editor/FormStatusMarker.svelte"; | ||||
| 	import { t } from "$lib/i18n"; | ||||
| 	import AuthMethodRow from "./AuthMethodRow.svelte"; | ||||
| 	import EnvelopePlusFill from "svelte-bootstrap-icons/lib/EnvelopePlusFill.svelte"; | ||||
| 
 | ||||
| 	type Props = { | ||||
| 		user: MeUser; | ||||
| 		canRemove: boolean; | ||||
| 		max: number; | ||||
| 		form: { error: RawApiError | null; ok: boolean } | null; | ||||
| 	}; | ||||
| 	let { user, canRemove, max, form }: Props = $props(); | ||||
| 
 | ||||
| 	let emails = $derived(user.auth_methods.filter((a) => a.type === "EMAIL")); | ||||
| </script> | ||||
| 
 | ||||
| <h3>{$t("auth.email-password-title")}</h3> | ||||
| 
 | ||||
| {#if emails.length > 0} | ||||
| 	<div class="row"> | ||||
| 		<div class="col-md"> | ||||
| 			<h4>Your email addresses</h4> | ||||
| 			<div class="list-group"> | ||||
| 				{#each emails as method (method.id)} | ||||
| 					<AuthMethodRow {method} {canRemove} /> | ||||
| 				{/each} | ||||
| 				{#if emails.length < max} | ||||
| 					<a class="list-group-item" href="/settings/auth/add-email"> | ||||
| 						<EnvelopePlusFill /> <strong>{$t("auth.add-email-address")}</strong> | ||||
| 					</a> | ||||
| 				{/if} | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="col-md"> | ||||
| 			<FormStatusMarker {form} /> | ||||
| 			<h4>Change password</h4> | ||||
| 			<form method="POST" action="?/password"> | ||||
| 				<div class="mb-1"> | ||||
| 					<label for="current" class="form-label">Current password</label> | ||||
| 					<input type="password" id="current" name="current" class="form-control" required /> | ||||
| 				</div> | ||||
| 				<div class="mb-1"> | ||||
| 					<label for="password" class="form-label">New password</label> | ||||
| 					<input type="password" id="password" name="password" class="form-control" required /> | ||||
| 				</div> | ||||
| 				<div class="mb-1"> | ||||
| 					<label for="confirm-password" class="form-label">Confirm new password</label> | ||||
| 					<input | ||||
| 						type="password" | ||||
| 						id="confirm-password" | ||||
| 						name="confirm-password" | ||||
| 						class="form-control" | ||||
| 						required | ||||
| 					/> | ||||
| 				</div> | ||||
| 				<div> | ||||
| 					<button type="submit" class="btn btn-secondary mt-2">Change password</button> | ||||
| 				</div> | ||||
| 			</form> | ||||
| 		</div> | ||||
| 	</div> | ||||
| {:else} | ||||
| 	<p>{$t("auth.no-email-addresses")}</p> | ||||
| 	<p> | ||||
| 		<a class="btn btn-outline-secondary" href="/settings/auth/add-email"> | ||||
| 			<EnvelopePlusFill /> | ||||
| 			{$t("auth.add-email-address")} | ||||
| 		</a> | ||||
| 	</p> | ||||
| {/if} | ||||
|  | @ -19,6 +19,8 @@ | |||
| 				return $t("auth.successful-link-tumblr"); | ||||
| 			case "FEDIVERSE": | ||||
| 				return $t("auth.successful-link-fedi"); | ||||
| 			case "EMAIL": | ||||
| 				return $t("auth.successful-link-email"); | ||||
| 			default: | ||||
| 				return "<you shouldn't see this!>"; | ||||
| 		} | ||||
|  |  | |||
|  | @ -56,7 +56,12 @@ | |||
| 		"register-with-google": "Register with a Google account", | ||||
| 		"remote-google-account-label": "Your Google account", | ||||
| 		"register-with-tumblr": "Register with a Tumblr account", | ||||
| 		"remote-tumblr-account-label": "Your Tumblr account" | ||||
| 		"remote-tumblr-account-label": "Your Tumblr account", | ||||
| 		"email-password-title": "Email and password", | ||||
| 		"add-email-address": "Add email address", | ||||
| 		"no-email-addresses": "You haven't linked any email addresses yet.", | ||||
| 		"check-inbox-for-link-hint": "Check your inbox for a link!", | ||||
| 		"successful-link-email": "Your account has successfully been linked to the following email address:" | ||||
| 	}, | ||||
| 	"error": { | ||||
| 		"bad-request-header": "Something was wrong with your input", | ||||
|  |  | |||
|  | @ -1,7 +1,44 @@ | |||
| import { apiRequest } from "$api"; | ||||
| import { apiRequest, fastRequest } from "$api"; | ||||
| import ApiError, { ErrorCode, type RawApiError } from "$api/error.js"; | ||||
| import type { AuthUrls } from "$api/models/auth"; | ||||
| import log from "$lib/log"; | ||||
| 
 | ||||
| export const load = async ({ fetch }) => { | ||||
| 	const urls = await apiRequest<AuthUrls>("POST", "/auth/urls", { fetch, isInternal: true }); | ||||
| 	return { urls }; | ||||
| }; | ||||
| 
 | ||||
| export const actions = { | ||||
| 	password: async ({ request, fetch, cookies }) => { | ||||
| 		const body = await request.formData(); | ||||
| 		const current = body.get("current") as string | null; | ||||
| 		const password = body.get("password") as string | null; | ||||
| 		const password2 = body.get("confirm-password") as string | null; | ||||
| 
 | ||||
| 		if (password !== password2) { | ||||
| 			return { | ||||
| 				ok: false, | ||||
| 				error: { | ||||
| 					status: 400, | ||||
| 					code: ErrorCode.BadRequest, | ||||
| 					message: "Passwords do not match", | ||||
| 				} as RawApiError, | ||||
| 			}; | ||||
| 		} | ||||
| 
 | ||||
| 		try { | ||||
| 			await fastRequest("POST", "/auth/email/change-password", { | ||||
| 				body: { current, new: password }, | ||||
| 				isInternal: true, | ||||
| 				fetch, | ||||
| 				cookies, | ||||
| 			}); | ||||
| 
 | ||||
| 			return { ok: true, error: null }; | ||||
| 		} catch (e) { | ||||
| 			if (e instanceof ApiError) return { ok: false, error: e.obj }; | ||||
| 			log.error("error changing password:", e); | ||||
| 			throw e; | ||||
| 		} | ||||
| 	}, | ||||
| }; | ||||
|  |  | |||
|  | @ -1,14 +1,13 @@ | |||
| <script lang="ts"> | ||||
| 	import AuthMethodList from "$components/settings/AuthMethodList.svelte"; | ||||
| 	import AuthMethodRow from "$components/settings/AuthMethodRow.svelte"; | ||||
| 	import type { PageData } from "./$types"; | ||||
| 	import EmailSettings from "$components/settings/EmailSettings.svelte"; | ||||
| 	import type { ActionData, PageData } from "./$types"; | ||||
| 
 | ||||
| 	type Props = { data: PageData }; | ||||
| 	let { data }: Props = $props(); | ||||
| 	type Props = { data: PageData; form: ActionData }; | ||||
| 	let { data, form }: Props = $props(); | ||||
| 
 | ||||
| 	let max = $derived(data.meta.limits.max_auth_methods); | ||||
| 	let canRemove = $derived(data.user.auth_methods.length > 1); | ||||
| 	let emails = $derived(data.user.auth_methods.filter((m) => m.type === "EMAIL")); | ||||
| 	let discordAccounts = $derived(data.user.auth_methods.filter((m) => m.type === "DISCORD")); | ||||
| 	let googleAccounts = $derived(data.user.auth_methods.filter((m) => m.type === "GOOGLE")); | ||||
| 	let tumblrAccounts = $derived(data.user.auth_methods.filter((m) => m.type === "TUMBLR")); | ||||
|  | @ -16,14 +15,7 @@ | |||
| </script> | ||||
| 
 | ||||
| {#if data.urls.email_enabled} | ||||
| 	<h3>Email addresses</h3> | ||||
| 	<AuthMethodList | ||||
| 		methods={emails} | ||||
| 		{canRemove} | ||||
| 		{max} | ||||
| 		buttonLink="/settings/auth/add-email" | ||||
| 		buttonText="Add email address" | ||||
| 	/> | ||||
| 	<EmailSettings user={data.user} {canRemove} {max} {form} /> | ||||
| {/if} | ||||
| {#if data.urls.discord} | ||||
| 	<h3>Discord accounts</h3> | ||||
|  |  | |||
|  | @ -0,0 +1,44 @@ | |||
| import { fastRequest } from "$api"; | ||||
| import ApiError, { ErrorCode, type RawApiError } from "$api/error.js"; | ||||
| import log from "$lib/log.js"; | ||||
| import { redirect } from "@sveltejs/kit"; | ||||
| 
 | ||||
| export const load = async ({ parent }) => { | ||||
| 	const { user } = await parent(); | ||||
| 	return { firstEmail: user.auth_methods.filter((a) => a.type === "EMAIL").length === 0 }; | ||||
| }; | ||||
| 
 | ||||
| export const actions = { | ||||
| 	add: async ({ request, fetch, cookies }) => { | ||||
| 		const body = await request.formData(); | ||||
| 		const email = body.get("email") as string; | ||||
| 		const password = body.get("password") as string | null; | ||||
| 		const password2 = body.get("confirm-password") as string | null; | ||||
| 
 | ||||
| 		if (password2 && password !== password2) { | ||||
| 			return { | ||||
| 				ok: false, | ||||
| 				error: { | ||||
| 					status: 400, | ||||
| 					code: ErrorCode.BadRequest, | ||||
| 					message: "Passwords do not match", | ||||
| 				} as RawApiError, | ||||
| 			}; | ||||
| 		} | ||||
| 
 | ||||
| 		try { | ||||
| 			await fastRequest("POST", "/auth/email/add-account", { | ||||
| 				body: { email, password }, | ||||
| 				isInternal: true, | ||||
| 				fetch, | ||||
| 				cookies, | ||||
| 			}); | ||||
| 
 | ||||
| 			return { ok: true, error: null }; | ||||
| 		} catch (e) { | ||||
| 			if (e instanceof ApiError) return { ok: false, error: e.obj }; | ||||
| 			log.error("error adding email address to account:", e); | ||||
| 			throw e; | ||||
| 		} | ||||
| 	}, | ||||
| }; | ||||
|  | @ -0,0 +1,49 @@ | |||
| <script lang="ts"> | ||||
| 	import { t } from "$lib/i18n"; | ||||
| 	import { Button } from "@sveltestrap/sveltestrap"; | ||||
| 	import type { ActionData, PageData } from "./$types"; | ||||
| 	import FormStatusMarker from "$components/editor/FormStatusMarker.svelte"; | ||||
| 
 | ||||
| 	type Props = { data: PageData; form: ActionData }; | ||||
| 	let { data, form }: Props = $props(); | ||||
| </script> | ||||
| 
 | ||||
| <div class="mx-auto w-lg-75"> | ||||
| 	<h3>Link a new email address</h3> | ||||
| 
 | ||||
| 	<FormStatusMarker {form} successMessage={$t("auth.check-inbox-for-link-hint")} /> | ||||
| 
 | ||||
| 	<form method="POST" action="?/add"> | ||||
| 		<div class="mb-1"> | ||||
| 			<label for="email" class="form-label">{$t("auth.log-in-form-email-label")}</label> | ||||
| 			<input | ||||
| 				type="email" | ||||
| 				id="email" | ||||
| 				name="email" | ||||
| 				placeholder="me@example.com" | ||||
| 				class="form-control" | ||||
| 				required | ||||
| 			/> | ||||
| 		</div> | ||||
| 		<div class="mb-1"> | ||||
| 			<label for="password" class="form-label">{$t("auth.log-in-form-password-label")}</label> | ||||
| 			<input type="password" id="password" name="password" class="form-control" required /> | ||||
| 		</div> | ||||
| 		{#if data.firstEmail} | ||||
| 			<div class="mb-1"> | ||||
| 				<label for="confirm-password" class="form-label"> | ||||
| 					{$t("auth.confirm-password-label")} | ||||
| 				</label> | ||||
| 				<input | ||||
| 					type="password" | ||||
| 					id="confirm-password" | ||||
| 					name="confirm-password" | ||||
| 					class="form-control" | ||||
| 					required | ||||
| 				/> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 
 | ||||
| 		<button type="submit" class="btn btn-secondary mt-2">{$t("auth.add-email-address")}</button> | ||||
| 	</form> | ||||
| </div> | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue