DEV Community

Cover image for Login Magic for Your SvelteKit App (It's Easier Than You Think!)
Michael Amachree
Michael Amachree

Posted on

Login Magic for Your SvelteKit App (It's Easier Than You Think!)

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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[]
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode
  • 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}

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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
};

Enter fullscreen mode Exit fullscreen mode

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');

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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');

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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 };

Enter fullscreen mode Exit fullscreen mode

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)