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
				
			
		|  | @ -183,7 +183,7 @@ public class EmailAuthController( | ||||||
|         return NoContent(); |         return NoContent(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     [HttpPost("add-email")] |     [HttpPost("add-account")] | ||||||
|     [Authorize("*")] |     [Authorize("*")] | ||||||
|     public async Task<IActionResult> AddEmailAddressAsync([FromBody] AddEmailAddressRequest req) |     public async Task<IActionResult> AddEmailAddressAsync([FromBody] AddEmailAddressRequest req) | ||||||
|     { |     { | ||||||
|  | @ -208,6 +208,9 @@ public class EmailAuthController( | ||||||
|         } |         } | ||||||
|         else |         else | ||||||
|         { |         { | ||||||
|  |             ValidationUtils.Validate( | ||||||
|  |                 [("password", ValidationUtils.ValidatePassword(req.Password))] | ||||||
|  |             ); | ||||||
|             await authService.SetUserPasswordAsync(CurrentUser!, req.Password); |             await authService.SetUserPasswordAsync(CurrentUser!, req.Password); | ||||||
|             await db.SaveChangesAsync(); |             await db.SaveChangesAsync(); | ||||||
|         } |         } | ||||||
|  | @ -232,7 +235,7 @@ public class EmailAuthController( | ||||||
|         return NoContent(); |         return NoContent(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     [HttpPost("add-email/callback")] |     [HttpPost("add-account/callback")] | ||||||
|     [Authorize("*")] |     [Authorize("*")] | ||||||
|     public async Task<IActionResult> AddEmailCallbackAsync([FromBody] EmailCallbackRequest req) |     public async Task<IActionResult> AddEmailCallbackAsync([FromBody] EmailCallbackRequest req) | ||||||
|     { |     { | ||||||
|  |  | ||||||
|  | @ -32,6 +32,7 @@ public class AccountCreationMailable(Config config, AccountCreationMailableView | ||||||
|     { |     { | ||||||
|         To(view.To) |         To(view.To) | ||||||
|             .From(config.EmailAuth.From!) |             .From(config.EmailAuth.From!) | ||||||
|  |             .Subject("Create an account") | ||||||
|             .View("~/Views/Mail/AccountCreation.cshtml", view) |             .View("~/Views/Mail/AccountCreation.cshtml", view) | ||||||
|             .Text(PlainText()); |             .Text(PlainText()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -32,6 +32,7 @@ public class AddEmailMailable(Config config, AddEmailMailableView view) | ||||||
|     { |     { | ||||||
|         To(view.To) |         To(view.To) | ||||||
|             .From(config.EmailAuth.From!) |             .From(config.EmailAuth.From!) | ||||||
|  |             .Subject("Confirm adding this email address to an existing account") | ||||||
|             .View("~/Views/Mail/AddEmail.cshtml", view) |             .View("~/Views/Mail/AddEmail.cshtml", view) | ||||||
|             .Text(PlainText()); |             .Text(PlainText()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ | ||||||
| <p> | <p> | ||||||
|     Please continue creating a new pronouns.cc account by using the following link: |     Please continue creating a new pronouns.cc account by using the following link: | ||||||
|     <br /> |     <br /> | ||||||
|     <a href="@Model.BaseUrl/auth/callback/email/@Model.Code">Confirm your email address</a> |     <a href="@Model.BaseUrl/auth/callback/email/@Model.Code">@Model.BaseUrl/auth/callback/email/@Model.Code</a> | ||||||
|     <br /> |     <br /> | ||||||
|     Note that this link will expire in one hour. |     Note that this link will expire in one hour. | ||||||
| </p> | </p> | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ | ||||||
| <p> | <p> | ||||||
|     Hello @@@Model.Username, please confirm adding this email address to your account by using the following link: |     Hello @@@Model.Username, please confirm adding this email address to your account by using the following link: | ||||||
|     <br /> |     <br /> | ||||||
|     <a href="@Model.BaseUrl/auth/callback/email/@Model.Code">Confirm your email address</a> |     <a href="@Model.BaseUrl/auth/callback/email/@Model.Code">@Model.BaseUrl/auth/callback/email/@Model.Code</a> | ||||||
|     <br /> |     <br /> | ||||||
|     Note that this link will expire in one hour. |     Note that this link will expire in one hour. | ||||||
| </p> | </p> | ||||||
|  |  | ||||||
|  | @ -8,7 +8,7 @@ | ||||||
| 	let name = $derived( | 	let name = $derived( | ||||||
| 		method.type === "EMAIL" ? method.remote_id : (method.remote_username ?? method.remote_id), | 		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> | </script> | ||||||
| 
 | 
 | ||||||
| <div class="list-group-item"> | <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"); | 				return $t("auth.successful-link-tumblr"); | ||||||
| 			case "FEDIVERSE": | 			case "FEDIVERSE": | ||||||
| 				return $t("auth.successful-link-fedi"); | 				return $t("auth.successful-link-fedi"); | ||||||
|  | 			case "EMAIL": | ||||||
|  | 				return $t("auth.successful-link-email"); | ||||||
| 			default: | 			default: | ||||||
| 				return "<you shouldn't see this!>"; | 				return "<you shouldn't see this!>"; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | @ -56,7 +56,12 @@ | ||||||
| 		"register-with-google": "Register with a Google account", | 		"register-with-google": "Register with a Google account", | ||||||
| 		"remote-google-account-label": "Your Google account", | 		"remote-google-account-label": "Your Google account", | ||||||
| 		"register-with-tumblr": "Register with a Tumblr 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": { | 	"error": { | ||||||
| 		"bad-request-header": "Something was wrong with your input", | 		"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 type { AuthUrls } from "$api/models/auth"; | ||||||
|  | import log from "$lib/log"; | ||||||
| 
 | 
 | ||||||
| export const load = async ({ fetch }) => { | export const load = async ({ fetch }) => { | ||||||
| 	const urls = await apiRequest<AuthUrls>("POST", "/auth/urls", { fetch, isInternal: true }); | 	const urls = await apiRequest<AuthUrls>("POST", "/auth/urls", { fetch, isInternal: true }); | ||||||
| 	return { urls }; | 	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"> | <script lang="ts"> | ||||||
| 	import AuthMethodList from "$components/settings/AuthMethodList.svelte"; | 	import AuthMethodList from "$components/settings/AuthMethodList.svelte"; | ||||||
| 	import AuthMethodRow from "$components/settings/AuthMethodRow.svelte"; | 	import EmailSettings from "$components/settings/EmailSettings.svelte"; | ||||||
| 	import type { PageData } from "./$types"; | 	import type { ActionData, PageData } from "./$types"; | ||||||
| 
 | 
 | ||||||
| 	type Props = { data: PageData }; | 	type Props = { data: PageData; form: ActionData }; | ||||||
| 	let { data }: Props = $props(); | 	let { data, form }: Props = $props(); | ||||||
| 
 | 
 | ||||||
| 	let max = $derived(data.meta.limits.max_auth_methods); | 	let max = $derived(data.meta.limits.max_auth_methods); | ||||||
| 	let canRemove = $derived(data.user.auth_methods.length > 1); | 	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 discordAccounts = $derived(data.user.auth_methods.filter((m) => m.type === "DISCORD")); | ||||||
| 	let googleAccounts = $derived(data.user.auth_methods.filter((m) => m.type === "GOOGLE")); | 	let googleAccounts = $derived(data.user.auth_methods.filter((m) => m.type === "GOOGLE")); | ||||||
| 	let tumblrAccounts = $derived(data.user.auth_methods.filter((m) => m.type === "TUMBLR")); | 	let tumblrAccounts = $derived(data.user.auth_methods.filter((m) => m.type === "TUMBLR")); | ||||||
|  | @ -16,14 +15,7 @@ | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| {#if data.urls.email_enabled} | {#if data.urls.email_enabled} | ||||||
| 	<h3>Email addresses</h3> | 	<EmailSettings user={data.user} {canRemove} {max} {form} /> | ||||||
| 	<AuthMethodList |  | ||||||
| 		methods={emails} |  | ||||||
| 		{canRemove} |  | ||||||
| 		{max} |  | ||||||
| 		buttonLink="/settings/auth/add-email" |  | ||||||
| 		buttonText="Add email address" |  | ||||||
| 	/> |  | ||||||
| {/if} | {/if} | ||||||
| {#if data.urls.discord} | {#if data.urls.discord} | ||||||
| 	<h3>Discord accounts</h3> | 	<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