Svelte Sapper retain session on page refresh (reload)
Asked Answered
P

2

7

I've just recently started using svelte and sapper, and I'm trying to persist a stored session even when a user refreshes the page. (I hope this can be done).

The idea is that a user can sign in, and be redirected to the homepage as an authenticated used. BUT when I hit refresh in my browser, the session is empty and user has to go through sign in process over again.

Any Ideas?

So far I could not find a solution. Below some of the files the are involved in this process.

server.js

import sirv from "sirv";
import polka from "polka";
import compression from "compression";
import * as sapper from "@sapper/server";
import bodyParser from "body-parser";
import session from "express-session";
import sessionFileStore from "session-file-store";

const { PORT, NODE_ENV } = process.env;
const dev = NODE_ENV === "development";
const FileStore = sessionFileStore(session);

polka()
  .use(
    bodyParser.json(),
    session({
      secret: "secret",
      resave: false,
      saveUninitialized: true,
      cookie: {
        maxAge: 31536000,
      },
      store: new FileStore({
        path: process.env.NOW ? `/tmp/sessions` : `.sessions`,
      }),
    })
  )
  .use(
    compression({ threshold: 0 }),
    sirv("static", { dev }),
    sapper.middleware({
      session: (req) => ({
        user: req.session && req.session.user,
      }),
    })
  )
  .listen(PORT, (err) => {
    if (err) console.log("error", err);
  });

login.svelte

<script context="module">
  export async function preload({ params }, { user }) {
    if (user) {
      this.redirect(302, `/`);
    }
  }
</script>

<script>
  import { goto, stores } from "@sapper/app";
  import api from "../api.js";
  import Button from "../components/Button.svelte";
  import Input from "../components/Input.svelte";
  import InputPassword from "../components/InputPassword.svelte";

  let errors;
  let email;
  let password;
  let disabled;

  const { session } = stores();

  const handleSubmit = async () => {
    try {
      errors = null;
      disabled = true;
      await api.get("/csrf-cookie");
      const authToken = await api.post("/login", { email, password });
      api.defaults.headers.common["Authorization"] = `Bearer ${authToken.data}`;
      const user = await api.get("/me");
      session.set({ user: user.data });
      disabled = false;
      goto("/");
    } catch (e) {
      errors = e;
      disabled = false;
    }
  };
</script>

<style>
  .login-form {
    max-width: 35em;
    margin: 5% auto;
    padding: 2em;
    background: rgba(233, 233, 233, 0.5);
    border: 1px solid rgba(151, 151, 151, 0.5);
    border-radius: 5px;
  }

  .form-title {
    margin-top: 0;
    font-size: 1.2em;
  }

  .error-block {
    color: red;
  }
</style>

<svelte:head>
  <title>Login</title>
</svelte:head>

<div class="login-form">
  <h3 class="form-title">Login</h3>
  <form on:submit|preventDefault={handleSubmit}>
    <Input placeholder="Username" id="email" bind:value={email} />
    <InputPassword placeholder="Password" id="password" bind:value={password} />
    <Button {disabled} type="submit">Login</Button>

    {#if errors}<span class="error-block">{errors}</span>{/if}
  </form>
</div>

index.svelte

<script context="module">
  export async function preload({ params }, { user }) {
    console.log(user); // undefined
    if (!user) {
      this.redirect(302, `/login`);
    }
  }
</script>

<h1>Dashboard</h1>

I'm using Laravel 8 sanctum for Auth.

Not sure what else I need to provide to get to the bottom of this issue.

Prosopopoeia answered 18/10, 2020 at 11:26 Comment(0)
H
1

It appears as though you lifted most of your code from the sapper realworld project (correct me if I'm wrong), but you forgot to implement a server-side 'api route' to add the freshly logged in user to the session.

In the realworld project, when the user logs in, a POST request is made to the server-side /auth/login route, which is served by the following function:

import * as api from 'api.js';

export function post(req, res) {
    const user = req.body;

    api.post('users/login', { user }).then(response => {
        if (response.user) req.session.user = response.user;
        res.setHeader('Content-Type', 'application/json');

        res.end(JSON.stringify(response));
    });
}

What this function does is:

  1. it relays the request to the /users/login endpoint of the realworld project API
  2. if the response to that request contains a user object, it stores that object into the server-side session
  3. it returns the original API response as JSON back to the app, where it is used to populate the session store (if the original response contained a user object)

Considering you're obviously NOT using the realworld project API to authenticate against, but your own auth process, what you have to add is a similar server-side route as the one above, but one that will:

  • relay the login request to your own process instead,
  • field the response and use that response to set the session user,
  • and finally pass the response on to the client (or, alternatively, pass either the user object stored into the session or an error message otherwise).

Considering the API calls you're using to set the user on the client side in your code, that function would look something like this (saving that file as /routes/auth/login.js for example):

import * as api from 'api.js';

export async function post(req, res) {
    const { email, password } = req.body;
    const authToken = await api.post("/login", { email, password });
    api.defaults.headers.common["Authorization"] = `Bearer ${authToken.data}`;
    const user = await api.get("/me");
    if (user) req.session.user = user.data;
    res.setHeader('Content-Type', 'application/json');
    res.end(JSON.stringify(user));
}

and the handleSubmit method in your login.svelte file becomes:

  const handleSubmit = async () => {
    try {
      errors = null;
      disabled = true;
      // substitute your auth API request chain with a proxy request to
      // the server-side API where you will set the server-side session user
      const user = await fetch('/auth/login', {
        method: 'POST',
        credentials: 'include',
        body: JSON.stringify({ email, password }),
        headers: { 'Content-Type': 'application/json' },
      })
      session.set({ user: user.data });
      disabled = false;
      goto("/");
    } catch (e) {
      errors = e;
      disabled = false;
    }
  };

Note that in your particular case, you'd probably want to store the auth token in the session as well, to avoid having to request a new token every time you want to make an authenticated request to your data API.

Hahnemann answered 19/10, 2020 at 15:36 Comment(1)
Cool hints for implementing authorization in sapper. I've used your excellent workouts in this context. Best nologinItalic
I
1

Use the svelte localStorage:

Create a store e.g. myStore.js

import { writable } from 'svelte/store';

export let mystore = writable({
    session: ""
});


export function setSession(session) {
    mystore.set({
        session: session
    });
    session = session; // refresh UI
}

Subscribe to it in routes/_layout.svelte

<script>
    import {mystore, setSession} from './myStore.js'

    let session = setSession("A_SESSION"); // here comes the session


    const unsubscribeMyStore = mystore.subscribe(value => {
        session = session;
    });
</script>

<A_COMPONENT bind:session={$mystore}/> // if the component exports session

Use in A_COMPONENT:

<script>
    export let session;
</script>

<div>
{session.session}
</div>
Italic answered 23/10, 2020 at 10:41 Comment(4)
Using localStorage is a workaround at best. The server is still unaware that the session has been initialized, session expiration is not handled, etc. LocalStorage is indeed a way to persist data across reloads, but it's not a 1-for-1 substitute of proper session management.Hahnemann
@Italic I'm interested in your idea because I'm trying to persist some UI state on the client while navigating a Sapper app. However, I doesn't work, or even implements what you say (there's no reference to localStorage in your code). It would be very helpful if you could edit your answer with a complete example.Corvus
@NicolasLeThierryd'Ennequin There was an error in _layout.svelte. The writable variable need an $ as prefix. I've created an working repo: github.com/ivosdc/sapper-appstore-exampleItalic
I've found another implementation with localStorage: chasingcode.dev/blog/svelte-persist-state-to-localstorageItalic

© 2022 - 2024 — McMap. All rights reserved.