Introduction
From the last article, we concluded user registeration and authentication flow. It was surreal to me and I hope you find it intriguing too. In this article (possibly the last in this series), we'll look at how authenticated users can update their details.
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.
Notabene
The project's file structure has considerably been modified from where we left off. Also, most of the scripts have be re-written in TypeScript. The concept of SvelteKit environment variables, TypeScript's interfaces, powerful loader, and a host of others were also implemented. We now have the following file structure for the frontend
project:
├── package.json
├── package-lock.json
├── README.md
├── src
│ ├── app.d.ts
│ ├── app.html
│ ├── components
│ │ ├── Header
│ │ │ ├── Header.svelte
│ │ │ ├── john.svg
│ │ │ └── svelte-logo.svg
│ │ └── Loader
│ │ └── Loader.svelte
│ ├── dist
│ │ └── css
│ │ ├── style.min.css
│ │ └── style.min.css.map
│ ├── global.d.ts
│ ├── lib
│ │ ├── formats
│ │ │ └── formatString.ts
│ │ ├── helpers
│ │ │ ├── buttonText.ts
│ │ │ └── whitespacesHelper.ts
│ │ ├── interfaces
│ │ │ ├── error.interface.ts
│ │ │ ├── user.interface.ts
│ │ │ └── variables.interface.ts
│ │ ├── store
│ │ │ ├── loadingStore.ts
│ │ │ ├── notificationStore.ts
│ │ │ └── userStore.ts
│ │ └── utils
│ │ ├── constants.ts
│ │ └── requestUtils.ts
│ ├── routes
│ │ ├── accounts
│ │ │ ├── login
│ │ │ │ └── index.svelte
│ │ │ ├── register
│ │ │ │ └── index.svelte
│ │ │ └── user
│ │ │ └── [username]-[id].svelte
│ │ ├── index.svelte
│ │ └── __layout.svelte
│ └── sass
│ ├── _about.scss
│ ├── _form.scss
│ ├── _globals.scss
│ ├── _header.scss
│ ├── _home.scss
│ ├── style.scss
│ └── _variables.scss
├── static
│ ├── favicon.png
│ ├── robots.txt
│ ├── svelte-welcome.png
│ └── svelte-welcome.webp
├── svelte.config.js
└── tsconfig.json
Accept my apologies for the incoveniences.
Now, let's get right into adding this functionality.
Update user data
It's a very common thing in web applications to allow users alter their initial data. Let's provide this feature our application's users too.
Create a .svelte
file in routes/accounts/user/
directory. You are at liberty to give it any name you want. However, I'd like to make it dynamic. To make a dynamic page routing in SvelteKit, you use []
(square brackets) with the dynamic field inside and then .svelte
. For our purpose, we want the URL
to have user's username and ID. Therefore, the name of our dynamic
file will be [username]-[id].svelte
. Awesome huh! SvelteKit is truly awesome.
Next, let's purpulate this newly created file with the following content:
<script context="module" lang="ts">
import { variables } from '$lib/utils/constants';
import { getCurrentUser } from '$lib/utils/requestUtils';
import type { Load } from '@sveltejs/kit';
import type { User } from '$lib/interfaces/user.interface';
export const load: Load = async ({ fetch }) => {
const [userRes, errs] = await getCurrentUser(
fetch,
`${variables.BASE_API_URI}/token/refresh/`,
`${variables.BASE_API_URI}/user/`
);
const userResponse: User = userRes;
if (errs.length > 0 && !userResponse.id) {
return {
status: 302,
redirect: '/accounts/login'
};
}
return {
props: { userResponse }
};
};
</script>
<script lang="ts">
import { notificationData } from '$lib/store/notificationStore';
import { scale } from 'svelte/transition';
import { UpdateField } from '$lib/utils/requestUtils';
import { onMount } from 'svelte';
import { nodeBefore } from '$lib/helpers/whitespacesHelper';
export let userResponse: User;
const url = `${variables.BASE_API_URI}/user/`;
onMount(() => {
const notifyEl = document.getElementById('notification') as HTMLElement;
if (notifyEl && $notificationData !== '') {
setTimeout(() => {
notifyEl.classList.add('disappear');
notificationData.update(() => '');
}, 3000);
}
});
let triggerUpdate = async (e: Event) => {
const sibling = nodeBefore(<HTMLElement>e.target);
await UpdateField(sibling.name, sibling.value, url);
};
</script>
<div class="container" transition:scale|local={{ start: 0.7, delay: 500 }}>
{#if userResponse.id}
<h1>
{userResponse.full_name ? userResponse.full_name : userResponse.username} profile
</h1>
{/if}
<div class="user" transition:scale|local={{ start: 0.2 }}>
<div class="text">
<input
aria-label="User's full name"
type="text"
placeholder="User's full name"
name="full_name"
value={userResponse.full_name}
/>
<button class="save" aria-label="Save user's full name" on:click={(e) => triggerUpdate(e)} />
</div>
</div>
<div class="user" transition:scale|local={{ start: 0.3 }}>
<div class="text">
<input
aria-label="User's username"
type="text"
placeholder="User's username"
name="username"
value={userResponse.username}
/>
<button class="save" aria-label="Save user's username" on:click={(e) => triggerUpdate(e)} />
</div>
</div>
<div class="user" transition:scale|local={{ start: 0.4 }}>
<div class="text">
<input
aria-label="User's email"
placeholder="User's email"
type="email"
name="email"
value={userResponse.email}
/>
<button class="save" aria-label="Save user's email" on:click={(e) => triggerUpdate(e)} />
</div>
</div>
<div class="user" transition:scale|local={{ start: 0.5 }}>
<div class="text">
<input
aria-label="User's bio"
placeholder="User's bio"
type="text"
name="bio"
value={userResponse.bio}
/>
<button class="save" aria-label="Save user's bio" on:click={(e) => triggerUpdate(e)} />
</div>
</div>
<div class="user" transition:scale|local={{ start: 0.6 }}>
<div class="text">
<input
aria-label="User's date of birth"
type="date"
name="birth_date"
placeholder="User's date of birth"
value={userResponse.birth_date}
/>
<button
class="save"
aria-label="Save user's date of birth"
on:click={(e) => triggerUpdate(e)}
/>
</div>
</div>
</div>
Whoa!!! That's a lot, man! Errm... It's but let's go through it.
Module
script section: We started the file by creating a scriptmodule
. Inside it is the magicalload
function which does only one thing: get the current user. Were you successful at that? Yes? Put the response inuserResponse
variable and make it available to the rest of the program usingprops
. No? Redirect the user to thelogin
page. Pretty simple huh? I think it's.Second script section: This section's snippets are pretty basic. The major things to note are the retrieval of the props made available by our
module
, and the definition oftriggerUpdate
asynchronous function. To retrieve and then expose props values, we only didexport let userResponse: User;
and that's it. What about thetriggerUpdate
function? Well, it is a very short function with this definition:
let triggerUpdate = async (e: Event) => {
const sibling = nodeBefore(<HTMLElement>e.target);
await UpdateField(sibling.name, sibling.value, url);
};
It accepts an Event
object, and using it, determines the value and name of the previous sibling (an input) using a custom function, named nodeBefore
. Why not use (<HTMLElement>e.target).previousSibling
instead? This MDN article, How whitespace is handled by HTML, CSS, and in the DOM, explained it. As a matter of fact, the snippets in $lib/helpers/whitespacesHelper.ts
were ported from the JavaScript snippets made available on the article. Then, we called on UpdateField
function, having this content:
// lib -> utils -> requestUtils.ts
...
export const UpdateField = async (
fieldName: string,
fieldValue: string,
url: string
): Promise<[object, Array<CustomError>]> => {
const userObject: UserResponse = { user: {} };
let formData: UserResponse | any;
if (url.includes('/user/')) {
formData = userObject;
formData['user'][`${fieldName}`] = fieldValue;
} else {
formData[`${fieldName}`] = fieldValue;
}
const [response, err] = await handlePostRequestsWithPermissions(fetch, url, formData, 'PATCH');
if (err.length > 0) {
console.log(err);
return [{}, err];
}
console.log(response);
notificationData.set(`${formatText(fieldName)} has been updated successfully.`);
return [response, []];
};
This function just prepares the data to be sent to the server and then calls on the function that really sends it: handlePostRequestsWithPermissions
. handlePostRequestsWithPermissions
is a multipurpose or maybe generic function that can be used to make any post requests that require some permissions. Though written to work for this project, it can be modified to suit other projects' needs. It's content is:
// lib -> utils -> requestUtils.ts
...
export const handlePostRequestsWithPermissions = async (
fetch,
targetUrl: string,
body: unknown,
method = 'POST'
): Promise<[object, Array<CustomError>]> => {
const res = await fetch(`${variables.BASE_API_URI}/token/refresh/`, {
method: 'POST',
mode: 'cors',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
refresh: `${browserGet('refreshToken')}`
})
});
const accessRefresh = await res.json();
const jres = await fetch(targetUrl, {
method: method,
mode: 'cors',
headers: {
Authorization: `Bearer ${accessRefresh.access}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
if (method === 'PATCH') {
if (jres.status !== 200) {
const data = await jres.json();
console.error(`Data: ${data}`);
const errs = data.errors;
console.error(errs);
return [{}, errs];
}
return [jres.json(), []];
} else if (method === 'POST') {
if (jres.status !== 201) {
const data = await jres.json();
console.error(`Data: ${data}`);
const errs = data.errors;
console.error(errs);
return [{}, errs];
}
return [jres.json(), []];
}
};
...
It currently handles POST
and PATCH
requests but as said earlier, it can be extended to accommodate PUT
, DELETE
, and other "unsafe" HTTP verbs.
The triggerUpdate
method was bound to the click event of the button
element attached to each input element on the form. When you focus on the input element, a disk-like image pops up at right-most part the the input and clicking it triggers triggerUpdate
which in-turn calls on updateField
, and then handlePostRequestsWithPermissions
.
[Heaves a sigh of relief], that's basically it! If I get less busy, I might still work on this project to make it more than just an authentication system. Contributions are also welcome. Kindly drop comments if there's anything you wanna let me know. See y'all...
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 (5)
Considering the Load function that redirect the user incase no user is found. My question then is, do I have to always make a request to the backend api to protect pages from non authenticated users?
Great question. You don't have to do that. A better way of implementing protected pages without having to go back to the server is to store user info in a special SvelteKit store,
session
. The value gets destroyed as soon as you destroy the components or the session expires.Can you show how or point to a resource?
Sadly, there is no much resource online for that aside the example in Real World Svelte app. But I will try to explain it here. I will assume you are using TypeScript and that my user type is like:
If this type declaration is in
src/lib/types/user.interface.ts
, I can then import it toapp.d.ts
and make theSession
typed as follows:Then in your login page, you first ensure that the user is not logged in by having this load function:
isEmpty
is a function that checks whether or not an object is empty. An implementation is:In the
load
function, we ensure that a logged in user can't re-login. In the other script tag in the login component and immediately after a user successfully logs in, you can then set the user in the session:Since
session
is a store shipped with SvelteKit, we used the auto-subscription sign,$
to access and set the user properties to the response gotten from the backend after the user signs in. Which means the backend must return the user's data as declared in:After setting this, on every protected page, you can have a load function such as:
This time, it doesn't go back to the backend to fetch user's data. Instead, it gets the user data stored in the
session
store and validates the requests made by the user. Thesession
store can be imported like:Hope this helps.
Yeah, thanks for taking the time. Though I don't write Typescript, but I can understand what you're trying to do.