Intro
Hey there, SvelteKit fans! Ever wanted to add that extra layer of security to your app, like a login system, but worried it would be a massive headache? Well, fret no more! This article is here to be your friendly guide to building a secure login system for your SvelteKit app. We'll be using some awesome tools to make things smooth and simple, like Prisma for our database, Tailwind CSS for a sharp look, and of course, SvelteKit itself to handle everything else.
By the end of this, you'll have a login system up and running without feeling like you just coded your way through a maze. So, grab your favorite coding beverage and let's jump in! (We'll start with the code in the next section.)
Building on a Great Foundation
I actually started this journey by following a fantastic guide on Joy of Code. It's a great resource, but some things needed a little tweaking to work smoothly in 2024. So, consider this your upgraded version – a 2024 refresh for a seamless login experience!
Assuming You Already Have an App
I'm assuming you already have a SvelteKit app set up and ready to go. If not, no worries! Just run this command in your terminal:
npm create svelte@latest <app name>
Replace <app name>
with the desired name for your app. This command will create a fresh SvelteKit project.
Setting Up Prisma
Next, we'll need to set up Prisma, a powerful tool that makes interacting with databases a breeze. We'll use SQLite for development purposes, but we'll discuss production options later. Run the following command in your terminal:
npx prisma init --datasource-provider sqlite
This command will create a prisma
folder and a .env
file at the root of your project. The .env
file will store the connection string for your database. Inside the prisma
folder, you'll find the Prisma schema file and the SQLite database file.
Let's Talk Schema
The Prisma schema defines the structure of our database tables. Here's the code that defines the User
and Roles
tables:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model User {
id String @id @default(uuid())
username String @unique
passwordHash String
userAuthToken String @unique
isAdmin Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
role Roles @relation(fields: [roleId], references: [id])
roleId BigInt
}
model Roles {
id BigInt @id @default(autoincrement())
name String @unique
users User[]
}
This schema creates two tables: User
and Roles
. The User
table stores user information like username, password hash, and authentication token. The Roles
table allows you to define different user roles with unique permissions.
Why BigInt for ID?
I chose BigInt
for the ID field because I plan on using CockroachDB in production. This makes it easier to have a consistent schema that works with both SQLite and CockroachDB without any issues.
We've covered the initial setup, and now we're ready to dive into the code itself! Stay tuned for the next section, where we'll start building the login functionality for your SvelteKit app.
Conquering the Database with Prisma (Our Fancy Treasure Chest)
Now that we have our map (schema) and our treasure chest (database) set up, it's time to grab the loot (data) using Prisma! To do this, we'll need a special tool called the Prisma Client.
Installing the Treasure Hunter (Prisma Client)
First, let's open our terminal again and cast this magic spell (command):
npm i @prisma/client
This command installs the @prisma/client
package, which acts like our treasure hunter. It also runs a command called prisma generate
behind the scenes. This command generates a special Prisma Client specifically designed for our project's schema. Think of it as a custom key that unlocks the treasure chest in exactly the way we need!
Opening the Treasure Chest (Pushing the Database)
Next, let's use another magic spell (command) to create the actual database tables based on our schema:
npx prisma db push
This command uses the Prisma Client we just installed and pushes our schema to the database, creating the User
and Roles
tables we defined earlier. Now, our treasure chest is officially open and ready for us to explore!
Introducing Our Treasure Guide (Prisma Client)
Finally, let's create a guide (Prisma Client instance) that we can use throughout our project to interact with the database. Here's a bit of code that does just that:
// src/lib/database.ts
import prisma from '@prisma/client'
export const db = new prisma.PrismaClient()
This code imports the prisma
object from the @prisma/client
package and creates a new instance called db
. This db
instance is like our personal treasure guide; we can use it to interact with the database, add new users, retrieve user information, and more!
We've successfully set up Prisma and are now ready to leverage its power to build secure login functionality for our SvelteKit app. Stay tuned for the next section, where the real adventure begins!
Building the Login Form (Our Portal to Adventure)
Now that we have our treasure chest unlocked (database) and our guide by our side (Prisma Client), it's time to build the login form – the portal that will allow users to enter our app. We'll use Svelte and Tailwind CSS to create a user-friendly and visually appealing form. Here's what we'll cover in the next section:
- Creating a Svelte component for the login form
- Using Tailwind CSS to style the form elements (inputs, buttons)
- Handling form submission events (capturing username and password)
- Validating user input (making sure the username and password are entered correctly)
With a secure and stylish login form in place, we'll be well on our way to building a robust authentication system for our SvelteKit app!
Building User Registration: A Smooth Start
We've conquered the database, and now it's time to create a registration system for your awesome SvelteKit app. This will allow users to sign up for an account and unlock all the cool features you've built. Buckle up, because we're about to break down the code for the registration page (Register/+page.svelte) and the server-side logic (Register/+page.server.ts) in a way that's easy to follow.
1. The Registration Page: Where the Magic Begins
Let's start with the Register.svelte
component. This is your user's first impression of your app, so let's make it a good one! This is where users will enter their registration information.
<!-- Register.svelte -->
<script lang="ts">
import type { ActionData } from './$types';
export let form: ActionData;
let showPassword = false;
$: password = showPassword ? 'text' : 'password';
</script>
<div class="flex min-h-screen items-center justify-center bg-gray-50 px-4 py-12 dark:bg-gray-800 sm:px-6 lg:px-8">
<div class="w-full max-w-md space-y-8">
<div>
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900 dark:text-white">
Register
</h2>
</div>
<form action="?/register" method="POST" class="mt-8 space-y-6">
<!-- Form inputs -->
</form>
</div>
</div>
Code Breakdown:
- We import the necessary things to make the registration work (like
ActionData
from./$types
). - We have a variable called
showPassword
that lets users see what they're typing for their password (super helpful!).
In the HTML section, we have a form where users can input their username, password, and choose whether they're an admin (but psst, that's a secret for later).
2. Handling What Users Type
Inside the form, we have sections for the username, password, and an "admin" checkbox.
<!-- Inside the form -->
<div>
<label for="username" class="sr-only">Username</label>
<input
id="username"
name="username"
type="text"
required
placeholder="Username"
/>
</div>
<div>
<label for="password" class="sr-only">Password</label>
<input
id="password"
name="password"
type={password}
required
placeholder="Password"
/>
</div>
<div class="flex items-center py-5">
<input
type="checkbox"
bind:checked={showPassword}
id="showPassword"
class="form-checkbox h-5 w-5 text-indigo-600"
/>
<label for="showPassword" class="ml-2 text-sm text-gray-700 hover:cursor-pointer dark:text-gray-200">
Show Password
</label>
</div>
<div class="flex items-center pt-5">
<input
type="checkbox"
name="admin"
id="admin"
class="form-checkbox h-5 w-5 text-green-600 dark:text-green-400"
/>
<label for="admin" class="ml-2 text-sm text-gray-700 dark:text-gray-200">
Check if you're an admin
</label>
</div>
- The username and password fields are required, because, well, you gotta have those!
- The password field lets users see what they're typing with a handy "Show Password" toggle.
- The "admin" checkbox is there for special users, but we won't talk about that just yet .
3. Oops! Username Taken
Sometimes, usernames might already be taken by someone else (shocker, right?). If that happens, we'll show a friendly message letting the user know they need to pick a different one.
<!-- After the form -->
{#if form?.user}
<p class="error">Username is taken.</p>
{/if}
4. All Set? Let's Register!
<!-- After the error message -->
<button
type="submit"
class="group relative flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-indigo-700 dark:hover:bg-indigo-800 dark:focus:ring-indigo-700"
>
Register
</button>
Once users have filled out the form, they can click the "Register" button to complete the process. This will trigger some code on the server to create their account. (Yes, I know Tailwind classes can get a bit long-winded at times, but hey, it makes styling a breeze!)
5. The Server-Side: Where the Action Happens
Now, let's move on to the behind-the-scenes stuff in Register.server.ts
. This is where the server does its magic to create user accounts and make everything work smoothly. But before we dive in, there's one more crucial ingredient: secure password storage.
Hashing Passwords with bcryptjs
We'll be using a library called bcryptjs
to securely hash user passwords before storing them in the database. Hashing is a one-way process that transforms a password into a scrambled string, making it impossible to decrypt and retrieve the original password. This is essential for protecting user data.
To install bcryptjs
and its type definitions, you can run the following commands in your terminal:
npm install bcryptjs
npm install --save @types/bcryptjs # or
npm install -D @types/bcryptjs # for type definitions only
Now that bcryptjs
is ready to go, let's take a look at the code in Register.server.ts
:
// Register.server.ts
import { error, fail, redirect } from '@sveltejs/kit';
import type { Action, Actions, PageServerLoad } from './$types';
import bcrypt from 'bcryptjs';
import { db } from '$lib/database';
import { Admin_PW } from '$env/static/private';
We've added bcrypt
to our imports, which will allow us to securely hash user passwords in the registration process.
6. Creating a New User
The register
function is where the real fun happens! This is where the server takes the information from the form and creates a new user account.
// Register.server.ts
const adminHash = Admin_PW;
const register: Action = async ({ request }) => {
// Form data processing
};
7. Extracting Information
First, the server grabs the username, password, and admin status from the form data.
// Inside the register action
const data = await request.formData();
const username = data.get('username');
const password = data.get('password');
const admin = data.get('admin');
8. Checking Usernames (No Duplicates Allowed!)
The server checks if the chosen username is already taken. If it is, we send a message back to the user letting them know they need to pick a different one.
// Inside the register action
const isAdmin = admin === 'on' && bcrypt.compareSync(password.toString(), adminHash);
// Checking if the username already exists
const existingUser = await db.user.findUnique({ where: { username } });
if (existingUser) {
return fail(400, { user: true });
}
// Creating the user
await createUser(username, password, isAdmin);
9. Creating the User Account
If the username is good to go, the server creates the user account with a secure password hash.
10. Welcome Aboard!
Once the account is created, the user is redirected to the login page, ready to start exploring your awesome app!
// Inside the register action
return redirect(303, '/Login');
Full Code for Register Page Component
<!-- Register.svelte -->
<script lang="ts">
import type { ActionData } from './$types';
export let form: ActionData;
let showPassword = false;
$: password = showPassword ? 'text' : 'password';
</script>
<div class="flex min-h-screen items-center justify-center bg-gray-50 px-4 py-12 dark:bg-gray-800 sm:px-6 lg:px-8">
<div class="w-full max-w-md space-y-8">
<div>
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900 dark:text-white">
Register
</h2>
</div>
<form action="?/register" method="POST" class="mt-8 space-y-6">
<div class="-space-y-px rounded-md shadow-sm">
<div>
<label for="username" class="sr-only">Username</label>
<input
id="username"
name="username"
type="text"
required
placeholder="Username"
/>
</div>
<div>
<label for="password" class="sr-only">Password</label>
<input
id="password"
name="password"
type={password}
required
placeholder="Password"
/>
</div>
<div class="flex items-center py-5">
<input
type="checkbox"
bind:checked={showPassword}
id="showPassword"
class="form-checkbox h-5 w-5 text-indigo-600"
/>
<label for="showPassword" class="ml-2 text-sm text-gray-700 hover:cursor-pointer dark:text-gray-200">
Show Password
</label>
</div>
<div class="flex items-center pt-5">
<input
type="checkbox"
name="admin"
id="admin"
class="form-checkbox h-5 w-5 text-green-600 dark:text-green-400"
/>
<label for="admin" class="ml-2 text-sm text-gray-700 dark:text-gray-200">
Check if you're an admin
</label>
</div>
</div>
{#if form?.user}
<p class="error">Username is taken.</p>
{/if}
<div>
<button
type="submit"
class="group relative flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-indigo-700 dark:hover:bg-indigo-800 dark:focus:ring-indigo-700"
>
Register
</button>
</div>
</form>
</div>
</div>
Full Code for Register Page Server-Side Logic
// Register.server.ts
import { error, fail, redirect } from '@sveltejs/kit';
import type { Action, Actions, PageServerLoad } from './$types';
import bcrypt from 'bcryptjs';
import { db } from '$lib/database';
import { Admin_PW } from '$env/static/private';
export const load: PageServerLoad = async (session) => {
var sessionData = session.cookies.get('session');
if (sessionData) {
return redirect(303, '/');
}
return {};
};
const adminHash = Admin_PW;
const register: Action = async ({ request }) => {
const data = await request.formData();
const username = data.get('username');
const password = data.get('password');
const admin = data.get('admin');
let shouldRedirect = false;
if (typeof username !== 'string' || typeof password !== 'string' || !username || !password) {
return error(400, 'Username and Password must be a string');
}
const isAdmin = admin === 'on' && bcrypt.compareSync(password.toString(), adminHash);
try {
const existingUser = await db.user.findUnique({
where: { username }
});
if (existingUser) {
return fail(400, { user: true });
}
await createUser(username, password, isAdmin);
// Check if admin === 'on' and isAdmin is false after user creation
if (admin === 'on' && !isAdmin) {
shouldRedirect = true;
}
} catch (error) {
console.error('Error during user registration:', error);
return fail(500, { error: 'Internal server error' });
}
if (shouldRedirect) {
return redirect(303, '/Login');
}
return redirect(303, '/Login');
};
async function createRoleIfNotExists(roleName: string) {
const existingRole = await db.roles.findUnique({
where: { name: roleName }
});
if (!existingRole) {
await db.roles.create({
data: { name: roleName }
});
}
}
async function createUser(username: string, password: string, isAdmin: boolean) {
const passwordHash = isAdmin ? adminHash : await bcrypt.hash(password, 10);
const roleName = isAdmin ? 'ADMIN' : 'USER';
let role = await db.roles.findUnique({
where: { name: roleName }
});
if (!role) {
await createRoleIfNotExists(roleName);
role = await db.roles.findUnique({
where: { name: roleName }
});
}
await db.user.create({
data: {
username,
passwordHash,
isAdmin,
userAuthToken: crypto.randomUUID(),
role: { connect: { id: role?.id } }
}
});
}
export const actions: Actions = { register };
This completes our user registration for your SvelteKit app. Now, users can register and start using your app!
Hey heads up!
Before you dive into coding the server-side logic (Register.server.ts), make sure you've created and saved the registration form component (Register.svelte) first. TypeScript might throw some errors because it can't find the component yet. Don't worry, these will magically disappear once you've created the Actions in Register.server.ts! Just a little behind-the-scenes quirk to keep in mind.
Phew, that was a lot of ground to cover!
We've built a solid foundation for user registration in your SvelteKit app. To keep things comfortable, we'll break it up here and continue in another article. Next time, we'll tackle user login, logout, and maybe even dive into creating a profile page. But that's not all! We also need to make sure our shiny new registration system is production-ready. Stay tuned, because the best is yet to come!
Keywords: SvelteKit, user registration, user authentication (auth), secure password hashing, bcryptjs, SvelteKit user management
Top comments (0)