Repository navigation
patterns/macro #562
Replies: 4 comments 4 replies
|
bro wtf is this, how do i pass user from middleware to rest of my protected route? |
|
"Macro allows us to define a custom field to the hook." is a very insufficient explanation |
|
An example with a betterauth and a role-based auth system: export type AuthProps = {
roles: UserRole[]
} | false;
export const authPlugin = new Elysia({ name: "better-auth" })
.mount(auth.handler)
.macro("auth", (props: AuthProps) => ({
async resolve({ request, status }) {
if( ! props ) return;
const session = await auth.api.getSession({
headers: request.headers,
});
const {roles} = props;
if (!session) return status(401);
const dbUser = await db.user.findUnique({
where: { id: session.user.id },
select: { role: true },
});
if( ! dbUser )
return status(401);
let driverAccount;
if( ! roles.includes(dbUser.role) )
return status(401)
/**
* Handle driver role
*/
if( roles.includes("DRIVER") && roles.includes(dbUser?.role) )
driverAccount = await db.driverAccount.findFirst({
where: {
driver: {
userId: session.user.id,
},
},
select: {
id: true,
isSetupComplete: true,
mustChangePassword: true,
tempPassword: true,
},
});
return {
auth,
session: {
...session,
driverAccount: roles.includes("DRIVER") ? driverAccount : null,
},
user: session.user
};
},
}) );
new Elysia()
.use(authPlugin)
.get("/test-restricted", ({ session }) => {
return "I'm authed!"
}, {
auth: {
roles: ["ADMIN", "CUSTOMER"]
}
})
// no session available as param
.get("/non-authed", ({ }) => {
},{
auth: false // or just omit the whole auth object
})
// no session available as param
.get("non-authed-2", () => {
}) |
|
Another example using authentication and authorization flow: export const authPlugin = new Elysia({ name: 'better-auth' })
.use(createDbPlugin())
.macro('auth', (props: AuthProps) => ({
async resolve({ request, status, db }) {
if( ! props ) return;
/**
* Starting authed context
*/
const auth = await getAuth();
const session = await auth.api.getSession({
headers: request.headers
});
if( ! session )
return status(401);
/**
* Handle authorize and delegate roles to the userRestaurant relation table
*/
if( ! Array.isArray(props.roles) )
return {
...session
}
if( Array.isArray( props.roles) && ! props.roles.includes(session.user.role as UserRestaurantRole) )
return status(401);
let restaurant : Restaurant | null = null;
/**
* Preload restaurant to be provided in the elysia context
*/
if( session.user.selectedRestaurantId ) {
const findings = await db
.select()
.from(restaurantTable)
.where(
eq(restaurantTable.id, session.user.selectedRestaurantId)
).limit(1)
if( findings.length )
restaurant = findings.at(0)!;
}
return {
...session,
restaurant: restaurant
}
}
}))
.macro({
authorize: ({ restaurantIdParam = 'restaurantId', roles }: AuthorizeProps) => {
return {
auth: {
roles: true
},
resolve: async (ctx) => {
const { status, params, db, session } = ctx as typeof ctx & {
session?: Awaited<ReturnType<typeof getAuth>>['$Infer']['Session'];
params: {
restaurantId?: string
}
};
if( ! session )
return ctx.status(401);
const rid = params.restaurantId
?? session.user.selectedRestaurantId
if (!rid) {
return { roleInRestaurant: session.user.role }
}
const membership = await db
.select({ role: userRestaurant.role })
.from(userRestaurant)
.where(and(
eq(userRestaurant.userId, session.user.id),
eq(userRestaurant.restaurantId, rid),
))
.limit(1)
if (membership.length === 0)
return status(403, 'Non sei membro di questo ristorante')
const userRole = membership[0].role;
const requiredMinLevel = Math.min(
...roles.map(r => roleHierarchy[r]),
)
if (roleHierarchy[userRole] < requiredMinLevel)
return status(403, 'Ruolo insufficiente per questa risorsa')
return {
roleInRestaurant: userRole,
}
}
}
}
}) Example usage: export const healthRoute = new Elysia({ prefix: '/restaurant' })
.use(authPlugin)
.get('/', () => ({ status: 'ok' }), {
detail: { summary: 'Health check endpoint' },
})
.get('/:restaurantId', ({session, user, roleInRestaurant}) => ({ status: 'ok' }), {
//check auth to properly expose session and user in the ctx
auth: {
roles: true
},
//define granular role access depending on the :restaurantId param
authorize: {
roles: ["OWNER"]
},
detail: { summary: 'Health check endpoint' },
})That's the most Dev friendly solution I was able to use in my project. Macro chaining is not propely able to infer the types, even if for example the auth macro is available in the authorize one. Type infer is not working, but if you console.log the ctx, session and user objects are available, so providing a proper typization into the "child" macro will solve the issue. Also, even if both the route and the authorization macro are defining |
Uh oh!
There was an error while loading. Please reload this page.
patterns/macro
Macro allows us to define a custom field to the hook.
https://elysiajs.com/patterns/macro
All reactions