DEV Community

Cover image for SvelteKit Surreal Database Authentication
Jonathan Gamble
Jonathan Gamble

Posted on Edited on

SvelteKit Surreal Database Authentication

SvelteKit Surreal Login

TL;DR

I created a login for Surreal Database and SvelteKit. The core server function can be reused in ANY TS SSR Framework!

Setup

Create a new SvelteKit project with TS.

npx sv create surreal-auth
ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ” ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”

Install latest version of Surreal JS. I am not using alpha for this demo.

npm i -D surrealdb
ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ” ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”

Surreal JS SDK

Unfortunately, you must manually handle errors with try and catch. I am hoping to get this fixed with destructuring. Here I created some helper functions to remedy this and put all the database functions in one place.

Connect

export async function surrealConnect({
    namespace,
    database,
    url
}: {
    namespace: string,
    database: string,
    url: string
}) {

    const db = new Surreal();

    try {

        await db.connect(url, {
            namespace,
            database
        });

    } catch (e) {

        if (e instanceof SurrealDbError) {
            return {
                data: null,
                error: e
            };
        }

        if (e instanceof Error) {
            return {
                data: null,
                error: e
            };
        }

        return {
            data: null,
            error: new Error('Unknown error during SurrealDB connection')
        };
    }
    return {
        data: db,
        error: null
    };
}
ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ” ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”

Login

export async function surrealLogin({
    db,
    namespace,
    database,
    username,
    password
}: {
    db: Surreal,
    namespace: string,
    database: string,
    username: string,
    password: string
}) {

    try {

        const signinData = await db.signin({
            namespace,
            database,
            variables: {
                username,
                password
            },
            access: 'user'
        });

        return {
            data: signinData,
            error: null
        };

    } catch (e) {

        if (e instanceof SurrealDbError) {
            return {
                data: null,
                error: e
            };
        }

        if (e instanceof Error) {
            return {
                data: null,
                error: e
            };
        }

        return {
            data: null,
            error: new Error('Unknown error during login')
        };
    }
};
ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ” ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”

Register

export async function surrealRegister({
    db,
    namespace,
    database,
    username,
    password
}: {
    db: Surreal,
    namespace: string,
    database: string,
    username: string,
    password: string
}) {

    try {

        const signupData = await db.signup({
            namespace,
            database,
            variables: {
                username,
                password
            },
            access: 'user'
        });

        return {
            data: signupData,
            error: null
        };

    } catch (e) {

        if (e instanceof SurrealDbError) {
            return {
                data: null,
                error: e
            };
        }

        if (e instanceof Error) {
            return {
                data: null,
                error: e
            };
        }

        return {
            data: null,
            error: new Error('Unknown error during registration')
        };
    }
};
ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ” ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”

Change Password

export async function surrealChangePassword({
    db,
    currentPassword,
    newPassword,
    userId
}: {
    db: Surreal,
    currentPassword: string,
    newPassword: string,
    userId: string
}) {

    try {

        const query = `
            UPDATE $id
            SET password = crypto::argon2::generate($new)
            WHERE crypto::argon2::compare(password, $old)
        `;

        const [result] = await db.query<[{
            id: string,
            password: string,
            username: string
        }][]>(query, {
            id: new StringRecordId(userId),
            old: currentPassword,
            new: newPassword
        });

        if (!result) {
            return {
                data: null,
                error: new Error("Password change failed")
            };
        }

        return {
            data: result[0],
            error: null
        };

    } catch (error) {

        if (error instanceof SurrealDbError) {
            return {
                data: null,
                error
            };
        }

        if (error instanceof Error) {
            return {
                error,
                data: null
            };
        }
        return {
            error: new Error('Unknown query error'),
            data: null
        };
    }
}
ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ” ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”

📝 You need to use the correct namespace, database, and access user when performing surreal functions.

Surreal Server

I wanted this function to be reusable in any framework, with inspiration from supabase-ssr. We can setup our server function to handle cookies and our credentials.

export function surrealServer({
    cookies: {
        cookieName,
        setCookie,
        getCookie
    },
    credentials: {
        url,
        namespace,
        database
    }
}: {
    cookies: {
        cookieName?: string,
        setCookie: SetCoookieFn,
        getCookie: GetCookieFn
    },
    credentials: {
        url: string,
        namespace: string,
        database: string
    }
}) {

    const tokenName = cookieName || 'surreal_token';

    const surrealToken = getCookie(tokenName);
...
ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ” ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”

Connect Wrapper

async function connect() {

    const { data: db, error: connectError } = await surrealConnect({
        namespace,
        database,
        url
    });

    if (connectError) {
        return {
            data: null,
            error: connectError
        };
    }

    if (surrealToken) {

        await db.authenticate(surrealToken);

        return {
            data: db,
            error: null
        };
    }

    // No token, ensure logged out

    logout();

    return {
        data: db,
        error: null
    };
}
ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ” ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”

📝 Check for token, and authenticate if there is one.

Login and Register

async function login(username: string, password: string) {

    logout();

    const { data: db, error: dbError } = await connect();

    if (dbError) {
        return {
            db: null,
            error: dbError
        };
    }

    const {
        data: token,
        error: loginError
    } = await surrealLogin({
        db,
        namespace,
        database,
        username,
        password
    });

    if (loginError) {
        return {
            db: null,
            error: loginError
        };
    }

    setCookie(
        tokenName,
        token,
        TOKEN_COOKIE_OPTIONS
    );

    return {
        db,
        error: null
    };
};

async function register(username: string, password: string) {

    logout();

    const { data: db, error: dbError } = await connect();

    if (dbError) {
        return {
            db: null,
            error: dbError
        };
    }

    const {
        data: token,
        error: registerError
    } = await surrealRegister({
        db,
        namespace,
        database,
        username,
        password
    });

    if (registerError) {
        return {
            db: null,
            error: registerError
        };
    }

    setCookie(
        tokenName,
        token,
        TOKEN_COOKIE_OPTIONS
    );

    return {
        db,
        error: null
    };
};
ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ” ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”

📝 Register or Login then save the token. This is a cookie wrapper function that you can set to work in any framework.

Change Password

async function changePassword(oldPassword: string, newPassword: string) {

    const userId = getUser();

    if (!userId) {
        return {
            data: null,
            error: new Error('Not authenticated')
        };
    }

    const { data: db, error: dbError } = await connect();

    if (dbError) {
        return {
            data: null,
            error: dbError
        };
    }

    const { data, error: changeError } = await surrealChangePassword({
        db,
        currentPassword: oldPassword,
        newPassword,
        userId
    });


    if (changeError) {
        return {
            data: null,
            error: changeError
        };
    }

    return {
        data,
        error: null
    };
}
ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ” ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”

Logout

function logout() {

    const token = getCookie(tokenName);

    if (token) {
        // delete cookie equivalent
        setCookie(tokenName, '', {
            ...TOKEN_COOKIE_OPTIONS,
            maxAge: 0            
        });
    }
};
ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ” ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”

📝 We can delete a cookie by setting it to expire. No need for an extraneous deleteCookie function.

Get User

function getUser() {

    const token = getCookie(tokenName);

    if (!token) {
        return null;
    }

    return decodeJwt(token).ID as string;
}

async function getUserInfo() {

    const { data: db, error: dbError } = await connect();

    if (dbError) {
        return null;
    }

    const info = await db.info();

    if (!info?.id) {
        return null;
    }

    return info.id.toString();
}
ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ” ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”

📝 We DO NOT want to make an extraneous call to the database for every server call, but on secure queries, like change password, we do. To avoid extra fetching, we can decode the JWT and use getUser, otherwise, we can get the user data directly from the database with getUserInfo.

export function decodeJwt(token: string) {
    try {
        const [, payloadB64] = token.split('.');
        return JSON.parse(
            Buffer.from(payloadB64, 'base64url').toString()
        ) as JwtPayload;
    } catch {
        return {};
    }
}
ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ” ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”

📝 We need try and catch to prevent a problem with JSON.parse() or Buffer in case of data corruption.

Server Hook

For SvelteKit, we can use the hooks.server.ts file to allow our surreal server to be available everywhere.

import type { Handle } from '@sveltejs/kit';
import {
    PRIVATE_SURREALDB_URL,
    PRIVATE_SURREALDB_NAMESPACE,
    PRIVATE_SURREALDB_DATABASE
} from '$env/static/private';
import { surrealServer } from './lib/surreal/surreal-server';

const config = {
    url: PRIVATE_SURREALDB_URL,
    namespace: PRIVATE_SURREALDB_NAMESPACE,
    database: PRIVATE_SURREALDB_DATABASE
};

export const handle: Handle = async ({ event, resolve }) => {

    event.locals.surreal = surrealServer({
        cookies: {
            setCookie: (name, value, options) => 
                event.cookies.set(name, value, options),
            getCookie: (name) => event.cookies.get(name)
        },
        credentials: {
            url: config.url,
            namespace: config.namespace,
            database: config.database
        }
    });

    return resolve(event);
};
ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ” ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”

We use the event in hooks with native cookie functions. This can work in ANY framework.

Remember to update the types.

import type { surrealServer } from '$lib/surreal/surreal-server';

declare global {
    namespace App {
        // interface Error {}
        interface Locals {
            surreal: ReturnType<typeof surrealServer>;
        }
        // interface PageData {}
        // interface PageState {}
        // interface Platform {}
    }
}

export {};
ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ” ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”

Auth Guards

Hide page from unauthorized users:

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

export const load: PageServerLoad = async ({ locals: { surreal } }) => {

    const userId = surreal.getUser();

    if (!userId) {
        redirect(303, '/login');
    }
};
ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ” ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”

Hide Login page from session users:

export const load: PageServerLoad = async ({ locals: { surreal } }) => {

    const userId = surreal.getUser();

    if (userId) {
        redirect(303, '/');
    }
};
ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ” ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”

Server Actions

    login: async ({ request, locals: { surreal } }) => {

        const formData = await request.formData();

        const { username, password } = Object.fromEntries(formData);

        if (typeof username !== 'string' || typeof password !== 'string') {
            error(500, 'Invalid form data');
        }

        const {
            error: loginError
        } = await surreal.login(username, password);

        if (loginError) {
            return {
                error: loginError.message
            };
        }

        redirect(303, '/');
    },

    logout: async ({ locals: { surreal } }) => {
        surreal.logout();
        redirect(303, '/');
    }
ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ” ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”

If we're just logging in, we don't need to verify username and password strings. If we did, we could use valibot.

const registerSchema = v.object({
    username: v.pipe(
        v.string(),
        v.minLength(3, 'Username must be at least 3 characters long')
    ),
    password: v.pipe(
        v.string(),
        v.minLength(3, 'Password must be at least 3 characters long')
    )
});

...

export const actions: Actions = {

    register: async ({ request, locals: { surreal } }) => {

        const formData = await request.formData();

        const data = Object.fromEntries(formData);

        const result = v.safeParse(registerSchema, data);

        if (!result.success) {
            error(400, result.issues[0].message);
        }

        const { username, password } = result.output;

        const {
            error: registerError
        } = await surreal.register(username, password);

        if (registerError) {
            error(500, registerError.message);
        }

        redirect(303, '/');
    }
};
ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ” ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”

Or, we could manually check for something simple.

export const actions: Actions = {

    changePassword: async ({ request, locals: { surreal } }) => {

        const formData = await request.formData();

        const { oldPassword, newPassword } = Object.fromEntries(formData);

        if (typeof oldPassword !== 'string'
            || typeof newPassword !== 'string'
            || newPassword.length < 3) {

            error(500, 'Invalid password data');
        }

        const {
            data: passwordChangeData,
            error: passwordChangeError
        } = await surreal.changePassword(oldPassword, newPassword);

        if (passwordChangeError) {
            return {
                error: passwordChangeError.message
            };
        }

        if (passwordChangeData?.password) {
            return {
                success: true
            };
        }

        return {
            success: true
        };
    }
};
ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ” ðŸ“Ē āđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™: jdgamble/sveltekit surreal database authentication e - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”

Surreal Alpha is going to start handling REFRESH tokens, but this is the latest stable version for now.

And that's all folks!

Repo: GitHub
Example Schema: GitHub

J

Top comments (0)