Authentication system using Golang and Sveltekit - Frontend Log in and out
Tags: go, webdev, javascript, tutorial
Published: 2023-06-05 20:52:32

## Introduction

To see the code we wrote in the previous article in action, we'll implement complementary frontend pages in this article. Most of the code in this article is similar to what we did in registration and token validation.

## Source code

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

{% github Sirneij/go-auth %}

## Implementation

### Step 1: Login page

For the login route, we need to create a `+page.svelte` in `routes/auth/login`:

```html
<!-- routes/auth/login/+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 handleLogin = async () => {
        return async ({ result }) => {
            await applyAction(result);
        };
    };

    let message = '';
    if ($page.url.searchParams.get('message')) {
        message = $page.url.search.split('=')[1].replaceAll('%20', ' ');
    }
</script>

<div class="container">
    <form class="content" method="POST" action="?/login" use:enhance={handleLogin}>
        <h1 class="step-title">Login User</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}

        {#if message}
            <h4 class="step-subtitle">{message}</h4>
        {/if}

        <input type="hidden" name="next" value={$page.url.searchParams.get('next')} />
        <div class="input-box">
            <span class="label">Email:</span>
            <input class="input" type="email" name="email" placeholder="Email address" />
        </div>
        <div class="input-box">
            <span class="label">Password:</span>
            <input class="input" type="password" name="password" placeholder="Password" />
            <a href="/auth/password/request-change" style="margin-left: 1rem;">Forgot password?</a>
        </div>
        <div class="btn-container">
            <button class="button-dark">Login</button>
            <p>Have no account? <a href="/auth/register">Register here</a>.</p>
        </div>
    </form>
</div>
```

As usual, this is just mostly HTML with Svelte's syntactic sugar. If you view this page in the browser, it looks just like this:

![Application's login page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gg2g3bscjg4cl201ek9h.png "Application's login page")

It also has a corresponding `+page.server.js` which handles the form submission:

```js
// routes/auth/login/+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
     * @param fetch - Fetch object from sveltekit
     * @param cookies - SvelteKit's cookie object
     * @returns Error data or redirects user to the home page or the previous page
     */
    login: async ({ request, fetch, cookies }) => {
        const data = await request.formData();
        const email = String(data.get('email'));
        const password = String(data.get('password'));
        const next = String(data.get('next'));

        /** @type {RequestInit} */
        const requestInitOptions = {
            method: 'POST',
            credentials: 'include',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                email: email,
                password: password
            })
        };

        const res = await fetch(`${BASE_API_URI}/users/login/`, requestInitOptions);

        if (!res.ok) {
            const response = await res.json();
            const errors = formatError(response.error);
            return fail(400, { errors: errors });
        }

        if (res.headers.has('Set-Cookie')) {
            const sessionID = Object.fromEntries(res.headers)
                ['set-cookie'].split(';')[0]
                .split(/=(.*)/s)[1];

            const path = Object.fromEntries(res.headers)['set-cookie'].split(';')[1].split('=')[1];
            const maxAge = Number(
                Object.fromEntries(res.headers)['set-cookie'].split(';')[2].split('=')[1]
            );

            cookies.set('go-auth-sessionid', sessionID, {
                httpOnly: true,
                sameSite: 'lax',
                path: path,
                secure: true,
                maxAge: maxAge
            });
        }

        throw redirect(303, next || '/');
    }
};
```

In the `load()` function, we bar authenticated users from accessing the page since they are already logged in. Then, the named form action. It's pretty similar to what we did before. The major difference is how we extracted the session token from the response header and set the cookie in the browser:

```js
...
    if (res.headers.has('Set-Cookie')) {
        const sessionID = Object.fromEntries(res.headers)
            ['set-cookie'].split(';')[0]
            .split(/=(.*)/s)[1];

        const path = Object.fromEntries(res.headers)['set-cookie'].split(';')[1].split('=')[1];
        const maxAge = Number(
            Object.fromEntries(res.headers)['set-cookie'].split(';')[2].split('=')[1]
        );

        cookies.set('go-auth-sessionid', sessionID, {
            httpOnly: true,
            sameSite: 'lax',
            path: path,
            secure: true,
            maxAge: maxAge
        });
    }
...
```

This saved cookie is then used in `frontend/src/hooks.server.js` to fetch the currently logged user:

```js
// frontend/src/hooks.server.js
import { BASE_API_URI } from '$lib/utils/constants';

/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
    if (event.locals.user) {
        // if there is already a user  in session load page as normal
        return await resolve(event);
    }
    // get cookies from browser
    const session = event.cookies.get('go-auth-sessionid');

    if (!session) {
        // if there is no session load page as normal
        return await resolve(event);
    }

    // find the user based on the session
    const res = await event.fetch(`${BASE_API_URI}/users/current-user/`, {
        credentials: 'include',
        headers: {
            Cookie: `sessionid=${session}`
        }
    });

    if (!res.ok) {
        // if there is no session load page as normal
        return await resolve(event);
    }

    // if `user` exists set `events.local`
    const response = await res.json();

    event.locals.user = response;
    if (event.locals.user.profile.birth_date) {
        event.locals.user.profile.birth_date = response['profile']['birth_date'].split('T')[0];
    }

    // load page as normal
    return await resolve(event);
}
```

`handle` hook is useful in achieving this thereby ensuring that our application's users remain logged in as long as their cookies are still valid. Immediately such cookies expire or get invalidated, users must re-login. A user whose cookie is valid gets access to data via the `/users/current-user/` endpoint, one of the handlers implemented in the last article. Also, note that you need to send the cookie with the request for it to succeed.

Having retrieved the logged-in user, we need to make sure all other parts of our application have access it to. To do that, we will create a `+layour.server.js` and from there propagate the data:

```js
// frontend/src/routes/+layout.server.js
/** @type {import('./$types').LayoutServerLoad} */
export async function load({ locals }) {
    return {
        user: locals.user
    };
}
```

But since we already have `+layout.js`, the data from `+layour.server.js` will not be available unless we instruct `+layout.js` to propagate the data:

```js
// frontend/src/routes/+layout.js

/** @type {import('./$types').LayoutLoad} */
export async function load({ fetch, url, data }) {
    const { user } = data;
    return { fetch, url: url.pathname, user };
}
```

Now, every page can access the data via the `data.user` object of the `page` store. We will use it next.

### Step 2: Logout logic

To log users out, we will also stick to web standards by using the form tag for such an action. Our `frontend/src/lib/components/Header.svelte` should now look like this:

```html
<script>
    import { applyAction, enhance } from '$app/forms';
    import { page } from '$app/stores';
    import Developer from '$lib/img/hero-image.png';
    import Avatar from '$lib/img/teamavatar.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>
            {#if !$page.data.user}
                <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>
            {:else}
                <div class="header-nav-item">
                    <a href="/auth/about/{$page.data.user.id}">
                        <img
                            src={$page.data.user.thumbnail ? $page.data.user.thumbnail : Avatar}
                            alt={`${$page.data.user.first_name} ${$page.data.user.last_name}`}
                        />
                    </a>
                </div>
                <form
                    class="header-nav-item"
                    action="/auth/logout"
                    method="POST"
                    use:enhance={async () => {
                        return async ({ result }) => {
                            await applyAction(result);
                        };
                    }}
                >
                    <button type="submit">logout</button>
                </form>
            {/if}
        </div>
    </div>
</header>
```

You can see that we accessed the user's data via `$page.data.user`. The `$` at the start is called [auto-subscription of stores][1].

In this part of the header:

```html
...
    <form
        class="header-nav-item"
        action="/auth/logout"
        method="POST"
        use:enhance={async () => {
            return async ({ result }) => {
                await applyAction(result);
            };
        }}
    >
        <button type="submit">logout</button>
    </form>
...
```

If you check the `action`, we are referencing a route, `/auth/logout`. The logout route. This route is non-existent yet. We will create it. However, this route will not have a `+page.svelte` file because we don't have what to show there. However, it will have a `+page.server.js` because that is what will send our data to the backend. Since we used the route as our action, then the form action will be unnamed or default. If the action were `/auth/logout?/logout`, then the form action wouldn't be the default:

```js
// frontend/src/routes/auth/logout/+page.server.js

import { BASE_API_URI } from '$lib/utils/constants';
import { fail, redirect } from '@sveltejs/kit';

/** @type {import('./$types').PageServerLoad} */
export async function load({ locals }) {
    // redirect user if not logged in
    if (!locals.user) {
        throw redirect(302, `/auth/login?next=/auth/logout`);
    }
}

/** @type {import('./$types').Actions} */
export const actions = {
    default: async ({ fetch, cookies }) => {
        /** @type {RequestInit} */
        const requestInitOptions = {
            method: 'POST',
            credentials: 'include',
            headers: {
                'Content-Type': 'application/json',
                Cookie: `sessionid=${cookies.get('go-auth-sessionid')}`
            }
        };

        const res = await fetch(`${BASE_API_URI}/users/logout/`, requestInitOptions);

        if (!res.ok) {
            const response = await res.json();
            const errors = [];
            errors.push({ error: response.error, id: 0 });
            return fail(400, { errors: errors });
        }

        // eat the cookie
        cookies.delete('go-auth-sessionid', { path: '/' });

        // redirect the user
        throw redirect(302, '/auth/login');
    }
};
```

Here, we only sent the cookie to the server. With that, the server knows who wants to log out. Then, it deletes such a cookie from the server and on a successful response, we also delete it in the browser.

With that, we will end it here. The next article will be about token regeneration and password reset functionalities. See you!

## 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://learn.svelte.dev/tutorial/auto-subscriptions "Svelte's store auto-subscription"