is there something similar to Angular route guards in SvelteKit
Asked Answered
O

7

6

I am trying to build an SPA with role based access using Svelte. Coming from Angular with its route guards, I want to setup something similar.

What would be the best way to do that?

EDIT:

I was indeed referring to SvelteKit, and I updated my question to reflect that.

Oberammergau answered 3/7, 2021 at 16:59 Comment(0)
H
3

Answer as of @sveltejs/kit 1.0.0-next.539:

You can create a layout file at the top of the directory you want to guard.

For example: src/routes/+layout.svelte for the root path. All child directories will use this layout, and therefore the logic you define here will be executed on every page underneath.

Example

<script lang="ts">
    import { isSignedIn } from '$lib/store'
    import { browser } from '$app/environment'
    import { goto } from '$app/navigation'
    import { page } from '$app/stores'

    $: if (browser && !$isSignedIn && $page.url.pathname !== '/auth/sign-in') {
        console.log(
            'we are running in the browser, not signed in, and not on the sign in page, we are going to the sign in page'
        )
        goto('/auth/sign-in')
    } else if (browser && $isSignedIn && $page.url.pathname === '/auth/sign-in') {
        console.log(
            'we are running in the browser, signed in, and on the sign in page, we are going to the home page'
        )
        goto('/')
    }
</script>

<slot />

How to determine is authenticated?

In the above example, the logic whether the current user is authentificated or not is determined by some state isSignedIn from the src/lib/store.ts file.

How to redirect?

To maybe redirect, we use the goto function from SvelteKit. However, since this is not available on the server-side, we have to make sure we are in the browser. SvelteKit helps us with the browser variable here.

Prevent infinite redirect loop

We want to redirect to some sign in page, but we don't want to redirect to the sign in page, when we are already viewing the sign in page.

  • Therefore we should scope our layout, so our sign in page is not a child of our layout with the redirect logic.

  • Or we adjust our condition to check that we are not viewing the sign in page: For this we can use the page object and check on the page.url.

Reactive watch

If we only want to check on page load we are fine by putting a if statement in the script part.

If we want to also trigger on any changes, for example on the auth state, we should execute the logic reactively by putting $: in front of our expressions.

Hoof answered 9/11, 2022 at 18:57 Comment(1)
Would I be able to do access token refresh in there async?Yearbook
A
2

I'm assuming you are referring to SvelteKit, which includes routing. Please confirm?

If you have subfolders for each route, you can create a __layout.svelte file in each route folder which will be run on every request to that route. You can put your guard logic in the layout file.

Example:

File location: /src/admin/__layout.svelte

<script context="module">
    import { goto } from '$app/navigation';
    import { browser } from '$app/env';
    // import your user store here.

    // if (browser && $user)

    // Add your specific guard logic here, you need to include the 'browser' 
    //check otherwise Vite tries to process it on the server side
    if (browser) {
        // Use Goto to redirect users if neccessary
        goto('login');
    }
</script>

<!--<slot> the same as router-outlet -->
<slot />
Annettannetta answered 9/7, 2021 at 10:14 Comment(3)
I was indeed referring to SvelteKit. I have updated my question to reflect that. I have implemented something similar to this. The concern I have is that if I have a lot of different routes, I will have to implement the guard logic in all of those routes, which is essentially a duplication.Oberammergau
But if you add your logic into the __layout.svelte file, the logic will apply to all sub-routes.Annettannetta
This doesn't make sense. How do you access user store if it uses localstorage, also how would you stop it from going into the goto('login') code if a user exists?Crucible
C
0

svelte-router-spa module offers good and practical features for routing, minimum configurations and simple usage. Guards are included.

https://www.npmjs.com/package/svelte-router-spa#usage

Chutney answered 24/7, 2021 at 12:7 Comment(0)
C
0

My user store doesn't work inside <script context="module"> so I have to do it client side...the drawback being you get a flash of rendered page:

<script>
    import { goto } from '$app/navigation';
    import { browser } from '$app/env';
    import { userStore } from '$stores/user.js';

    if (browser && !$userStore) {
        goto('/login');
    }
</script>

<slot />

My user store uses localStorage for token so it must be done in browser (not ssr)

You can hide the protected content with a conditional:

<script>
    import { goto } from '$app/navigation';
    import { browser } from '$app/env';
    import { userStore } from '$stores/user.js';

    if (browser && !$userStore) {
        goto('/login');
    }
</script>

{#if $userStore}
    <slot />
{/if}

Crucible answered 7/3, 2022 at 4:21 Comment(0)
B
0

Check this package https://www.npmjs.com/package/sveltekit-route-guard

// src/hook.server.ts
import { verify } from 'jsonwebtoken'
import { JWT_SECRET } from '$env/static/private'
import { createRouteGuard } from 'sveltekit-route-guard'

import type { User } from '@prisma/client'
import type { Handle, RequestEvent } from '@sveltejs/kit'

const getCurrentUser = (event: RequestEvent) => {
 try {
  const token = event.cookies.get('token')
  return verify(token || '', JWT_SECRET) as User
 } catch (_) {
  return null
 }
}

export const handle: Handle = createRouteGuard({
 redirect,
 routes: [
  {
   pathname: '/projects',
   meta: {
    auth: true
   }
  },
  {
   pathname: '/login',
   meta: {
    auth: false
   }
  }
 ],
 beforeEach(to, event, next) {
  // check if the user is authenticated ot not
  const user = getCurrentUser(event)
  if (user) event.locals.user = user

  // not authenticated and requires authentication is true
  if (!user && to.meta?.auth) {
   return next('/login')
  }

  // already authenticated, can't go to /login
  if (user && to.meta?.auth === false) {
   return next('/')
  }

  // no guard, continue the request
  return next()
 }
})
Broadway answered 28/1, 2023 at 22:32 Comment(0)
Z
0

Here's how I'm protecting routes. I keep seeing this question a lot, so I'm going to answer with my method which is easy to re-use.

I have a AuthLayout component that I just wrap around the routes I want to protect.

/* src/lib/components/AuthLayout.svelte */
<script lang="ts">
    import { goto } from '$app/navigation';
    import authStore from '$lib/stores/auth';
    import { onDestroy } from 'svelte';

    let ok = false;

    const unsubscribe = authStore.subscribe((state) => {
        if (!state.initializing && state.fbUser !== undefined) {
            ok = state.fbUser !== null;
            if (!ok) {
                goto('/login');
            }
        }
    });

    onDestroy(unsubscribe);
</script>

{#if ok}
    <slot />
{:else}
    <div />
{/if}

A few things are happening here. I have a authStore that looks like this:

/* src/lib/stores/auth.ts */
import { writable } from 'svelte/store';
import type { User as FbUser } from 'firebase/auth';

export type AuthStore = {
    initializing: boolean;
    fbUser: FbUser | null | undefined;
};

const authStore = writable<AuthStore>({
    initializing: true,
    fbUser: undefined
});

export default authStore;

I am using Firebase auth, so in my top level layout.svelte, I've got the following:

/* src/routes/+layout.svelte */
<script lang="ts">
    import '../app.css';

    import { onMount } from 'svelte';
    import authStore from '$lib/stores/auth';
    import Footer from './Footer.svelte';
    import Header from './Header.svelte';
    import { auth } from '../firebase';

    onMount(() => {
        auth.onAuthStateChanged(async (user) => {
            authStore.update((state) => ({
                ...state,
                initializing: false,
                fbUser: user
            }));
        });
    });
</script>

<Header />
<main><slot /></main>
<Footer />

I update my authStore whenever the firebase user value changes in my onAuthStateChanged listener.

Now, let's circle back to the AuthLayout component. It grabs the fbUser value from the authStore and if it is null, I redirect to the "/login" route.

The reason there is a ok boolean is to avoid the brief flicker when the content you're trying to protect might show. By using the ok check, I can ensure that I just show a empty div when there is no firebase user. Otherwise, I will show the protected content via the <slot />. There is also an onDestroy callback so I don't have a memory leak with the store subscription.

Finally, here is an example of how I use AuthLayout to protect /welcome:

/* src/routes/welcome/+layout.svelte */
<script>
    import AuthLayout from '$lib/components/AuthLayout.svelte';
</script>

<AuthLayout><slot /></AuthLayout>

Voila! The route has been protected.

Zenas answered 12/2, 2023 at 5:41 Comment(0)
D
-3

EDIT:

The author has changed the question to sveltekit. The answer from @justintimeza is correct.

The below explanation is still valid for just svelte.

Svelte has no routing you can use packages like routify, tinro that gives you the utility for routes.

Also, take a look at svelte-routing regarding an approach.

Deanery answered 9/7, 2021 at 7:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.