elysia icon indicating copy to clipboard operation
elysia copied to clipboard

Reuse typings from within grouped functions in plugins or route handlers

Open KilianB opened this issue 1 year ago • 2 comments

This question is similar to https://github.com/elysiajs/elysia/issues/95 and https://github.com/elysiajs/elysia/issues/138 but with a slight twist.

When using a guard how can the dervied context be forwarded to external route handlers?


app.guard(
  {
    beforeHandle: async ({ bearer }) => {
      //handle validation and throw in case the user is not authenticated
      return new Response("Unauthorized. Token is invalid.", {
            status: 401,
       });
  },
  (app) => {
    //Make the jwt token available for all handlers where a valid user is present
    const ensuredAuthentication = app.derive(async ({ bearer }) => {#
      //We already did the verification in the beforeHandle, is there a way to prevent doing it a second time?
      const jwt = await jose.jwtVerify(bearer!, secret);
      return {
        jwt: jwt,
        getUser: async () => {
             ....
          );
        },
      };
    });

    //XXX -- This here is the correct type how can I reuse this within plugins?
    type AuthenticatedApp = typeof ensuredAuthentication;

    return ensuredAuthentication
      .use(dataRoutesAuthenticated)
      .use(userRoutesAuthenticated)
      .use(authRoutesAuthenticated);
  }
);


export const dataRoutesAuthenticated= (app: AuthenticatedApp)=>{
  return app.post("",({jwt})...
}

AuthenticatedApp is correctly typed, In the dataRoutes function but I can not export it since it is inside a closure.

I would be fine by typing it out manually once, but I don't understand how I can extend the Elaysia object without breaking all other types? I am loosing the overview when handling Context, Decorator Bases and all the Internal types.

//I would hope something like this would be applicable 
type AuthenticatedApp = typeof App & { request : {
   jwt: DecodedToken,
   getUser: () => Promise<User>
}}

//or even
type AuthenticatedApp = Elysia<typeof App,{request:{....}}

KilianB avatar Nov 27 '23 11:11 KilianB

I dont think Elysia is intended to support this, I found myself with this problem, have you found a way around it or just gave up?

Maybe you can adapt my solution. Instead of a guard, I use a .use custom middleware for authentication with added derived data.

const authMiddleware = (app: Elysia) =>
  app.onBeforeHandle(async (context) => {
    // handle authentication, or return 401/403 status if failed
  })
  .derive(async (context) => {
    return {
      user: {
        // attach some custom data to the context
      }
    }
  })

At this point you have 2 options (assuming you've split your app into several controllers):

  1. .use the middleware before your routes directly in each controller. This gives you the proper context, and only applies the middleware once per route.
  2. .use the middleware once near the top of your app, before the controllers. This allows you to skip using it for each controller, but you need an extra step in order to use the custom data within those controllers.

For alternative 1:

const controller = new Elysia()
  .get("/public", "I'm a public route")
  .use(authMiddleware)
  .get("/private", (context) => `I'm a private route, authenticated with ${context.user}`)

For alternative 2:

type ElysiaWithUser = ReturnType<typeof authMiddleware>
const controller = (app: ElysiaWithUser) => 
  app.get("/private", (context) => `I'm a private route, authenticated with ${context.user}`)

Then near the top of your app:

new Elysia()
  .use(authMiddleware)
  .use(controller) // controller must come after, otherwise it is public instead!
  .listen(...)

Mudbill avatar May 27 '24 10:05 Mudbill