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…
Reference in a new issue