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:
@@ -87,17 +87,6 @@ func UpdateUserHandler(client meilisearch.ServiceManager) func(e *core.RecordEve
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ChangeUserEmailHandler() func(e *core.RecordRequestEmailChangeRequestEvent) error {
|
|
||||||
return func(e *core.RecordRequestEmailChangeRequestEvent) error {
|
|
||||||
|
|
||||||
e.Record.Set("email", e.NewEmail)
|
|
||||||
if err := e.App.Save(e.Record); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func createDefaultUserSettings(app core.App, userId string) error {
|
func createDefaultUserSettings(app core.App, userId string) error {
|
||||||
collection, err := app.FindCollectionByNameOrId("settings")
|
collection, err := app.FindCollectionByNameOrId("settings")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -88,7 +88,6 @@ func registerMigrations(app *pocketbase.PocketBase) {
|
|||||||
func setupEventHandlers(app *pocketbase.PocketBase, client meilisearch.ServiceManager) {
|
func setupEventHandlers(app *pocketbase.PocketBase, client meilisearch.ServiceManager) {
|
||||||
app.OnRecordAfterCreateSuccess("users").BindFunc(hooks.CreateUserHandler(client))
|
app.OnRecordAfterCreateSuccess("users").BindFunc(hooks.CreateUserHandler(client))
|
||||||
app.OnRecordAfterUpdateSuccess("users").BindFunc(hooks.UpdateUserHandler(client))
|
app.OnRecordAfterUpdateSuccess("users").BindFunc(hooks.UpdateUserHandler(client))
|
||||||
app.OnRecordRequestEmailChangeRequest("users").BindFunc(hooks.ChangeUserEmailHandler())
|
|
||||||
|
|
||||||
app.OnRecordAfterCreateSuccess("trails").BindFunc(hooks.CreateTrailHandler(client))
|
app.OnRecordAfterCreateSuccess("trails").BindFunc(hooks.CreateTrailHandler(client))
|
||||||
app.OnRecordAfterUpdateSuccess("trails").BindFunc(hooks.UpdateTrailHandler(client))
|
app.OnRecordAfterUpdateSuccess("trails").BindFunc(hooks.UpdateTrailHandler(client))
|
||||||
@@ -155,6 +154,7 @@ func registerRoutes(se *core.ServeEvent, client meilisearch.ServiceManager) {
|
|||||||
se.Router.GET("/health", routes.Health)
|
se.Router.GET("/health", routes.Health)
|
||||||
|
|
||||||
se.Router.POST("/auth/token", routes.AuthToken)
|
se.Router.POST("/auth/token", routes.AuthToken)
|
||||||
|
se.Router.POST("/user/email", routes.UserEmailChange)
|
||||||
se.Router.POST("/waypoint/cluster", routes.WaypointCluster)
|
se.Router.POST("/waypoint/cluster", routes.WaypointCluster)
|
||||||
|
|
||||||
se.Router.POST("/trail-merge/suggest", routes.TrailMergeSuggest)
|
se.Router.POST("/trail-merge/suggest", routes.TrailMergeSuggest)
|
||||||
|
|||||||
62
db/routes/user_email_change.go
Normal file
62
db/routes/user_email_change.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||||
|
"github.com/pocketbase/pocketbase/apis"
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
"github.com/pocketbase/pocketbase/mails"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/routine"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UserEmailChange(e *core.RequestEvent) error {
|
||||||
|
if e.Auth == nil {
|
||||||
|
return apis.NewUnauthorizedError("Authentication required", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var data struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
if err := e.BindBody(&data); err != nil {
|
||||||
|
return apis.NewBadRequestError("Failed to read request data", err)
|
||||||
|
}
|
||||||
|
if data.Email == "" {
|
||||||
|
return apis.NewBadRequestError("Email is required", nil)
|
||||||
|
}
|
||||||
|
if data.Password == "" {
|
||||||
|
return apis.NewBadRequestError("Current password is required", nil)
|
||||||
|
}
|
||||||
|
if !e.Auth.ValidatePassword(data.Password) {
|
||||||
|
return apis.NewBadRequestError("Invalid password", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Auth.Set("email", data.Email)
|
||||||
|
e.Auth.Set("verified", false)
|
||||||
|
if err := e.App.Save(e.Auth); err != nil {
|
||||||
|
var verr validation.Errors
|
||||||
|
if errors.As(err, &verr) {
|
||||||
|
return apis.NewBadRequestError("Validation failed", verr)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
app := e.App
|
||||||
|
routine.FireAndForget(func() {
|
||||||
|
if err := mails.SendRecordVerification(app, e.Auth); err != nil {
|
||||||
|
app.Logger().Error("Failed to send verification email", "error", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
token, err := e.Auth.NewAuthToken()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.JSON(http.StatusOK, map[string]any{
|
||||||
|
"token": token,
|
||||||
|
"record": e.Auth,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
email?: string;
|
email?: string;
|
||||||
onsave?: (email: string) => void;
|
onsave?: (email: string, currentPassword: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { email = "", onsave }: Props = $props();
|
let { email = "", onsave }: Props = $props();
|
||||||
@@ -18,24 +18,28 @@
|
|||||||
|
|
||||||
export function openModal() {
|
export function openModal() {
|
||||||
setFields("email", email);
|
setFields("email", email);
|
||||||
|
setFields("currentPassword", "");
|
||||||
setErrors("email", []);
|
setErrors("email", []);
|
||||||
|
setErrors("currentPassword", []);
|
||||||
modal.openModal();
|
modal.openModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
const { form, errors, setFields, setErrors } = createForm<{
|
const { form, errors, setFields, setErrors } = createForm<{
|
||||||
email: string;
|
email: string;
|
||||||
|
currentPassword: string;
|
||||||
}>({
|
}>({
|
||||||
initialValues: { email: untrack(() => email) },
|
initialValues: { email: untrack(() => email), currentPassword: "" },
|
||||||
extend: validator({
|
extend: validator({
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
email: z
|
email: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, "required")
|
.min(1, "required")
|
||||||
.email("not-a-valid-email-address"),
|
.email("not-a-valid-email-address"),
|
||||||
|
currentPassword: z.string().min(1, "required"),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
onSubmit: async (form) => {
|
onSubmit: async (form) => {
|
||||||
onsave?.(form.email);
|
onsave?.(form.email, form.currentPassword);
|
||||||
modal.closeModal!();
|
modal.closeModal!();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -48,8 +52,9 @@
|
|||||||
bind:this={modal}
|
bind:this={modal}
|
||||||
>
|
>
|
||||||
{#snippet content()}
|
{#snippet content()}
|
||||||
<form id="email-form" use:form>
|
<form id="email-form" use:form class="flex flex-col gap-4">
|
||||||
<TextField name="email" error={$errors.email}></TextField>
|
<TextField name="email" label={$_("email")} error={$errors.email}></TextField>
|
||||||
|
<TextField name="currentPassword" type="password" label={$_("current-password")} error={$errors.currentPassword}></TextField>
|
||||||
</form>
|
</form>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet footer()}
|
{#snippet footer()}
|
||||||
|
|||||||
@@ -78,9 +78,10 @@ export async function logout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function users_update(user: User | { [K in keyof User]?: User[K] }, avatar?: File) {
|
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, {
|
let r = await fetch('/api/v1/user/' + user.id, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(user)
|
body: JSON.stringify(payload)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
@@ -121,6 +122,22 @@ export async function users_update(user: User | { [K in keyof User]?: User[K] },
|
|||||||
currentUser.set(merged);
|
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) {
|
export async function users_delete(user: User) {
|
||||||
const r = await fetch('/api/v1/user/' + user.id, {
|
const r = await fetch('/api/v1/user/' + user.id, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
|
|||||||
@@ -83,19 +83,23 @@ export async function POST(event: RequestEvent) {
|
|||||||
const params = event.params
|
const params = event.params
|
||||||
const safeParams = RecordIdSchema.parse(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) {
|
if (data.email !== undefined) {
|
||||||
const r = await event.locals.pb.collection('users').requestEmailChange(safeData.email);
|
return json({ message: 'Use POST /api/v1/user/{id}/email for email changes' }, { status: 400 });
|
||||||
event.locals.pb.authStore.record!.email = safeData.email;
|
|
||||||
}
|
}
|
||||||
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) {
|
if (safeData.password) {
|
||||||
const r = await event.locals.pb.collection('users').authWithPassword(safeData.email ?? safeData.username!, safeData.password);
|
const authR = await event.locals.pb.collection('users').authWithPassword(event.locals.pb.authStore.record!.email, safeData.password);
|
||||||
return json(r.record)
|
return json(authR.record)
|
||||||
} else {
|
|
||||||
return json(r);
|
|
||||||
}
|
}
|
||||||
|
return json(r);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
return handleError(e);
|
return handleError(e);
|
||||||
}
|
}
|
||||||
|
|||||||
33
web/src/routes/api/v1/user/[id]/email/+server.ts
Normal file
33
web/src/routes/api/v1/user/[id]/email/+server.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
logout,
|
logout,
|
||||||
users_delete,
|
users_delete,
|
||||||
users_update,
|
users_update,
|
||||||
|
users_update_email,
|
||||||
} from "$lib/stores/user_store";
|
} from "$lib/stores/user_store";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
@@ -52,9 +53,9 @@
|
|||||||
goto("/");
|
goto("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateEmail(email: string) {
|
async function updateEmail(email: string, currentPassword: string) {
|
||||||
try {
|
try {
|
||||||
await users_update({ ...$currentUser!, email: email });
|
await users_update_email($currentUser!.id!, email, currentPassword);
|
||||||
show_toast({
|
show_toast({
|
||||||
text: $_("email-updated"),
|
text: $_("email-updated"),
|
||||||
icon: "check",
|
icon: "check",
|
||||||
|
|||||||
Reference in New Issue
Block a user