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
Install latest version of Surreal JS. I am not using alpha for this demo.
npm i -D surrealdb
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
};
}
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')
};
}
};
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')
};
}
};
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
};
}
}
๐ 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);
...
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
};
}
๐ 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
};
};
๐ 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
};
}
Logout
function logout() {
const token = getCookie(tokenName);
if (token) {
// delete cookie equivalent
setCookie(tokenName, '', {
...TOKEN_COOKIE_OPTIONS,
maxAge: 0
});
}
};
๐ 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();
}
๐ 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 {};
}
}
๐ 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);
};
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 {};
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');
}
};
Hide Login page from session users:
export const load: PageServerLoad = async ({ locals: { surreal } }) => {
const userId = surreal.getUser();
if (userId) {
redirect(303, '/');
}
};
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, '/');
}
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, '/');
}
};
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
};
}
};
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)