DEV Community

Cover image for Authentication system using Golang and Sveltekit - Frontend user registration and activation
John Owolabi Idogun
John Owolabi Idogun

Posted on • Updated on

Authentication system using Golang and Sveltekit - Frontend user registration and activation

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:

GitHub logo Sirneij / go-auth

A fullstack session-based authentication system using golang and sveltekit

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")
    ...
}
Enter fullscreen mode Exit fullscreen mode

We then add frontendURL to the config type in cmd/api/main.go:

// cmd/api/main.go
...
type config struct {
    ...
    frontendURL     string
    ...
}
...
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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">&copy; {year} John Owolabi Idogun. All Rights Reserved.</span>
        </div>
    </div>
</footer>
Enter fullscreen mode Exit fullscreen mode

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 };
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Again, just a simple HTML page. We also loaded an image in the script tag.

Our page should look like this now:

Application's home page

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>
Enter fullscreen mode Exit fullscreen mode

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:

Application's register page

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}`);
    }
};
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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=
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

It is a simple route that looks just like this:

Application's activate page

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}`);
    }
};
Enter fullscreen mode Exit fullscreen mode

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? Consider contacting me for a job, something worthwhile or buying a coffee ☕. You can also connect with/follow me on LinkedIn and Twitter. It isn't bad if you help share this article for wider coverage. I will appreciate it...

Top comments (0)