Skip to content

$middleware.svelte to run code on client-side each time child page is rendered #1165

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
moisesbites opened this issue Apr 22, 2021 · 21 comments
Labels
feature / enhancement New feature or request router

Comments

@moisesbites
Copy link

moisesbites commented Apr 22, 2021

$ layout.svelte is a great idea to reflect the visual aspects and components common to all child pages.

It would be interesting if there was also a $ middleware.svelte with functions and routines that could be executed whenever a descendant page was called to be displayed, for example, because of access rules, filters, user functions or things like that, maybe more in terms of security.

I'm starting to work with sveltekit and I miss it.

I still haven't found a way to do that in Sveltekit.

I would be grateful if someone clarified to me if it already exists or if there is a way to do it safely.

@moisesbites
Copy link
Author

moisesbites commented Apr 22, 2021

I think that these issues is about that #552 #944

@Rich-Harris
Copy link
Member

What would you want to do that you can't do in load?

@Conduitry
Copy link
Member

I think the idea is that it would let you somehow have implied load behavior in all descendant pages under some path. But I don't see what we'd want to have besides error or context which are already possible with layout loads. Everything else should probably be handled explicitly in each load, possibly importing some shared helpers.

@benmccann benmccann added feature / enhancement New feature or request router labels Apr 22, 2021
@moisesbites
Copy link
Author

moisesbites commented Apr 22, 2021

What would you want to do that you can't do in load?

I think the idea is that it would let you somehow have implied load behavior in all descendant pages under some path. But I don't see what we'd want to have besides error or context which are already possible with layout loads. Everything else should probably be handled explicitly in each load, possibly importing some shared helpers.

I tried to use load, but, load didn't run "every time" as I needed. But, maybe I am doing something wrong.

I think I need to make my need more explicit:

I am developing a web application and not a web site. I didn't see anything as easy to do as Sveltekit.

So, in that regard, I have some pages open that do not need to be authenticated. But I have many other pages closed to unauthenticated or unauthorized users. Therefore, on all pages of the secure area, I need to check authentication, authorization, rules, permissions, every time. In addition, it should display objects or components partially, depending on the level of access.

But, as I use JWT with cookies, and everything is stateless, because of the need for scale (I need to use kubernetes and many PODs) I can't make the system behave safely. Sometimes it works, sometimes it doesn't, sometimes you lose the route I clicked on - even if you click on a route, the system keeps the previous route screen.

All pages in the safe area contain dynamic data and need to be updated with each click every time. I do not store user information on the web server (neither back-end API nor Sveltekit server). At most, I save the temporary data used in Redis to improve performance. The problem is that Sveltekit doesn't seem to run load every time.

@moisesbites
Copy link
Author

Maybe this one will work: #552 (comment)

I will try.

@Rich-Harris
Copy link
Member

load will re-run whenever its dependencies change. For example, this function...

export async function load({ session }) {
  if (!allowed(session.user)) {
    return { status: 403, error: new Error('Forbidden') };
  }

  // ...
}

...will re-run whenever session changes . That's generally the place you want to do these sorts of checks.

Triggering a change to session without a page reload is up to the developer; for example you could do this sort of thing...

import { session } from '$app/stores';

async function logout() {
  await fetch('/auth/logout', { method: 'POST' });
  $session.user = null;  
}

...but that doesn't solve the problem of e.g. logging out in a different tab. It's solvable in userland, but I almost wonder if SvelteKit should have some built-in stuff for refreshing the session from the server periodically (including when you refocus a page, swr style). #46 included some thinking along those lines.

@rmunn
Copy link
Contributor

rmunn commented Apr 23, 2021

+1 for some kind of built-in session refresh feature, configurable with an option or two in svelte.config.cjs. Logging out in a different tab is something almost any app will want to handle. Some apps may want to allow you to stay logged in in tab A when you log out of tab B, while other apps will want your logout in tab B to automatically log you out of tab A as well. So how and when the session is persisted to the server and refreshes in other tabs is something the user should be able to configure. But having a sensible default that will work for many use cases, and can be configured to match other needs (websocket push? polling?) would indeed be very useful for my needs. So I'm happy to hear thinking along those lines.

@moisesbites
Copy link
Author

I'm reading the posts above and trying test the suggestions.

How can I config svalte.config.cjs for that?

@Rich-Harris
Copy link
Member

Config for what?

@moisesbites
Copy link
Author

moisesbites commented Apr 23, 2021

Config for what?
@Rich-Harris

I was referring to the comment below of @rmunn. Excuse me for not being clearer.

+1 for some kind of built-in session refresh feature, configurable with an option or two in svelte.config.cjs.

I was testing the alternatives, last night.

I have not yet been able to make the system behave in the way that the requirements require, but I am making progress. I still think the solution would be to have more explicit "middleware" to run "every time" before any page is rendered in the DOM for all child pages. But, by learning more about sveltekit, I think I may be able to do that. Although middleware does not yet exist, I am trying to use the tools already available.

@moisesbites
Copy link
Author

moisesbites commented Apr 27, 2021

So, uptading.

I don't think I can find a solution to this problem without Sveltekit being explicitly prepared for that.

In the $ layout.svelte file I did:

<script>
...
  import { isLoggedIn } from '$lib/_fetchApi';
...
</script>

{#await isLoggedIn ()}
   <CenterBox>
     <Icon d = {spinner} width = "24" height = "24" animatedSpin />
   </CenterBox>
{: then u}
   <Nav />
   <slot />
{: catch error}
   <Login on: success = {logged in} />
{/ await}

In the _fechApi.js

async function requestPromess(entrypoint, method, dataPost) {
  let req = {
    method: method,
    compress: true,
    headers: {
      'Content-Type': 'application/json'
    }
  };
  if (method === 'POST') {
    req.body = JSON.stringify(dataPost);
  }
  const res = await fetch(hostApi + entrypoint, req);
  const obj = await res.json();

  return obj;
}
....
export async function isLoggedIn() {
  const usuario = await requestPromess('/auth/isloggedin', 'GET', {});
  console.log(JSON.stringify(usuario.data));
  if (usuario.status === 'fail') {
    throw 'usuário não autenticado';
  }
  return usuario.data.usuario;
}

For while I'm working in development stage, it don't work every time. I have about 3 months to produce some code to production. The system I am working is completely stateless not saving data in svelte's nodejs nor backend's nodejs.

The idea about having middleware that runs whenever a page or a route is clicked or requested, both in the browser and on the server, is very interesting. Something that was called before anything that was mounted on the DOM, or executed on the route component.

I know that my code above is very simple and looks childish, and it may not reflect the sveltekit concept yet, and I guarantee that I want to improve it, but it is a starting point for a completely stateless system, running on sveltekit.

I think this middleware should run mainly on the browser.

@poppa
Copy link

poppa commented Apr 27, 2021

[...]
I don't think I can find a solution to this problem without Sveltekit being explicitly prepared for that.
[...]

I'm in no way, shape or form a Svelte expert but it seems you are making things harder than necessary.

If you use the getContext() and getSession() hooks (https://kit.svelte.dev/docs#hooks) together with the session-store (https://kit.svelte.dev/docs#modules-$app-stores) I'm quite sure you will achieve what you'r trying to achieve without any involvement of middlewares.

Happy Svelte Coding!

@moisesbites
Copy link
Author

moisesbites commented Apr 28, 2021

[...]
I don't think I can find a solution to this problem without Sveltekit being explicitly prepared for that.
[...]

I'm in no way, shape or form a Svelte expert but it seems you are making things harder than necessary.

If you use the getContext() and getSession() hooks (https://kit.svelte.dev/docs#hooks) together with the session-store (https://kit.svelte.dev/docs#modules-$app-stores) I'm quite sure you will achieve what you'r trying to achieve without any involvement of middlewares.

Happy Svelte Coding!

Thank you for your commentary. In fact, Svelte is the best way to implement Web Apps. I'm new in JS frameworks. I worked with java, C#, .NET, since ASP/PHP.

I missed a middleware that could be made explicit, running in the Browser, where I could write routines that would be triggered every time a route, including its sub-routes, was accessed, more as if it were an event or trigger, like an "after route" "and" before accessing the page ", however, running in the Browser and not on the server.

In the last 3 years, I was working with DevOps Kubernetes/Docker.
But now, I have a new project, and I need to work with JS framework, and I'm new in this new big world. Of all those I have tested, by far, Svelte is the best for me. The code is simple, objective, elegant.

So, I'm a newbie about Svelte and Sveltekit, but I'm also becoming an enthusiast.

I thank you very much if you have an example of authentication / authorization with JWT Token using the API. The system I'm working on has an API on a nodejs back-end server with express. The problem is: when the user presses F5 to refresh the page, or press the "back a page" button on the browser, the stores disappear. And the front-end application needs to access the API directly. I didn't think about controlling the session on the Svelte server. Maybe, that's why I'm a little confused. I will see how to do this, using a session on the Svelte server.

Forgive me for my lack of experience with JS frameworks.

@poppa
Copy link

poppa commented Apr 28, 2021

[...]

So, I'm a newbie about Svelte and Sveltekit, but I'm also becoming an enthusiast.

So am I :)

[...]
Forgive me for my lack of experience with JS frameworks.

I think this is the wrong forum for "support" questions. Join the Svelte community at Discord and ask away there:
https://discord.com/invite/yy75DKs

Cheers

@moisesbites
Copy link
Author

moisesbites commented Apr 29, 2021

I think this is the wrong forum for "support" questions. Join the Svelte community at Discord and ask away there:
https://discord.com/invite/yy75DKs

Ok. Sorry, :-) It was not my intention to ask for support here.

After several simulations, I managed to make the system behave according to my need. I had to write the code below on every page in the route folder, not in '$layout.svelte'. Remembering that the system is stateless and does not use anything from the session on the node server that serves Svelte. I may even be able to change that in the future, but for now, those are the requirements I have.

For the ideal solution, and that's what I propose, in this case, it would be a $middleware.svelte file that runs every time when a page is accessed in a route, before the $layout.svelte page, and the correct page.svelte in the directory subtree. The section <script> is executed every time on the pages, so $middleware.svelte should be, too. However, one thing I felt necessary would be for changes in store variables to be reflected in $layout.svelte after running $middleware.svelte. In my case, as I said, I have to check security all the time, especially if the JWT token is revoked on the API by an administrator user, or if a permission to a page is taken while the user is online.

For while, I solved using something like that in every page, for example, for a simple logging check:

{#await isLoggedIn()}
  <CenterBox>
    <Icon d={spinner} width="24" height="24" animatedSpin />
  </CenterBox>
{:then u}
  {window.location.replace('/s')}
{:catch}
  {window.location.replace('/a/login')}
{/await}

But, because $layout.svelte is executed before the page, somethings common to all pages I needed to transfer for the pages.

I thought of another possibility, perhaps simpler, was that $layout.svelte had a configuration directive that would allow to run or render of its content, every time, or instead of just once. That is, where the developer would chooses whether he wants more performance (run once, as it is today) or more reactivity (run every time, as suggested).

@tarkah
Copy link

tarkah commented Apr 29, 2021

@moisesbites You can get load to run everytime, even within a $layout page for all sub-pages, by adding the following to your load function:

// Workaround to get this function to trigger on every router change
if (page.path) {}

I'm doing the same thing where I verify the JWT everytime the user changes routes under the /private route in my app. Here is my full load function:

<script lang="ts" context="module">
	import type { Load } from '@sveltejs/kit';

	export const load: Load = async ({ page, fetch, session }) => {
		const resp = await fetch('/api/auth/verify');

		// Workaround to get this function to trigger on every router change
		if (page.path) {
			//
		}

		if (resp.status === 200) {
			return {};
		}

		session.authenticated = false;

		return {
			status: 303,
			redirect: '/login'
		};
	};
</script>

I think this has something to do with the fact that load now depends on page, which changes every time the router changes, so it causes it to trigger. But I'm not exactly sure what's going on under the hood.

@moisesbites
Copy link
Author

moisesbites commented Apr 30, 2021

if (page.path) {}

I think this has something to do with the fact that load now depends on page, which changes every time the router changes, so it causes it to trigger. But I'm not exactly sure what's going on under the hood.

It's working very well in the script section. I don't know why, too. But, for now, it's working, at least when accessing the route the first time. @tarkah Thank you very much! For now, I will leave the blocking of features to the API.

My proposal remains: a svelte section or file for middleware or trigger or preprocess before layout or before page or both to process things before the layout is rendered and before the page is rendered. This "if (page.path) {}" solution is working, but I believe the code could be more elegant and efective for "every time".

@jthegedus
Copy link
Contributor

jthegedus commented May 21, 2021

Rich originally said:

load will re-run whenever its dependencies change

And then gave an example with session:

export async function load({ session }) {
  if (!allowed(session.user)) {
    return { status: 403, error: new Error('Forbidden') };
  }

  // ...
}

Which is it that defines session as a dependency? Is it:

// param destructuring?
export async function load({ session }) {

or

// param usage?
  if (!allowed(session.user)) {

If it is the params defined in the load func, then perhaps a way around if(page.path) is to destructure path in the params?

export async function load({ page: {path} }) {

@rmunn
Copy link
Contributor

rmunn commented Jul 23, 2021

To answer @jthegedus 's question:

Which is it that defines session as a dependency?

This is now better documented (I hope) in https://kit.svelte.dev/docs#loading-inputsession is defined as a dependency of the load() function by accessing it. Deferencing it in the params will access it, so if you deference session in the params then load() will always re-run when the session changes. Accessing it inside the body of the load() function (like if (!allowed(session.user))) will also cause the dependency to be noticed and cause load() to re-run when session changes.

If it is the params defined in the load func, then perhaps a way around if(page.path) is to destructure path in the params?

export async function load({ page: {path} }) {

Yes, that will work. If you don't need to access page.path inside the load() body, then dereferencing it as a parameter to load() is enough to cause load() to re-run every time page.path changes.

@benmccann benmccann changed the title $middleware.svelte for code as $layout.svelte is for view $middleware.svelte to run code on client-side each time child page is rendered Aug 26, 2021
@homerjam
Copy link

homerjam commented Feb 8, 2022

Is there a place to run arbitrary code on the client-side that say, populates a store on page load (before/after/during render)?

It seems like I need to use __layout.svelte but that seems like a weird place (not very intuitive, seemingly unrelated).

In Nuxt I use a "plugin" for this and I can optionally delay rendering - the naming is also slightly odd but it's a broader, more generic term at least.

@Rich-Harris
Copy link
Member

It seems you can get the desired behaviour with the root __layout.svelte, albeit by 'tricking' it into re-running on every navigation by accessing url:

<script context="module">
  export async function load({ url }) {
    const res = await fetch('/user');
    const user = await res.json();

    return {
      stuff: { user }
    };
  }
</script>

We could implement a less hacky way to demand revalidation on every navigation, but I don't think it's a great idea — the whole point of load dependency tracking is to minimise the amount of work that needs to happen on navigation. Instead, we should be refreshing session periodically: #1726.

Separately, it would be useful to have a client-side entry point distinct from layouts to perform initial setup work — there's #1530 for that.

Will close this as I don't think there are any action items that aren't covered by other issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature / enhancement New feature or request router
Projects
None yet
Development

No branches or pull requests

9 participants