FullStack JWT Auth: Diving into SvelteKit - Profile Update
Tags: javascript, typescript, webdev, svelte
Published: 2022-02-12 21:45:37

## 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:

{% github  Sirneij/django_svelte_jwt_auth  %}

## Live version

This project was deployed on heroku (backend) and vercel (frontend) and its live version can be accessed [here](https://django-sveltekit-jwt-auth.vercel.app/).

## 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:**

```shell
├── 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:

```typescript
<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 script `module`. Inside it is the magical `load` function which does only one thing: get the current user. Were you successful at that? Yes? Put the response in `userResponse` variable and make it available to the rest of the program using `props`. No? Redirect the user to the `login` 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 of `triggerUpdate` asynchronous function. To retrieve and then expose props values, we only did `export let userResponse: User;` and that's it. What about the `triggerUpdate` function? Well, it is a very short function with this definition:

  ```typescript
  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][1], 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:

  ```typescript
  // 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:

  ```typescript
  // 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 ☕](mailto:sirneij@gmail.com). You can also connect with/follow me on [LinkedIn](https://www.linkedin.com/in/idogun-john-nelson/).

[1]: https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace "How whitespace is handled by HTML, CSS, and in the DOM"