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:
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
, orvenv
. 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>
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}`];
}
};
...
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>
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)
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?