How To Overwrite Pocketbase Default Pages

No Comments
Modified: 12.02.2024

Do you want to learn how to overwrite Pocketbase’s default pages – confirm verification, reset password, and update email? In this guide, we will do exactly that inside our expense tracker SvelteKit application. We will first update our configuration in Pocketbase and then create the pages with their logic in SvelteKit.

Configure Pocketbase

Before we create our custom pages in SvelteKit, we have to specify in Pocketbase that we want to use our own. For this, we open the Web UI, go to settings, then to mail settings, and here we will update the Action URL of each template.

The action URL will be: {APP_URL}:5173/auth/<action>/{TOKEN}

Where the actions are according to the template:

Next, we will create the actual routes inside our SvelteKit project.

Overwrite the Pocketbase default page: confirm verification

We will start off with the confirm verification page. As a first step, we will create /src/routes/auth/layout.svelte and in here we specify a container that centres all its contents for the actual pages:

Need help or want to share feedback? Join my discord community!

<div class="h-full w-full flex justify-center items-center">
    <slot />
</div>

Next we, we will create a directory for confirm-verification with the token as a slug. Inside of /src/routes/auth/confirm-verification/[token] we will create a +page.svelte and a +page.server.ts file. Inside of +page.server.ts we will try to verify the token on load and then return if it is a success or not. The second thing we do is evaluate the query parameters to see if we resend a verification request, and if that is the case, we need to show data based on that.

import { redirect } from "@sveltejs/kit";
import type { Actions, PageServerLoad } from "./$types";

export const load: PageServerLoad = async ({params, locals, url}) => {
    const token = params.token;
    const resend = url.searchParams.has('resend');
    const success = url.searchParams.get('success') === 'true';

    if (resend) {
        return {resend, success}
    }
    try {
        await locals.pb.collection('users').confirmVerification(token);
        return { success: true }
    } catch (error) {
        console.error(error);
        return { success: false }
    }
};

export const actions: Actions = {
    resendVerification: async ({ locals, request }) => {
        const data = await request.formData();
        const email = data.get('email') as string;

        try {
            await locals.pb.collection('users').requestVerification(email);
        } catch (error) {
            console.error(error);
            return redirect(300, `?success=false&resend`);
        }

        return redirect(300, `?success=true&resend`);
    }
};

Besides loading data, we also have a form action to resend the verification email in case of verification failure.

KOFI Logo

If this guide is helpful to you and you like what I do, please support me with a coffee!

We have four states inside the frontend based on the two variables – success and resend. We will create them using if statements in different positions. For example, everywhere where success is false, we want to allow the user to resend the email, and everywhere where success is true, we want to display a success message.

<script lang="ts">
    import { enhance } from '$app/forms';
    import type { PageData } from './$types';

    export let data: PageData;
</script>


{#if data.success}
    <aside class="alert variant-ghost-success">
        <div class="alert-message">
            <h3 class="h3">Success</h3>
            <p>{!data.resend ? "Verification successful!" : "Verification email resent successfully!"}</p>
        </div>
    </aside>
{:else}
    <aside class="alert variant-ghost-error">
        <div class="alert-message">
            <h3 class="h3">Error</h3>
            <p>{!data.resend ? "Verification failed!" : "Failed to resend verification email!"}</p>
            <hr class="my-2"/>
            <form action="?/resendVerification" method="post" use:enhance>
                <input name="email" class="input variant-filled my-2" />
                <button type="submit" class="btn variant-filled">Resend verification email</button>
            </form>
        </div>
    </aside>
{/if}

Next, we will create the password reset page. It is really similar to this logic with some smaller differences.

Overwrite the Pocketbase default page: password reset

Before we create the actual page, we will create a route group called (authed), and inside of it, create a src/routes/auth/(authed)/layout.server.ts. In the file, we will request the user and make it available on the two pages that we will create next. We do that so that we can prefill the request email if possible.

import { serializeNonPOJOs } from '$lib/utils';
import type { AuthModel } from 'pocketbase';
import type { LayoutServerLoad } from './$types';

export const load = (async ({locals}) => {
    const user = locals.pb.authStore.model;
    return {user: serializeNonPOJOs(user)} as {user: AuthModel};
}) satisfies LayoutServerLoad;

We we will again create a directory for confirm-password-reset with the token as a slug. Inside of /src/routes/auth/confirm-password-reset/[token] we will create a +page.svelte and a +page.server.ts file. In +page.server.ts we will evaluate the query parameters and return them for the frontend. This means we check the success value and whether an email resend was requested.

import { redirect } from "@sveltejs/kit";
import type { Actions, PageServerLoad } from "./$types";
import { serializeNonPOJOs } from "$lib/utils";

export const load: PageServerLoad = async ({params, locals, url}) => {
    const token = params.token;
    const resend = url.searchParams.has('resend');
    const hasSuccess = url.searchParams.has('success');
    const success = url.searchParams.get('success') === 'true';

    if (resend) {
        return {resend, success, token}
    } else if (hasSuccess) {
        return {success, token}
    }

    return { token }
};

Besides the load function, we have two form actions. One to reset the password, and one to request the password reset email again. Both will use a pocketbase function and then update the query parameters based on whether it worked or not. These query parameters are then evaluated in the load function and made available in +page.svelte.

export const actions: Actions = {
    confirmPasswordReset: async ({ locals, request }) => {
        const data = await request.formData();
        const token = data.get('token') as string;
        const password = data.get('password') as string;

        try {
            await locals.pb.collection('users').confirmPasswordReset(token, password, password);
        } catch (error) {
            console.error(error);
            return redirect(300, `?success=false`)
        }

        return redirect(300, `?success=true`)
    },
    resendPasswordReset: async ({ locals, request }) => {
        const data = await request.formData();
        const email = data.get('email') as string;

        try {
            await locals.pb.collection('users').requestPasswordReset(email);
        } catch (error) {
            console.error(error);
            return redirect(300, `?success=false&resend`)
        }

        return redirect(300, `?success=true&resend`)
    },
};

Inside the frontend we now have five states. Four states are based on the two variables, and then one more that checks whether the success variable is set. It is not set in case resetting the password has not been tried yet, meaning that we want to allow the user to reset their password in that case.
Besides that, we again show success messages when success is true and allow the user to request the email in case success is false:

<script lang="ts">
    import { enhance } from '$app/forms';
    import type { PageData } from './$types';

    export let data: PageData;    
</script>

{#if !Object.keys(data).includes('success')}
<aside class="alert variant-ghost-success">
    <div class="alert-message">
        <h3 class="h3">Change your password</h3>
        <p>Enter your new password and click on "Update password"!</p>
        <hr class="my-2 !border-t-surface-50"/>
        <form action="?/confirmPasswordReset" method="post" use:enhance>
            <input name="token" type="hidden" value={data.token} />
            <input name="password" type="password" class="input variant-filled my-2" />
            <button type="submit" class="btn variant-filled">Update password</button>
        </form>
    </div>
</aside>
{:else if data.success}
<aside class="alert variant-ghost-success">
    <div class="alert-message">
        <h3 class="h3">Change your password</h3>
        <p>{!data.resend ? "You successfully updated your password!" : "Update password requested successfully! Please check your emails."}</p>
        {#if !data.resend}
        <a href="/login" class="btn variant-filled">Go back to login</a>
        {/if}
    </div>
</aside>
{:else}
    <aside class="alert variant-ghost-error">
        <div class="alert-message">
            <h3 class="h3">Change your password</h3>
            <p>{!data.resend ? "Invalid Token! Please enter your email and try again." : "Failed to request update password! Please try again."}</p>
            <hr class="my-2 !border-t-surface-50"/>
            <form action="?/resendPasswordReset" method="post" use:enhance>
                <input name="email" class="input variant-filled my-2" value={data.user?.email || ""}  />
                <button type="submit" class="btn variant-filled">Request update password</button>
            </form>
        </div>
    </aside>
{/if}

Overwrite the Pocketbase default page: change email

The change email page is exactly the same as the password reset one, with different texts and function names we call. Therefore, we have the following +page.server.ts inside of /src/routes/auth/confirm-email-change/[token]:

import { redirect } from "@sveltejs/kit";
import type { Actions, PageServerLoad } from "./$types";

export const load: PageServerLoad = async ({params, locals, url}) => {
    const token = params.token;
    const resend = url.searchParams.has('resend');
    const hasSuccess = url.searchParams.has('success');
    const success = url.searchParams.get('success') === 'true';

    if (resend) {
        return {resend, success, token}
    } else if (hasSuccess) {
        return {success, token}
    }

    return { token }
};

export const actions: Actions = {
    confirmEmailChange: async ({ locals, request }) => {
        const data = await request.formData();
        const token = data.get('token') as string;
        const password = data.get('password') as string;

        try {
            await locals.pb.collection('users').confirmEmailChange(token, password);
        } catch (error) {
            console.error(error);
            return redirect(300, `?success=false`)
        }

        return redirect(300, `?success=true`)
    },
    resendEmailChange: async ({ locals, request }) => {
        const data = await request.formData();
        const email = data.get('email') as string;

        try {
            await locals.pb.collection('users').requestEmailChange(email);
        } catch (error) {
            console.error(error);
            return redirect(300, `?success=false&resend`)
        }

        return redirect(300, `?success=true&resend`)
    },
};

And the following +page.svelte:

<script lang="ts">
    import { enhance } from '$app/forms';
    import type { PageData } from './$types';

    export let data: PageData;
</script>

{#if !Object.keys(data).includes('success')}
<aside class="alert variant-ghost-success">
    <div class="alert-message">
        <h3 class="h3">Change your email</h3>
        <p>Enter your password and click on "Update email"!</p>
        <hr class="my-2 !border-t-surface-50"/>
        <form action="?/confirmEmailChange" method="post" use:enhance>
            <input name="token" type="hidden" value={data.token} />
            <input name="password" type="password" class="input variant-filled my-2" />
            <button type="submit" class="btn variant-filled">Update email</button>
        </form>
    </div>
</aside>
{:else if data.success}
<aside class="alert variant-ghost-success">
    <div class="alert-message">
        <h3 class="h3">Change your email</h3>
        <p>{!data.resend ? "You successfully updated your email!" : "Update email requested successfully! Please check your emails."}</p>
        {#if !data.resend}
        <a href="/login" class="btn variant-filled">Go back to login</a>
        {/if}
    </div>
</aside>
{:else}
    <aside class="alert variant-ghost-error">
        <div class="alert-message">
            <h3 class="h3">Change your email</h3>
            <p>{!data.resend ? "Invalid Token! Please enter the new email and try again." : "Failed to request update email! Please try again."}</p>
            <hr class="my-2 !border-t-surface-50"/>
            <form action="?/resendEmailChange" method="post" use:enhance>
                <input name="email" class="input variant-filled my-2" value={data.user?.email || ""} />
                <button type="submit" class="btn variant-filled">Request update email</button>
            </form>
        </div>
    </aside>
{/if}

And with that, we have overwritten the default Pocketbase pages.

You can also access the current state of the project in this repository and the branch called “course-7”.

Conclusion

In this post, you learned how to overwrite the Pocketbase default pages – confirm verification, reset password and update email – by first configuring Pocketbase accordingly and then creating these pages yourself in SvelteKit. I hope you liked the course so far. See you in the next part!

In case you have any questions, feel free to ask them, and if you want to stay up to date with all my posts, consider subscribing to my monthly newsletter!

[convertkit form=2303042]

Discussion (0)