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 have built so far. Let's incept!
Source code
The source code for this series is hosted on GitHub via:
rust-auth
A full-stack secure and performant authentication system using rust's Actix web and JavaScript's SvelteKit.
This application resulted from this series of articles and it's currently live here(I have disabled the backend from running live).
Run locally
You can run the application locally by first cloning it:
~/$ git clone https://github.com/Sirneij/rust-auth.git
After that, change directory into each subdirectory: backend
and frontend
in different terminals. Then following the instructions in each subdirectory to run them.
Implementation
You can get the overview of the code for this article on github.
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). 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:
~/rust-auth/backend$ cargo add actix-cors
Next, let's whitelist our frontend application to share our backend resources:
// 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 from working. We then allowed the allowed origins to make GET
, POST
, PATCH
, and DELETE
requests. actix-cors 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 for our styling and we need to install and set it up. The steps to achieve that were enumerated here.
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 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. Install it as a dev package:
~/rust-auth/frontend$ npm i -D @vitejs/plugin-basic-ssl
Then open up frontend/vite.config.ts
:
//
...
import basicSsl from '@vitejs/plugin-basic-ssl';
...
plugins: [basicSsl(), sveltekit()],
...
Also, proceed to your package.json
file and replace the dev
under scripts
with:
// 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
:
<!-- 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:
<!--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
:
// 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:
// 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. 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:
// 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
:
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:
// 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:
// 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 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:
<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:
<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 made your routes/+layout.svelte
look like the one in the repo, you should see this image:
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.
Outro
Enjoyed this article? I'm a Software Engineer and Technical Writer actively seeking new opportunities, particularly in areas related to web security, finance, healthcare, and education. If you think my expertise aligns with your team's needs, let's chat! You can find me on LinkedIn and Twitter.
If you found this article valuable, consider sharing it with your network to help spread the knowledge!
Top comments (0)