Introduction
I think it is great for us to see, in action, the backend code we've been writing. This intermittent switch tends to refresh me as I will focus on writing a different set of languages and doing something more of aesthetics than extreme logic. If you have a different opinion, kindly drop it in the comment section.
We will build out the user registration and activation pages in this article. Before then, we will provide the frontend URL for the backend since it's needed to correctly construct the email.
Source code
The source code for this series is hosted on GitHub via:
go-auth
This repository accompanies a series of tutorials on session-based authentication using Go at the backend and JavaScript (SvelteKit) on the front-end.
It is currently live here (the backend may be brought down soon).
To run locally, kindly follow the instructions in each subdirectory.
Implementation
Step 1: Set the frontend URL in the backend
We need to provide the base URL of a front-end application since our backend needs it. As usual, we'll set FRONTEND_URL
in our .env
file and load it into our app's config
type:
// cmd/api/config.go
...
func updateConfigWithEnvVariables() (*config, error) {
...
// Frontend URL
flag.StringVar(&cfg.frontendURL, "frontend-url", os.Getenv("FRONTEND_URL"), "Frontend URL")
...
}
We then add frontendURL
to the config
type in cmd/api/main.go
:
// cmd/api/main.go
...
type config struct {
...
frontendURL string
...
}
...
With that, let's build the front end.
Step 2: CSS and layout
We will not delve into explaining the CSS of the code but we need to be aware that there is a styles.css
file in frontend/src/lib/css
folder. If you followed the previous series, you might know why we put our files in the lib
folder. With its help, files in it can be imported via the $lib
alias. It will contain library code (utilities and components).
Next, we will create a frontend/src/routes/+layout.svelte
:
<script>
import Footer from '$lib/components/Footer.svelte';
import Header from '$lib/components/Header.svelte';
import Transition from '$lib/components/Transition.svelte';
import '$lib/css/styles.min.css';
/** @type {import('./$types').PageData} */
export let data;
</script>
<Transition key={data.url} duration={600}>
<Header />
<slot />
<Footer />
</Transition>
It's pretty simple. We imported some components: Footer
, Header
, and Transition
. We also needed some data for the page: url
. This is needed for the Transition
component:
<!-- frontend/src/lib/components/Transition.svelte -->
<script>
import { slide } from 'svelte/transition';
/** @type {string} */
export let key;
/** @type {number} */
export let duration = 300;
</script>
{#key key}
<div in:slide={{ duration, delay: duration }} out:slide={{ duration }}>
<slot />
</div>
{/key}
url
is used as a key because it needs to be unique. We utilized Svelte's nifty transition package to provide a good page transition.
Next is Header
:
<!-- frontend/src/lib/components/Header.svelte -->
<script>
import { page } from '$app/stores';
import Developer from '$lib/img/hero-image.png';
</script>
<header class="header">
<div class="header-container">
<div class="header-left">
<div class="header-crafted-by-container">
<a href="https://github.com/Sirneij">
<span>Developed by</span><img src={Developer} alt="John Owolabi Idogun" />
</a>
</div>
</div>
<div class="header-right">
<div class="header-nav-item" class:active={$page.url.pathname === '/'}>
<a href="/">home</a>
</div>
<div class="header-nav-item" class:active={$page.url.pathname === '/auth/login'}>
<a href="/auth/login">login</a>
</div>
<div class="header-nav-item" class:active={$page.url.pathname === '/auth/register'}>
<a href="/auth/register">register</a>
</div>
</div>
</div>
</header>
In our CSS, there is .active
class that highlights the active link. We used Svelte's class directive to do that, e.g. class:active={$page.url.pathname === '/auth/register'}
. Next is Footer
:
<!-- frontend/src/lib/components/Footer.svelte -->
<script>
import Developer from '$lib/img/hero-image.png';
const year = new Date().getFullYear();
</script>
<footer class="footer-container">
<div class="footer-branding-container">
<div class="footer-branding">
<a class="footer-crafted-by-container" href="https://github.com/Sirneij">
<span>Developed by</span>
<img class="footer-branded-crafted-img" src={Developer} alt="John Owolabi Idogun" />
</a>
<span class="footer-copyright">© {year} John Owolabi Idogun. All Rights Reserved.</span>
</div>
</div>
</footer>
Just a basic HTML.
Back to the +layout.svelte
file. We exported a data
variable. This variable was exported from +layout.js
:
// frontend/src/routes/+layout.js
/** @type {import('./$types').LayoutLoad} */
export async function load({ fetch, url }) {
return { fetch, url: url.pathname };
}
Next, let's build the home page:
<!-- frontend/src/routes/+page.svelte -->
<script>
import Developer from '$lib/img/hero-image.png';
</script>
<div class="hero-container">
<div class="hero-logo"><img src={Developer} alt="John Owolabi Idogun" /></div>
<h3 class="hero-subtitle subtitle">
This application is the demonstration of a series of tutorials on session-based authentication
using Go at the backend and JavaScript (SvelteKit) on the front-end.
</h3>
<div class="hero-buttons-container">
<a class="button-dark" href="https://dev.to/sirneij/series/23239" data-learn-more>Learn more</a>
</div>
</div>
Again, just a simple HTML page. We also loaded an image in the script tag.
Our page should look like this now:
Step 3: User registration page
Having built out the layout of the pages, it's time to look into the user registration page:
<!-- frontend/src/routes/auth/register/+page.svelte -->
<script>
import { applyAction, enhance } from '$app/forms';
import { receive, send } from '$lib/utils/helpers';
import { scale } from 'svelte/transition';
/** @type {import('./$types').ActionData} */
export let form;
/** @type {import('./$types').SubmitFunction} */
const handleRegister = async () => {
return async ({ result }) => {
await applyAction(result);
};
};
</script>
<div class="container">
<form class="content" action="?/register" method="POST" use:enhance={handleRegister}>
<h1 class="step-title title">Register</h1>
{#if form?.errors}
{#each form?.errors as error (error.id)}
<h4
class="step-subtitle warning"
in:receive={{ key: error.id }}
out:send={{ key: error.id }}
>
{error.error}
</h4>
{/each}
{/if}
<div class="input-box">
<span class="label">Email:</span>
<input class="input" type="email" name="email" placeholder="Email address" />
</div>
{#if form?.fieldsError && form?.fieldsError.email}
<p class="warning" transition:scale|local={{ start: 0.7 }}>
{form?.fieldsError.email}
</p>
{/if}
<div class="input-box">
<span class="label">First name:</span>
<input class="input" type="text" name="first_name" placeholder="First name" />
</div>
<div class="input-box">
<span class="label">Last name:</span>
<input class="input" type="text" name="last_name" placeholder="Last name" />
</div>
<div class="input-box">
<span class="label">Password:</span>
<input class="input" type="password" name="password" placeholder="Password" />
</div>
{#if form?.fieldsError && form?.fieldsError.password}
<p class="warning" transition:scale|local={{ start: 0.7 }}>
{form?.fieldsError.password}
</p>
{/if}
<div class="input-box">
<span class="label">Confirm password:</span>
<input class="input" type="password" name="confirm_password" placeholder="Password" />
</div>
{#if form?.fieldsError && form?.fieldsError.confirmPassword}
<p class="warning" transition:scale|local={{ start: 0.7 }}>
{form?.fieldsError.confirmPassword}
</p>
{/if}
<div class="btn-container">
<button class="button-dark">Register</button>
<p>Already registered? <a href="/auth/login">Login here</a>.</p>
</div>
</form>
</div>
All our authentication-related routes will be in the routes/auth/
folder and for the register
route, it is located in routes/auth/register/+page.svelte
. The page looks like this:
The entire code should look very familiar because it is just HTML. However, we rely primarily on SvelteKit's form actions which is a preferred way to submit form data to the server. The action, a NAMED action, we will use is the register
action and it was specified in the action
attribute of our form tag. Form actions live in a +page.server.js
file, in the same folder where +page.svelte
lives. This doesn't mean you can’t access form actions in a +page.server.js
file located in a different route. In fact, we'll use such an action in this project:
// frontend/src/routes/auth/register/+page.server.js
import { BASE_API_URI } from '$lib/utils/constants';
import { formatError, isEmpty, isValidEmail, isValidPasswordMedium } from '$lib/utils/helpers';
import { fail, redirect } from '@sveltejs/kit';
/** @type {import('./$types').PageServerLoad} */
export async function load({ locals }) {
// redirect user if logged in
if (locals.user) {
throw redirect(302, '/');
}
}
/** @type {import('./$types').Actions} */
export const actions = {
/**
*
* @param request - The request object
* @param fetch - Fetch object from sveltekit
* @returns Error data or redirects user to the home page or the previous page
*/
register: async ({ request, fetch }) => {
const formData = await request.formData();
const email = String(formData.get('email'));
const firstName = String(formData.get('first_name'));
const lastName = String(formData.get('last_name'));
const password = String(formData.get('password'));
const confirmPassword = String(formData.get('confirm_password'));
// Some validations
/** @type {Record<string, string>} */
const fieldsError = {};
if (!isValidEmail(email)) {
fieldsError.email = 'That email address is invalid.';
}
if (!isValidPasswordMedium(password)) {
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.';
}
if (confirmPassword.trim() !== password.trim()) {
fieldsError.confirmPassword = 'Password and confirm password do not match.';
}
if (!isEmpty(fieldsError)) {
return fail(400, { fieldsError: fieldsError });
}
const registrationBody = {
email,
first_name: firstName,
last_name: lastName,
password
};
/** @type {RequestInit} */
const requestInitOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(registrationBody)
};
const res = await fetch(`${BASE_API_URI}/users/register/`, requestInitOptions);
if (!res.ok) {
const response = await res.json();
const errors = formatError(response.error);
return fail(400, { errors: errors });
}
const response = await res.json();
throw redirect(303, `/auth/confirming?message=${response.message}`);
}
};
Let's skip the load
function for now. Our action is one of the keys in the actions
object. It's an async
function that takes in event
(destructured to retrieve on { request, fetch }
in our case). Using the request
, we retrieved the form's data and then individual fields using their names. You must set name attributes on each of the form fields as a result thereby conforming to web standards. Then, we validate the inputs to provide a better user experience. This complements the server-side validations put in place. This means that if a user disables javascript in the browser, the backend will catch the mistakes instead. The frontend validation logic lives in frontend/src/lib/utils/helpers.js
:
// frontend/src/lib/utils/helpers.js
// @ts-nocheck
import { quintOut } from 'svelte/easing';
import { crossfade } from 'svelte/transition';
export const [send, receive] = crossfade({
duration: (d) => Math.sqrt(d * 200),
// eslint-disable-next-line no-unused-vars
fallback(node, params) {
const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform;
return {
duration: 600,
easing: quintOut,
css: (t) => `
transform: ${transform} scale(${t});
opacity: ${t}
`
};
}
});
/**
* Validates an email field
* @file lib/utils/helpers/input.validation.ts
* @param {string} email - The email to validate
*/
export const isValidEmail = (email) => {
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) => {
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) => {
const mediumRegex = new RegExp(
'^(((?=.*[a-z])(?=.*[A-Z]))|((?=.*[a-z])(?=.*[0-9]))|((?=.*[A-Z])(?=.*[0-9])))(?=.{6,})'
);
return mediumRegex.test(password.trim());
};
/**
* Test whether or not an object is empty.
* @param {Record<string, string>} obj - The object to test
* @returns `true` or `false`
*/
export function isEmpty(obj) {
for (const _i in obj) {
return false;
}
return true;
}
/**
* Test whether or not an object is empty.
* @param {any} obj - The object to test
* @returns `true` or `false`
*/
export function formatError(obj) {
const errors = [];
if (typeof obj === 'object' && obj !== null) {
if (Array.isArray(obj)) {
obj.forEach((/** @type {Object} */ error) => {
Object.keys(error).map((k) => {
errors.push({
error: error[k],
id: Math.random() * 1000
});
});
});
} else {
Object.keys(obj).map((k) => {
errors.push({
error: obj[k],
id: Math.random() * 1000
});
});
}
} else {
errors.push({
error: obj.charAt(0).toUpperCase() + obj.slice(1),
id: 0
});
}
return errors;
}
We then make a request to our backend's register
endpoint. We saved our backend's base URL in a .env
file:
<!-- frontend/.env -->
VITE_BASE_API_URI_DEV=http://127.0.0.1:8080
VITE_BASE_API_URI_PROD=
Your environment variable must start with VITE_
for vite to pick it up. Then, in frontend/src/lib/utils/constants.js
, we loaded it:
// frontend/src/lib/utils/constants.js
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;
If there was an error from the backend, we properly form it and send it back to the form page. Otherwise, we move to the confirming
route with the response of the backend:
<!-- frontend/src/routes/auth/confirming/+page.svelte -->
<script>
import { page } from '$app/stores';
let message = '';
if ($page.url.search) {
message = $page.url.search.split('=')[1].replaceAll('%20', ' ');
}
</script>
<div class="container">
<div class="content">
<h1 class="step-title title">Email sent!</h1>
<h4 class="step-subtitle normal">
{message}
</h4>
</div>
</div>
You can test the registration form now.
Step 4: Account activation route
Having built the registration route, we need the complementary route specified in the registration email sent so that users can activate their accounts immediately:
<!-- frontend/src/routes/auth/activate/[id]/+page.svelte -->
<script>
import { applyAction, enhance } from '$app/forms';
import { page } from '$app/stores';
import { receive, send } from '$lib/utils/helpers';
/** @type {import('./$types').ActionData} */
export let form;
/** @type {import('./$types').SubmitFunction} */
const handleActivate = async () => {
return async ({ result }) => {
await applyAction(result);
};
};
/** @type {string|undefined} */
let token = '';
$: if (token) {
token = token.split(' ').join('');
let finalVal = token.match(/.{1,3}/g)?.join(' ');
token = finalVal;
}
</script>
<div class="container">
<form class="content" method="POST" use:enhance={handleActivate}>
<h1 class="step-title">Activate your account</h1>
{#if form?.errors}
{#each form?.errors as error (error.id)}
<h4
class="step-subtitle warning"
in:receive={{ key: error.id }}
out:send={{ key: error.id }}
>
{error.error}
</h4>
{/each}
{/if}
<input type="hidden" name="user_id" value={$page.params.id} />
<div class="input-box">
<span class="label">Token:</span>
<input
type="text"
class="input"
name="token"
placeholder="XXX XXX"
inputmode="numeric"
bind:value={token}
maxlength="7"
minlength="7"
/>
</div>
<button class="button-dark">Activate</button>
</form>
</div>
It is a simple route that looks just like this:
It simply expects the token sent to such a user. Aside from the token, it also takes the user's ID as a hidden field. Remember that our user activation route requires the user's ID as a path variable. Again, this page has a complementary +page.server.js
file:
// frontend/src/routes/auth/activate/[id]/+page.server.js
import { BASE_API_URI } from '$lib/utils/constants';
import { formatError } from '$lib/utils/helpers';
import { fail, redirect } from '@sveltejs/kit';
/** @type {import('./$types').PageServerLoad} */
export async function load({ locals }) {
// redirect user if logged in
if (locals.user) {
throw redirect(302, '/');
}
}
/** @type {import('./$types').Actions} */
export const actions = {
/**
*
* @param request - The request object
* @returns Error data or redirects user to the home page or the previous page
*/
default: async ({ request }) => {
const data = await request.formData();
const userID = String(data.get('user_id'));
let token = String(data.get('token'));
token = token.split(' ').join('');
/** @type {RequestInit} */
const requestInitOptions = {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
token: token
})
};
const res = await fetch(`${BASE_API_URI}/users/activate/${userID}/`, requestInitOptions);
if (!res.ok) {
const response = await res.json();
const errors = formatError(response.error);
return fail(400, { errors: errors });
}
const response = await res.json();
throw redirect(303, `/auth/login?message=${response.message}`);
}
};
The only different things in this code are that we used a default
form action here so that we wouldn't specify the action
attribute on our HTML form tag. Then, we used a different backend endpoint and a different HTTP method as expected. And lastly, we redirected to the login route if the activation is successful.
Now, you should feel pretty proud of yourself to have built this beautiful, performant, functional and secure system to this point!
In the next article, we'll talk about logging users in and out and retrieving the data of the currently logged-in user. See ya!
Outro
Enjoyed this article? I'm a Software Engineer and Technical Writer actively seeking new opportunities, particularly in areas related to web security, finance, health care, and education. If you think my expertise aligns with your team's needs, let's chat! You can find me on LinkedIn: LinkedIn and Twitter: Twitter.
If you found this article valuable, consider sharing it with your network to help spread the knowledge!
Top comments (0)