Authentication system using rust (actix-web) and sveltekit - CORS and Frontend Integration
Tags: javascript, svelte, tailwindcss, tutorial
Published: 2023-04-26 15:46:16

## Introduction

So far so good, we have spawned some amazing systems that, "fictitiously", can register, verify, log in and out such a user. As rightly pointed out, this claim is still fictitious since there is currently no automated test (which will be added at a later stage) or an application that truly uses it. This article is about the latter point. We will be building our frontend application to communicate with the backend services we had built so far. Let's incept!

## Source code

The source code for this series is hosted on GitHub via:

{% github Sirneij/rust-auth %}

## Implementation

> You can get the overview of the code for this article on [github][1].

### Step 1: Install and Configure CORS

Right now, it's impossible to connect another standalone application, not running on the same address, to our current backend service successfully with some errors. The reason for that is what is termed [CORS (Cross-Origin Resource Sharing)][2]. Simply put, it is a security system that prevents systems of different origins (addresses) from sharing resources without being specifically allowed to.

In our current application configuration, we have not whitelisted any other application to share its resources. Let's do that with yet another awesome crate in the Actix Web ecosystem, [actix-cors][3]:

```shell
~/rust-auth/backend$ cargo add actix-cors
```

Next, let's whitelist our frontend application to share our backend resources:

```rust
// src/startup.rs
...
async fn run(
    listener: std::net::TcpListener,
    db_pool: sqlx::postgres::PgPool,
    settings: crate::settings::Settings,
) -> Result<actix_web::dev::Server, std::io::Error> {
    ...
    let server = actix_web::HttpServer::new(move || {
    actix_web::App::new()
        ...
        .wrap(
            actix_cors::Cors::default()
                .allowed_origin(&settings.frontend_url)
                .allowed_methods(vec!["GET", "POST", "PATCH", "DELETE"])
                .allowed_headers(vec![
                    actix_web::http::header::AUTHORIZATION,
                    actix_web::http::header::ACCEPT,
                ])
                .allowed_header(actix_web::http::header::CONTENT_TYPE)
                .expose_headers(&[actix_web::http::header::CONTENT_DISPOSITION])
                .supports_credentials()
                .max_age(3600),
            )
        ...
}
...
```

We allowed requests from `settings.frontend_url`, one of the settings we created before now. Ensure you set the value to the address of your front-end application. Mine was set to `https://localhost:3000`. Notice that I used HTTPS protocol because if we don't, the session configuration made in the previous article will prevent it to work. We then allowed the allowed origins to make `GET`, `POST`, `PATCH`, and `DELETE` requests. [actix-cors][3] add `OPTIONS` automatically for us. We also supported sending authentication credentials such as cookies. That's the last bit we need to do at the backend for now.

### Step 2: Install and setup TailwindCSS for SvelteKit

As stated, we will be using [TailwindCSS v3.3][4] for our styling and we need to install and set it up. The steps to achieve that were enumerated [here][5].

> Ensure you get that done before proceeding so that we will be on the same page.

Having installed Tailwind, let's start making our front-end application.

### Step 3: Serve SvelteKit dev with HTTPS

As earlier emphasized, we need our frontend application to be served via HTTPS protocol for our backend to recognize it. However, by default, [vite][6] serves apps in development via the HTTP protocol instead. We need to change this. Because Vite knew this scenario would happen, they made a package called [plugin-basic-ssl][7]. Install it as a dev package:

```shell
~/rust-auth/frontend$ npm i -D @vitejs/plugin-basic-ssl
```

Then open up `frontend/vite.config.ts`:

```typescript
// 
...
import basicSsl from '@vitejs/plugin-basic-ssl';
...
plugins: [basicSsl(), sveltekit()],
...
```

Also, proceed to your `package.json` file and replace the `dev` under `scripts` with:

```json
// frontend/package.json
{
...
    "dev": "vite dev --port 3000 --https",
...
}
```

Now, whenever you run your application using `npm run dev`, it will be served via HTTPS!

### Step 4: Build user registration UI and logic

Since that's done, let's make our user registration UI.

SvelteKit, like NextJS, uses filesystem-based routing. This means the path of any `+page.svelte` in the `routes` directory is automatically considered a route. If, for instance, you have `src/auth/register/+page.svelte`, your register URL in the browser will be `https://localhost:3000/auth/register`. This example is what we'll use. Open up `src/auth/register/+page.svelte`:

```html
<!-- src/auth/register/+page.svelte -->
<script lang="ts">
    import { goto } from '$app/navigation';
    import { page } from '$app/stores';
    import { apiResponse } from '$lib/stores/api.response.store';
    import { loading } from '$lib/stores/loading.store';
    import { notification } from '$lib/stores/notification.store';
    import { BASE_API_URI, happyEmoji } from '$lib/utils/constant';
    import {
        isValidEmail,
        isValidPasswordMedium,
        isValidPasswordStrong
    } from '$lib/utils/helpers/input.validation';
    import { post } from '$lib/utils/requests/posts.requests';
    import type { ApiResponse, CustomError } from '$lib/utils/types';
    import { flip } from 'svelte/animate';
    import { scale } from 'svelte/transition';
    let email = '',
        password = '',
        first_name = '',
        last_name = '',
        confirmPassword = '',
        errors: Array<CustomError> = [],
        fieldsError = { email: '', password: '', first_name: '', last_name: '', confirmPassword: '' },
        isValid = false;
    const handleRegister = async () => {
        isValid = true;
        if (!isValidEmail(email)) {
            isValid = false;
            fieldsError.email = 'That email address is invalid.';
        } else {
            fieldsError.email = '';
        }
        if (!isValidPasswordMedium(password)) {
            isValid = false;
            fieldsError.password =
                'Password is not valid. Password must contain six characters or more and has at least one lowercase and one uppercase alphabetical character or has at least one lowercase and one numeric character or has at least one uppercase and one numeric character.';
        } else {
            fieldsError.password = '';
        }
        if (confirmPassword.trim() !== password.trim()) {
            isValid = false;
            fieldsError.confirmPassword = 'Password does not match.';
        } else {
            fieldsError.confirmPassword = '';
        }
        if (isValid) {
            loading.setLoading(true, 'Please wait while we register you...');
            const [res, err] = await post($page.data.fetch, `${BASE_API_URI}/users/register/`, {
                first_name: first_name,
                last_name: last_name,
                email: email,
                password: password
            });
            if (err.length > 0) {
                loading.setLoading(false);
                errors = err;
            } else {
                loading.setLoading(false);
                const response: ApiResponse = res;
                $notification = {
                    message: `You have successfully registered ${happyEmoji}...`,
                    borderColor: `border-green-300 bg-green-100`,
                    textTopColor: 'text-green-800',
                    textBottomColor: 'text-green-600'
                };
                $apiResponse = {
                    message: response.message ? response.message : '',
                    status: response.status ? response.status : ''
                };
                await goto('/auth/confirming');
            }
        }
    };
</script>

<div class="flex items-center justify-center h-[60vh]">
    <form
        class="w-11/12 md:w-2/3 lg:w-1/3 rounded-xl flex flex-col items-center bg-slate-800 py-4"
        on:submit|preventDefault={handleRegister}
    >
        <h1 class="text-center text-2xl font-bold text-sky-400 mb-6">Register</h1>

        {#if errors}
            {#each errors as error (error.id)}
                <p
                    class="text-center text-rose-600"
                    transition:scale|local={{ start: 0.7 }}
                    animate:flip={{ duration: 200 }}
                >
                    {error.error}
                </p>
            {/each}
        {/if}

        <div class="w-3/4 mb-2">
            <input
                type="email"
                name="email"
                id="email"
                bind:value={email}
                class="w-full text-sky-500 placeholder:text-slate-600 border-none focus:ring-0 bg-main-color focus:outline-none py-2 px-3 rounded"
                placeholder="Email address"
                required
            />
            {#if fieldsError.email}
                <span class="text-center text-rose-600" transition:scale|local={{ start: 0.7 }}>
                    {fieldsError.email}
                </span>
            {/if}
        </div>

        <div class="w-3/4 mb-2">
            <input
                type="text"
                name="first_name"
                bind:value={first_name}
                id="first-name"
                class="w-full text-sky-500 placeholder:text-slate-600 border-none focus:ring-0 bg-main-color focus:outline-none py-2 px-3 rounded"
                placeholder="First name"
                required
            />
        </div>
        <div class="w-3/4 mb-2">
            <input
                type="text"
                name="last_name"
                id="last-name"
                bind:value={last_name}
                class="w-full text-sky-500 placeholder:text-slate-600 border-none focus:ring-0 bg-main-color focus:outline-none py-2 px-3 rounded"
                placeholder="Last name"
                required
            />
        </div>

        <div class="w-3/4 mb-2">
            <input
                type="password"
                name="password"
                bind:value={password}
                id="password"
                class="w-full text-sky-500 placeholder:text-slate-600 border-none focus:ring-0 bg-main-color focus:outline-none py-2 px-3 rounded"
                placeholder="Password"
                required
            />
            {#if fieldsError.password}
                <span class="text-center text-rose-600" transition:scale|local={{ start: 0.7 }}>
                    {fieldsError.password}
                </span>
            {/if}
        </div>

        <div class="w-3/4 mb-6">
            <input
                type="password"
                name="confirm-password"
                bind:value={confirmPassword}
                id="confirm-password"
                class="w-full text-sky-500 placeholder:text-slate-600 border-none focus:ring-0 bg-main-color focus:outline-none py-2 px-3 rounded"
                placeholder="Confirm password"
                required
            />
            {#if fieldsError.confirmPassword}
                <span class="text-center text-sm text-rose-600" transition:scale|local={{ start: 0.7 }}>
                    {fieldsError.confirmPassword}
                </span>
            {/if}
        </div>

        <div class="w-3/4">
            <button
                type="submit"
                class="py-2 bg-sky-800 w-full rounded text-blue-50 font-bold hover:bg-sky-700"
            >
                Create account
            </button>
        </div>
        <div class="w-3/4 flex flex-row justify-center mt-1">
            <span class="text-sm text-sky-400">
                Already have an account?<a
                    href="/auth/login"
                    class="ml-2 text-slate-400 underline hover:text-sky-400"
                >
                    Login.
                </a>
            </span>
        </div>
    </form>
</div>
```

That's a lot! A closer look will simplify things as usual. We are starting with the `script` tag. We opted to use `TypeScript` using the `lang='ts'` attribute. We then imported `goto` to programmatically browse to another page. It was used to progress to `auth/confirming` route after a user successfully registers. `src/routes/auth/confirming/+page.svelte` is simple:

```html
<!--src/routes/auth/confirming/+page.svelte-->
<div class="flex items-center justify-center h-[60vh]">
    <div
        class="w-11/12 md:w-2/3 lg:w-1/3 rounded-xl flex flex-col items-center divide-y bg-slate-800 py-4"
    >
        <h1 class="text-center text-2xl font-bold text-emerald-600 mb-2">Email sent!</h1>

        <p class="text-emerald-900 text-justify px-4 py-4">
            Lorem, ipsum dolor sit amet consectetur adipisicing elit. Accusamus autem quod minima deleniti
            esse ratione consectetur, aliquam commodi minus voluptates nobis, ipsam aperiam molestiae,
            quis laboriosam tempore corporis magni ad?
        </p>
    </div>
</div>
```

We will change the text in a later article.

We also imported `page`, a built-in store with all the available data about the current page. One of the data it made available was `fetch`, an extended version of the fetch API, provided by SvelteKit. I exposed it in `src/routes/+layout.ts`:

```typescript
// src/routes/+layout.ts
import type { LayoutLoad } from './$types';

export const load: LayoutLoad = async ({ fetch, url }) => {
    return { fetch, url: url.pathname };
};
```

Any data made available in `layout.ts` is available to all pages that are a superset of such a layout in SvelteKit. We also brought in three custom stores namely: `apiResponse`, `loading` and `notification`. Store, in Svelte, is a built-in equivalent of Redux in React. It is however simpler:

```typescript
// frontend/src/lib/stores/api.response.store.ts
import { writable } from 'svelte/store';

export const apiResponse = writable({ message: '', status: '' });

// frontend/src/lib/stores/loading.store.ts

import type { Loading } from '$lib/utils/types';
import { writable, type Writable } from 'svelte/store';

const newLoading = () => {
    const { subscribe, update, set }: Writable<Loading> = writable({
        status: 'IDLE',
        message: ''
    });

    const setNavigate = (isNavigating: boolean) => {
        update(() => {
            return {
                status: isNavigating ? 'NAVIGATING' : 'IDLE',
                message: ''
            };
        });
    };

    const setLoading = (isLoading: boolean, message = '') => {
        update(() => {
            return {
                status: isLoading ? 'LOADING' : 'IDLE',
                message: isLoading ? message : ''
            };
        });
    };

    return { subscribe, update, set, setNavigate, setLoading };
};

export const loading = newLoading();

// frontend/src/lib/stores/notification.store.ts
import { writable } from 'svelte/store';

export const notification = writable({
    message: '',
    borderColor: '',
    textTopColor: '',
    textBottomColor: ''
});
```

They are all writable stores. This means that we can set, get, and update them. There are [other types of Svelte stores][8]. Apart from `loading`, the stores were created using one-liners. As for loading, it appeared complex because we decided to make available custom methods to interact with the store.

Next, we imported some constants: `BASE_API_URI` and `happyEmoji`. `BASE_API_URI` is the URL of our backend application. It was introduced into our frontend using the environment variable:

```typescript
// frontend/src/lib/utils/constant.ts
import type { Topic } from './types';

export const BASE_API_URI = import.meta.env.DEV
    ? import.meta.env.VITE_BASE_API_URI_DEV
    : import.meta.env.VITE_BASE_API_URI_PROD;

export const danceEmoji = '💃';
export const angryEmoji = '😠';
export const sadEmoji = '😔';
export const happyEmoji = '😊';
export const thinkingEmoji = '🤔';
export const eyesRoll = '🙄';

export const redColor = '#dc3545';
export const greenColor = '#198754';
export const yellowColor = '#ffc107';
export const cyanColor = '#0dcaf0';

export const topics: Array<Topic> = [
    {
        id: 1,
        title: 'Backend Introduction',
        url: 'https://dev.to/sirneij/full-stack-authentication-system-using-rust-actix-web-and-sveltekit-1cc6'
    },
    {
        id: 2,
        title: 'Database and Redis Configuration',
        url: 'https://dev.to/sirneij/authentication-system-using-rust-actix-web-and-sveltekit-db-and-redis-config-38fp'
    },
    {
        id: 3,
        title: 'User Registration',
        url: 'https://dev.to/sirneij/authentication-system-using-rust-actix-web-and-sveltekit-user-registration-580h'
    },
    {
        id: 4,
        title: 'User session, Login and Logout',
        url: 'https://dev.to/sirneij/authentication-system-using-rust-actix-web-and-sveltekit-login-and-logout-1eb9'
    },
    {
        id: 5,
        title: 'CORS and Frontend Integration',
        url: 'https://dev.to/sirneij/authentication-system-using-rust-actix-web-and-sveltekit-login-and-logout-1eb9'
    }
];
```

At the top, we dynamically determine our current environment and appropriately set `BASE_API_URI`. In Vite, your environment variable must start with `VITE_` to be recognized. I used this `.env`:

```env
VITE_BASE_API_URI_DEV=http://127.0.0.1:5000
VITE_BASE_API_URI_PROD=
```

There are also other constants there. Moving on, I also imported some custom data validation logic which was defined in `$lib/utils/helpers/input.validation`. Do you wonder where the `$lib` emanated from? I do too. Well, it turns out in SvelteKit, to prevent ugly import paths, whenever your file lives in `src/lib/` directory, it automatically gets the `$lib` prefix. I use it a lot! The validation logic looks like this:

```typescript
// frontend/src/lib/utils/helpers/input.validation.ts
/**
 * Validates an email field
 * @file lib/utils/helpers/input.validation.ts
 * @param {string} email - The email to validate
 */
export const isValidEmail = (email: string) => {
    const EMAIL_REGEX =
        /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/;
    return EMAIL_REGEX.test(email.trim());
};
/**
 * Validates a strong password field
 * @file lib/utils/helpers/input.validation.ts
 * @param {string} password - The password to validate
 */
export const isValidPasswordStrong = (password: string) => {
    const strongRegex = new RegExp('^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})');

    return strongRegex.test(password.trim());
};
/**
 * Validates a medium password field
 * @file lib/utils/helpers/input.validation.ts
 * @param {string} password - The password to validate
 */
export const isValidPasswordMedium = (password: string) => {
    const mediumRegex = new RegExp(
        '^(((?=.*[a-z])(?=.*[A-Z]))|((?=.*[a-z])(?=.*[0-9]))|((?=.*[A-Z])(?=.*[0-9])))(?=.{6,})'
    );

    return mediumRegex.test(password.trim());
};
```

Just a file having a lot of regexes to validate the correct email address and detect whether your password is strong or medium in strength!!!

Next, we have a custom `post` function that abstracts away some stuff:

```typescript
// frontend/src/lib/utils/requests/posts.requests.ts
import type {
	ApiResponse,
	CustomError,
	LoginRequestBody,
	PasswordChange,
	RegenerateTokenRequestBody,
	RegisterRequestBody,
	User
} from '../types';

/**
 * Handle all POST-related requests.
 * @file lib/utils/requests/post.requests.ts
 * @param {typeof fetch} sveltekitFetch - Fetch object from sveltekit
 * @param {typeof fetch} url - The URL whose resource will be fetched.
 * @param {LoginRequestBody |  RegisterRequestBody | RegenerateTokenRequestBody | FormData |undefined} body - Body of the POST request
 * @param {RequestCredentials} [credentials='omit'] - Request credential. Defaults to 'omit'.
 * @param {'POST' | 'PUT' | 'PATCH' | 'DELETE'} [method='POST'] - Request method. Defaults to 'POST'.
 */
export const post = async (
    sveltekitFetch: typeof fetch,
    url: string,
    body:
        | LoginRequestBody
        | RegisterRequestBody
        | RegenerateTokenRequestBody
        | FormData
        | PasswordChange
        | undefined,
    credentials: RequestCredentials = 'omit',
    method: 'POST' | 'PUT' | 'PATCH' | 'DELETE' = 'POST'
): Promise<[object, Array<CustomError>]> => {
    try {
        const headers = { 'Content-Type': '' };
        const requestInitOptions: RequestInit = {
            method: method,
            mode: 'cors',
            credentials: credentials
        };
        if (!(body instanceof FormData)) {
            headers['Content-Type'] = 'application/json';
            requestInitOptions['headers'] = headers;
            if (body !== undefined) {
                requestInitOptions.body = JSON.stringify(body);
            }
        } else if (body instanceof FormData) {
            headers['Content-Type'] = 'multipart/form-data';
            if (body !== undefined) {
                requestInitOptions['body'] = body;
            }
        } else if (body === undefined && method !== 'DELETE') {
            const errors: Array<CustomError> = [
                { error: 'Unless you are performing DELETE operation, you must have a body.', id: 0 }
            ];
            return [{}, errors];
        }

        const res = await sveltekitFetch(url, requestInitOptions);

        if (!res.ok) {
            const response = await res.json();
            const errors: Array<CustomError> = [];

            errors.push({ error: response.error, id: 0 });

            return [{}, errors];
        }

        const res_json = await res.json();

        let response: ApiResponse | User;

        if (res_json['message']) {
            response = { message: res_json['message'], status: res_json['status'] };
        } else {
            response = {
                id: res_json['id'],
                email: res_json['email'],
                first_name: res_json['first_name'],
                last_name: res_json['last_name'],
                is_staff: res_json['is_staff'],
                thumbnail: res_json['thumbnail'],
                is_superuser: res_json['is_superuser']
            };
        }

        return [response, []];
    } catch (error) {
        console.error(`Error outside: ${error}`);
        const err = `${error}`;
        const errors: Array<CustomError> = [
            { error: 'An unknown error occurred.', id: 0 },
            { error: err, id: 1 }
        ];
        return [{}, errors];
    }
};
```

We are working with TYPEscript, so the types are expected and they all live in `frontend/src/lib/utils/types.ts`. Our `post` uses [SvelteKit fetch][9] to perform "data-sensitive" requests: `'POST' | 'PUT' | 'PATCH' | 'DELETE'`. It detects the type of body you are sending and serializes them accordingly before performing them. We then use Svelte's built-in animate package to provide a better user experience to our users.

In svelte/sveltekit, you don't need ref, setState, or hooks to bind a variable to an input. You just need to declare the variable and "bind" it to such an input element. For instance, if you have a variable, `email`, and you want it bounded to an input, just do:

```html
<script lang="ts">
  let email = '';
</script>

<input type="email" bind:value={email}>
```

And whatever the value a user types in, your `email` variable holds it automatically!!!

We used this to build out our form and perform some validations. There is a function we need to talk about briefly before we draw the curtains for this article, `handleRegister`. It is responsible for actually initiating the registration request. It also does some client-side validations.

> NOTE: Only client-side validation is a bad idea. Users can turn off JavaScript and they will fail. Ensure you complement such validations with server-side ones. It's important for best practices.

Notice that on the `form` tag, I put something like:

```html
<form ... on:submit|preventDefault={handleRegister}>
```

The `|preventDefault` helps ensure that submitting the form doesn't cause a browser refresh.

If your tailwind configuration was right and you make your `routes/+layout.svelte` look like the one in the repo, you should see this image:

![Registration Page](https://i.ibb.co/sgdpmBY/Screenshot-2023-04-26-at-4-30-51-PM.png "Complete registration page")

Did you get here? Congratulations!!! You just made your first real request to the backend!! See you in the next article where we'll enable login and logout in the front-end.

[1]: https://github.com/Sirneij/rust-auth/commit/5d5748113360bf14ec903eff7c754db09f048273 "Cors and frontend"
[2]: https://developer.mozilla.org/en-US/docs/Glossary/CORS "CORS (Cross-Origin Resource Sharing)"
[3]: https://crates.io/crates/actix-cors "Actix CORS"
[4]: https://tailwindcss.com/ "A utility-first CSS framework packed with classes like flex, pt-4, text-center and rotate-90 that can be composed to build any design, directly in your markup."
[5]: https://tailwindcss.com/docs/guides/sveltekit "TailwindCSS for SvelteKit"
[6]: https://vitejs.dev/ "Vite Next Generation Frontend Tooling"
[7]: https://github.com/vitejs/vite-plugin-basic-ssl "Basic https setup using an automatically generated self-signed certificate"
[8]: https://learn.svelte.dev/tutorial/readable-stores "Readable stores"
[9]: https://kit.svelte.dev/docs/load#making-fetch-requests "Making fetch requests "