fix email change issues (#973)

* fix email change

* isolated email update

* update email UI immediately

---------

Co-authored-by: Christian Beutel <>
This commit is contained in:
slothful-vassal
2026-05-10 08:38:55 +02:00
committed by GitHub
parent d2ac49470a
commit edad99c7b4
8 changed files with 140 additions and 29 deletions

View File

@@ -9,7 +9,7 @@
interface Props {
email?: string;
onsave?: (email: string) => void;
onsave?: (email: string, currentPassword: string) => void;
}
let { email = "", onsave }: Props = $props();
@@ -18,24 +18,28 @@
export function openModal() {
setFields("email", email);
setFields("currentPassword", "");
setErrors("email", []);
setErrors("currentPassword", []);
modal.openModal();
}
const { form, errors, setFields, setErrors } = createForm<{
email: string;
currentPassword: string;
}>({
initialValues: { email: untrack(() => email) },
initialValues: { email: untrack(() => email), currentPassword: "" },
extend: validator({
schema: z.object({
email: z
.string()
.min(1, "required")
.email("not-a-valid-email-address"),
currentPassword: z.string().min(1, "required"),
}),
}),
onSubmit: async (form) => {
onsave?.(form.email);
onsave?.(form.email, form.currentPassword);
modal.closeModal!();
},
});
@@ -48,8 +52,9 @@
bind:this={modal}
>
{#snippet content()}
<form id="email-form" use:form>
<TextField name="email" error={$errors.email}></TextField>
<form id="email-form" use:form class="flex flex-col gap-4">
<TextField name="email" label={$_("email")} error={$errors.email}></TextField>
<TextField name="currentPassword" type="password" label={$_("current-password")} error={$errors.currentPassword}></TextField>
</form>
{/snippet}
{#snippet footer()}

View File

@@ -78,9 +78,10 @@ export async function logout() {
}
export async function users_update(user: User | { [K in keyof User]?: User[K] }, avatar?: File) {
const { email: _email, ...payload } = user as any;
let r = await fetch('/api/v1/user/' + user.id, {
method: 'POST',
body: JSON.stringify(user)
body: JSON.stringify(payload)
})
if (!r.ok) {
@@ -121,6 +122,22 @@ export async function users_update(user: User | { [K in keyof User]?: User[K] },
currentUser.set(merged);
}
export async function users_update_email(userId: string, email: string, currentPassword: string) {
const r = await fetch(`/api/v1/user/${userId}/email`, {
method: 'POST',
body: JSON.stringify({ email, currentPassword }),
});
if (!r.ok) {
const response = await r.json();
throw new APIError(r.status, response.message, response.detail);
}
const model: User = await r.json();
const existing = get(currentUser);
currentUser.set({ ...(existing ?? {}), ...model } as User);
}
export async function users_delete(user: User) {
const r = await fetch('/api/v1/user/' + user.id, {
method: 'DELETE',

View File

@@ -83,19 +83,23 @@ export async function POST(event: RequestEvent) {
const params = event.params
const safeParams = RecordIdSchema.parse(params);
const safeData = UserUpdateSchema.parse(data);
if (safeParams.id !== event.locals.pb.authStore.record!.id) {
return json({ message: 'Forbidden' }, { status: 403 });
}
if (safeData.email && safeData.email != event.locals.pb.authStore.record!.email) {
const r = await event.locals.pb.collection('users').requestEmailChange(safeData.email);
event.locals.pb.authStore.record!.email = safeData.email;
if (data.email !== undefined) {
return json({ message: 'Use POST /api/v1/user/{id}/email for email changes' }, { status: 400 });
}
const r = await event.locals.pb.collection('users').update<User>(safeParams.id, safeData)
const safeData = UserUpdateSchema.parse(data);
const { email: _email, ...updateData } = safeData;
const r = await event.locals.pb.collection('users').update<User>(safeParams.id, updateData)
if (safeData.password) {
const r = await event.locals.pb.collection('users').authWithPassword(safeData.email ?? safeData.username!, safeData.password);
return json(r.record)
} else {
return json(r);
const authR = await event.locals.pb.collection('users').authWithPassword(event.locals.pb.authStore.record!.email, safeData.password);
return json(authR.record)
}
return json(r);
} catch (e: any) {
return handleError(e);
}

View File

@@ -0,0 +1,33 @@
import { handleError } from '$lib/util/api_util';
import { json, type RequestEvent } from '@sveltejs/kit';
export async function POST(event: RequestEvent) {
try {
const { id } = event.params;
if (id !== event.locals.pb.authStore.record?.id) {
return json({ message: 'Forbidden' }, { status: 403 });
}
const { email, currentPassword } = await event.request.json();
if (!email) {
return json({ message: 'email is required' }, { status: 400 });
}
if (!currentPassword) {
return json({ message: 'currentPassword is required' }, { status: 400 });
}
const emailChange = await event.locals.pb.send('/user/email', {
method: 'POST',
body: JSON.stringify({ email, password: currentPassword }),
});
event.locals.pb.authStore.save(emailChange.token, emailChange.record);
emailChange.record.email = email;
return json(emailChange.record);
} catch (e: any) {
return handleError(e);
}
}

View File

@@ -18,6 +18,7 @@
logout,
users_delete,
users_update,
users_update_email,
} from "$lib/stores/user_store";
import { onMount } from "svelte";
import { _ } from "svelte-i18n";
@@ -52,9 +53,9 @@
goto("/");
}
async function updateEmail(email: string) {
async function updateEmail(email: string, currentPassword: string) {
try {
await users_update({ ...$currentUser!, email: email });
await users_update_email($currentUser!.id!, email, currentPassword);
show_toast({
text: $_("email-updated"),
icon: "check",