DEV Community

Cover image for FullStack JWT Auth: Diving into SvelteKit - Login and Registration
John Owolabi Idogun
John Owolabi Idogun

Posted on

FullStack JWT Auth: Diving into SvelteKit - Login and Registration

Introduction

Having hit the ground running with SvelteKit by building our project's layout as well as its logout feature in the previous article, we'll continue exploring SvelteKit in this article by implementing login and registration features.

Source code

The overall source code for this project can be accessed here:

GitHub logo Sirneij / django_svelte_jwt_auth

A robust and secure Authentication and Authorization System built with Django and SvelteKit

django_svelte_jwt_auth

This is the codebase that follows the series of tutorials on building a FullStack JWT Authentication and Authorization System with Django and SvelteKit.

This project was deployed on heroku (backend) and vercel (frontend) and its live version can be accessed here.

To run this application locally, you need to run both the backend and frontend projects. While the latter has some instructions already for spinning it up, the former can be spinned up following the instructions below.

Run locally

To run locally

  • Clone this repo:

     git clone https://github.com/Sirneij/django_svelte_jwt_auth.git
    
  • Change directory into the backend folder:

     cd backend
    
  • Create a virtual environment:

     pipenv shell
    

    You might opt for other dependencies management tools such as virtualenv, poetry, or venv. It's up to you.

  • Install the dependencies:

    pipenv install
    
  • Make migrations and migrate the database:

     python manage.py makemigrations
     python manage.py migrate
    
  • Finally, run the application:

     python manage.py runserver
    

Live version

This project was deployed on heroku (backend) and vercel (frontend) and its live version can be accessed here.

Step 1: Implement the login functionality

Let's begin by implementing the login functionality of our app. Open up routes/accounts/login/index.svelte in your editor and make the content look like:

<script>
    import { notificationData } from '../../../store/notificationStore';
    import { post, browserSet, browserGet } from '$lib/requestUtils';
    import { goto } from '$app/navigation';
    import { BASE_API_URI } from '$lib/constants';
    import { fly } from 'svelte/transition';

    import { onMount } from 'svelte';

    let email = '',
        password = '',
        error;

    const handleLogin = async () => {
        if (browserGet('refreshToken')) {
            localStorage.removeItem('refreshToken');
        }
        const [jsonRes, err] = await post(fetch, `${BASE_API_URI}/login/`, {
            user: {
                email: email,
                password: password
            }
        });
        if (err) {
            error = err;
        } else if (jsonRes.user.tokens) {
            browserSet('refreshToken', jsonRes.user.tokens.refresh);
            notificationData.set('Login successful.');
            await goto('/');
        }
    };
    onMount(() => {
        const notifyEl = document.getElementsByClassName('notification');

        if (notifyEl && $notificationData !== '') {
            setTimeout(() => {
                notifyEl.display = 'none';
                notificationData.set('');
            }, 5000);
        }
    });
</script>

<svelte:head>
    <title>Login | FullStack Django & SvelteKit</title>
</svelte:head>

<section
    class="container"
    in:fly={{ x: -100, duration: 500, delay: 500 }}
    out:fly={{ duration: 500 }}
>
    <h1>Login</h1>
    {#if error}
        <p class="center error">{error}</p>
    {/if}
    <form class="form" on:submit|preventDefault={handleLogin}>
        <input
            bind:value={email}
            name="email"
            type="email"
            aria-label="Email address"
            placeholder="Email address"
        />
        <input
            bind:value={password}
            name="password"
            type="password"
            aria-label="password"
            placeholder="password"
        />
        <button class="btn" type="submit">Login</button>
        <p class="center">No account yet? <a href="/accounts/register">Get started</a>.</p>
    </form>
</section>
Enter fullscreen mode Exit fullscreen mode

This .svelte file contains a couple of new imports and some scripts aside the notificationData explained in the previous article. The first notable import is post. This abstracts away sending POST requests to the server and has the following definition in lib/requestUtils.ts:

// lib -> requestUtils.ts
...
export const post = async (fetch, url: string, body: unknown) => {
  try {
    const headers = {};
    if (!(body instanceof FormData)) {
      headers["Content-Type"] = "application/json";
      body = JSON.stringify(body);
      const token = browserGet("refreshToken");
      if (token) {
        headers["Authorization"] = `Bearer ${token}`;
      }
      const res = await fetch(url, {
        method: "POST",
        body,
        headers,
      });
      if (res.status === 400) {
        const data = await res.json();
        const error = data.user.error[0];
        return [{}, error];
        // throw { id: error.id, message: error };
      }
      const response = await res.json();
      return [response, ""];
    }
  } catch (error) {
    console.error(`Error outside: ${error}`);

    // throw { id: '', message: 'An unknown error occurred.' };
    return [{}, `An unknown error occurred. ${error}`];
  }
};
...
Enter fullscreen mode Exit fullscreen mode

It is an asynchronous function which expects the global window.fetch, the url to send the request, and the data to be sent. Looking into the try block, we enforced that only json data type will be handled and then went on to make the post request while ensuring proper error handling from the response.

Back to the .svelte file, we declared some variables — email, and password — and bound them to their respective form inputs using the bind:value directive. A very simple and intuitive way of binding input values without the ceremonial state bindings in react. To give feedbacks about possible errors, we also have the error variable declared which later on was given the error response from the post function.

Entering the handleLogin asynchronous function, we first remove any residual refreshToken that might lurking around user's browser. If not done, we will be faced with some non-informative error if the user tries to login. Then we called on our post function and passed in the required arguments. If no errors was encountered, we save the user's refeshToken to localStorage, update the notoficationData and redirects the user to the home page. The handleLogin function was called on the form's submission using the on:submit directive. Notice that before assigning this directive to the function, we added |preventDefault. This is extremely important to prevent full page refresh which defeats app-like feel.

Since users are automatically redirected to the login page after logging out of their accounts, we also implemented a simple way of resetting the notificationData and animating the notification via the setTimeout function located inside onMount. onMount is almost equivalent to react's componentDidMount lifecycle. The reason setTimeout was put inside this lifecycle is too ensure that the page has fully been loaded and we have access to document.getElementsByClassName('notification');.

Step 2: Implement the registration flow:

Now that we've gone through how the login was implemented, let's checkout the registration flow. In the routes/accounts/register/index.svelte, we have the snippets below:

// outes/accounts/register/index.svelte

<script>
    import { fly } from 'svelte/transition';
    import { goto } from '$app/navigation';
    import { BASE_API_URI } from '$lib/constants';
    import { notificationData } from '../../../store/notificationStore';

    let email = '',
        fullName = '',
        bio = '',
        username = '',
        password = '',
        confirmPassword = '',
        error = '';
    const submitForm = async () => {
        await fetch(`${BASE_API_URI}/register/`, {
            method: 'POST',
            mode: 'cors',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                user: {
                    email: email,
                    username: username,
                    password: password,
                    bio: bio,
                    full_name: fullName
                }
            })
        })
            .then((response) => {
                if (response.status === 201) {
                    notificationData.set('Registration successful. Login now.');
                    goto('/accounts/login');
                    // console.log('User:', response.json());
                } else if (response.status === 400) {
                    console.log(response.json());
                }
            })
            .catch((error) => {
                error = error;
                console.error('Error:', error);
            });
    };
    const passwordConfirm = () => (password !== confirmPassword ? false : true);
</script>

<svelte:head>
    <title>Register | FullStack Django & SvelteKit</title>
</svelte:head>

<section
    class="container"
    in:fly={{ y: 100, duration: 500, delay: 500 }}
    out:fly={{ duration: 500 }}
>
    <h1>Register</h1>
    {#if error}
        <p class="center error">{error}</p>
    {/if}
    <form class="form" on:submit|preventDefault={submitForm}>
        <input
            bind:value={email}
            type="email"
            aria-label="Email address"
            placeholder="Email address"
            required
        />
        <input
            bind:value={username}
            type="text"
            aria-label="Username"
            placeholder="Username"
            required
        />
        <input
            bind:value={fullName}
            type="text"
            aria-label="Full name"
            placeholder="Full name"
            required
        />
        <input
            bind:value={bio}
            type="text"
            aria-label="Brief bio"
            placeholder="Tell us about yourself..."
            required
        />
        <input
            bind:value={password}
            type="password"
            name="password"
            aria-label="password"
            placeholder="password"
            required
        />
        <input
            bind:value={confirmPassword}
            type="password"
            name="confirmPassword"
            aria-label="Confirm password"
            placeholder="Confirm password"
            required
        />
        {#if confirmPassword}
            <button class="btn" type="submit">Register</button>
        {:else}
            <button class="btn" type="submit" disabled>Register</button>
        {/if}
    </form>
</section>

Enter fullscreen mode Exit fullscreen mode

We did same thing as what we did with the login flow aside from using different api endpoint, updating notificationData to a different string, sending more data to the server, and redirecting to the login page. Also, we didn't use our post function here but using it should produce same output.

That's basically it! We have successfully implemented a robust full stack jwt authentication system! Though we have done some authorizations as well but not intentional enough. We will try to do some intentional authorizations in our bonus article where we'll look into how to update user data and maybe create an endpoint which only users with admin role can assess and manipulate its data! Please, be on a lookup for it!!!

Outro

Enjoyed this article, consider contacting me for a job, something worthwhile or buying a coffee ☕. You can also connect with/follow me on LinkedIn.

Top comments (1)

Collapse
 
ijsucceed profile image
Jeremy Ikwuje

Great article. Though am yet to run it. but I was searching for a use case on Sveltekit Authentication.

How do you then protect pages from users yet to log in using Svelte?