Authentication system using Golang and Sveltekit - Frontend user registration and activation
Tags: go, webdev, javascript, svelte
Published: 2023-06-04 12:36:08

## 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 Sirneij/go-auth %}

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

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

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

```html
<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:

```html
<!-- 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`:

```html
<!-- 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`:

```html
<!-- 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>
```

Just a basic HTML.

Back to the `+layout.svelte` file. We exported a `data` variable. This variable was exported from `+layout.js`:

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

```html
<!-- 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:


![Application's home page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sm3gbhi8cawq275f20rs.png "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:

```html
<!-- 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:


![Application's register page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/llp9nftqkogvfvy07tze.png "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][1] 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:

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

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

```env
<!-- 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][2] to pick it up. Then, in `frontend/src/lib/utils/constants.js`, we loaded it:

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

```html
<!-- 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:

```html
<!-- 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:


![Application's activate page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f2ujvljgpir3fauca45p.png "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:

```js
// 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](https://www.linkedin.com/in/idogun-john-nelson/) and Twitter: [Twitter](https://twitter.com/Sirneij).

If you found this article valuable, consider sharing it with your network to help spread the knowledge!

[1]: https://kit.svelte.dev/docs/form-actions "Form actions"
[2]: https://vitejs.dev/ "Vite Next Generation Frontend Tooling"