Auth and Databases in Sveltekit

2023-11-12T05:40:33.821Z

Recently I implemented HTTP Basic Auth using a session cookie in SvelteKit on Cloudflare Pages using Cloudflare D1 as a database backend. Here I’ll show some code snippets so you can do the same.

First you would want to set up your project according to this. So you can have a basic project set up.

Next you’ll want to add the database to your project. It’s easy to create using wrangler. Just remember to link it to your project in your cloudflare dash under settings > functions.

wrangler d1 create your-database-name

Then you can copy it into your wrangler.toml so your local dev works with it. To run with support for D1 you can use this oneliner.

npm run build && wrangler pages dev .svelte-kit/cloudflare

My final schema looks like this. Yours might be a bit different, but mine allows for adding songs to a playlist which will be the purpose of my website.

CREATE TABLE IF NOT EXISTS Users (id TEXT PRIMARY KEY, password TEXT);
CREATE TABLE IF NOT EXISTS Playlist (id TEXT PRIMARY KEY, email TEXT, url TEXT, date TEXT);

You’ll want to create these tables using wrangler both locally (on your dev server) and remotely.

wrangler d1 execute svelte-auth --local --file=./schema.sql
wrangler d1 execute svelte-auth --file=./schema.sql

Now you can add it to your app.d.ts like this.

declare global {
	namespace App {
		interface Platform {
			env?: {
				DB: D1Database;
			};
			context: {
				waitUntil(promise: Promise<any>): void;
			};
			caches: CacheStorage & { default: Cache };
		}
	}
}

export {};

Now you’re ready to set up authentication. It’s probably easier to just show you how I did it. I imported sha256 which is just a simple function I wrote to call web crypto to get the sha256sum of a string. Here is my code.

I added form actions. These allow you to quickly write code that will work with HTML <form>‘s.

import { redirect } from '@sveltejs/kit';
import sha256 from '$lib/sha256';

export const actions = {
	register: async (event) => {
		const data = await event.request.formData();
		const email = data.get('email');
		const password = await sha256(data.get('password')?.toString() ?? '');
		const { success } = await event.platform?.env?.DB.prepare('INSERT INTO Users VALUES (?, ?)')
			.bind(email, password)
			.all();
		if (success) {
			event.cookies.set('session', JSON.stringify({ email, password }));
			throw redirect(303, '/');
		}
	},
	login: async (event) => {
		const data = await event.request.formData();
		const email = data.get('email');
		const password = await sha256(data.get('password')?.toString() ?? '');
		const results = await event.platform?.env?.DB.prepare('SELECT password FROM Users WHERE id=?')
			.bind(email)
			.all();
		if (results.results[0].password === password) {
			event.cookies.set('session', JSON.stringify({ email, password }));
			throw redirect(303, '/');
		}
	}
};

Now this is enough to save a cookie with the user auth, but to re-auth on a page we need to check if their cookie is valid. So how I do this is I wrote another function called tryLogin.ts.

import type { D1Database } from '@cloudflare/workers-types';

const tryLogin = async (session_cookie: string | undefined, DB: D1Database) => {
	if (session_cookie) {
		const { email, password } = JSON.parse(session_cookie);
		const results = await DB.prepare('SELECT password FROM Users WHERE id=?').bind(email).all();
		if (results.results[0].password === password) {
			return true;
		}
	}
	return false;
};

export default tryLogin;

Now I can call this inside +page.server.ts.

import tryLogin from '$lib/tryLogin';
import getCurrentSong from '$lib/getCurrentSong';

export const load = async (event) => ({
	auth: await tryLogin(event.cookies.get('session'), event.platform?.env?.DB),
	song: await getCurrentSong(event.platform?.env?.DB)
});

Using it inside the +page.svelte is as simple as this one line.

export let data: {auth: boolean, song: {id: string, url: string}};

Now I wrote more form actions for adding and removing songs from the playlist here.

import { redirect } from '@sveltejs/kit';
import tryLogin from '$lib/tryLogin';

export const actions = {
	add: async (event) => {
		if (await tryLogin(event.cookies.get('session'), event.platform?.env?.DB)) {
			const session = JSON.parse(event.cookies.get('session'));
			const email = session.email;
			const formdata = await event.request.formData();
			const v = new URL(formdata.get('url')).searchParams.get('v');
			await event.platform?.env?.DB.prepare('INSERT INTO Playlist VALUES (?, ?, ?, ?)')
				.bind(crypto.randomUUID(), email, v, Math.floor(new Date().getTime()))
				.all();
		}
		throw redirect(303, '/');
	},
	remove: async (event) => {
		if (await tryLogin(event.cookies.get('session'), event.platform?.env?.DB)) {
			const formdata = await event.request.formData();
			const id = formdata.get('id');
			await event.platform?.env?.DB.prepare('DELETE FROM Playlist WHERE id=?').bind(id).all();
		}
		throw redirect(303, '/');
	}
};

Now this allows us to add songs, we just need the interface for it. So I wrote the main page to allow logged in users to submit songs.

<script lang="ts">
	import { enhance } from '$app/forms';
	import YouTube from '$lib/svelte-youtube.svelte';
	export let data: { auth: boolean; song: { id: string; url: string } };
</script>

{#if !data.auth}
	<p>Visit <a href="/login">login</a></p>
{/if}

<YouTube
	on:end={() => document.getElementById('remove_song')?.click()}
	videoId={data.song.url}
	options={{ playerVars: { autoplay: 1 } }}
/>

<form style="display: none;" method="POST" action="/playlist?/remove" use:enhance>
	<input name="id" value={data.song.id} />
	<button id="remove_song">remove</button>
</form>

{#if data.auth}
	<form method="POST" action="/playlist?/add" use:enhance>
		<label
			>Submit a song
			<input name="url" type="url" placeholder="paste a youtube link" pattern=".*v=.*" />
		</label>
		<button>Submit</button>
	</form>
	<p><a href="/logout">Log out</a></p>
{/if}

If you have any problems or you want to see the entire repo. The code is here and the live demo is here.