Skip to content

Instantly share code, notes, and snippets.

@lunaperegrina
Created May 9, 2025 00:28
Show Gist options
  • Save lunaperegrina/00cefccb2dbcdc6d6f78b69b759ab6f8 to your computer and use it in GitHub Desktop.
Save lunaperegrina/00cefccb2dbcdc6d6f78b69b759ab6f8 to your computer and use it in GitHub Desktop.
better-auth integration with elysia :)
// index.ts
// src/index.ts
import { Elysia, status } from 'elysia'
import { cors } from '@elysiajs/cors'
import { swagger } from '@elysiajs/swagger'
import UserController from '@/modules/users/controller'
import { rateLimit } from 'elysia-rate-limit'
import { createBullBoard } from '@bull-board/api';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { ElysiaAdapter } from '@bull-board/elysia';
import { emailQueue } from '@/lib/queue';
import { betterAuth } from "@/lib/auth/auth-macros";
import { auth, OpenAPI } from "@/auth";
import { logger } from '@grotto/logysia';
import staticPlugin from '@elysiajs/static'
import { EncryptedController } from '@/modules/encrypted-example/controller'
import { AsaasController } from '@/modules/asaas-example/controller'
const mailQueueAdapter = new ElysiaAdapter('/ui');
const openApiComponents = await OpenAPI.components;
const openApiPaths = await OpenAPI.getPaths();
createBullBoard({
queues: [new BullMQAdapter(emailQueue)],
serverAdapter: mailQueueAdapter,
options: {
// This configuration fixes a build error on Bun caused by eval (https://github.com/oven-sh/bun/issues/5809#issuecomment-2065310008)
uiBasePath: '../../node_modules/@bull-board/ui',
},
});
const app = new Elysia()
.onError(({ error, code, request }) => {
console.error('Error details:', {
error,
code,
method: request.method,
url: request.url,
headers: request.headers
});
if (code === 'NOT_FOUND') return new Response('Route not found', { status: 404 });
})
.use(logger({
logIP: true,
writer: {
write(msg: string) {
console.log(msg)
}
}
}))
// .use(cors({
// origin: process.env.BASE_API_URL,
// methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
// credentials: true,
// allowedHeaders: ["Content-Type", "Authorization"],
// }))
.use(staticPlugin())
.mount('auth', auth.handler)
.use(betterAuth)
// .group('/intern', (app) =>
// app
// // .onBeforeHandle(async ({ set, request: { headers }, status }) => {
// // const session = await auth.api.getSession({
// // headers,
// // });
// // if (!session) return status(401);
// // if (!session.user.flags.includes('admin')) {
// // set.status = 401;
// // return 'Unauthorized';
// // }
// // })
// .use(mailQueueAdapter.registerPlugin())
// .use(
// swagger({
// documentation: {
// components: openApiComponents,
// paths: openApiPaths
// },
// path: "/swagger",
// swaggerOptions: {
// configUrl: "/swagger.json"
// }
// })
// )
// )
.use(UserController)
.use(EncryptedController)
.use(AsaasController)
.get('', () => {
return {
message: 'Hello World!'
}
})
.get('/public/hello', () => 'Hello World in public route!')
.get('/private/hello', ({ body }) => {
console.log(body)
console.log('private hello!')
return 'Hello World!'
}, {
requireFlags: ['admin']
})
.use(
swagger({
documentation: {
components: openApiComponents,
paths: openApiPaths
},
path: "/swagger",
swaggerOptions: {
configUrl: "/swagger.json"
}
})
)
.use(mailQueueAdapter.registerPlugin())
// .use(rateLimit())
.listen(3001, ({ url, port }) => {
console.log(`🦊 Running on ${url.hostname}:${port}...`);
console.log(`For the UI of bullmq, open http://localhost:${port}/ui`);
console.log(`For the Swagger, open http://localhost:${port}/swagger`);
// console.log('Make sure Redis is running on port 6379 by default');
// console.log('To populate the queue, run:');
// console.log(` curl http://localhost:${port}/add?title=Example`);
});
export { app }
---------------------------------------------------------------
// auth.ts
import { prismaAdapter } from "better-auth/adapters/prisma";
import { prisma } from "@/lib/prisma";
import { magicLink, emailOTP, twoFactor } from "better-auth/plugins";
import { betterAuth } from "better-auth";
import { openAPI } from 'better-auth/plugins'
import { organization } from "better-auth/plugins"
import { emailQueue } from "@/lib/queue";
import { createAuthMiddleware } from "better-auth/api";
import { sendNotification } from "@/lib/observabilty/notifications"
export const auth = betterAuth({
secret: process.env.BETTER_AUTH_SECRET,
appName: process.env.BASE_API_NAME || "elysia-boilerplate",
// baseURL: `${process.env.BASE_API_URL}/secure`,
basePath: '/secure',
// basePath: "/auth/secure",
session: {
cookieCache: {
enabled: true,
maxAge: 5 * 60 // Cache duration in seconds
}
},
rateLimit: {
window: 10, // time window in seconds
max: 100, // max requests in the window
},
hooks: {
after: createAuthMiddleware(async (ctx) => {
if (ctx.path.startsWith("/sign-up")) {
const newSession = ctx.context.newSession;
if (newSession) {
sendNotification({
type: "user-register",
name: newSession.user.name,
})
}
}
}),
},
emailVerification: {
sendOnSignUp: false,
sendVerificationEmail: async ({ user, token, url }, request) => {
console.log("---> send verification email:", user, token, url)
await emailQueue.add('emailQueue',
{
emailData: {
to: user.email,
from: '[email protected]',
subject: 'Verify your email',
html: `Click the link to verify your email: ${url}`
}
})
},
},
advanced: {
cookiePrefix: `x-${process.env.BASE_API_NAME}` || "x-elysia-boilerplate",
},
user: {
additionalFields: {
flags: {
type: "string[]",
required: false,
defaultValue: ["user:read"],
},
lang: {
type: "string",
required: false,
defaultValue: "en",
},
},
},
database: prismaAdapter(prisma, {
provider: "postgresql",
}),
emailAndPassword: {
enabled: true,
minPasswordLength: 8,
sendResetPassword: async ({ user, url, token }, request) => {
console.log("---> send reset password:", user, url, token)
await emailQueue.add('emailQueue',
{
emailData: {
to: user.email,
from: '[email protected]',
subject: 'Reset your password',
html: `Click the link to reset your password: ${url}`
}
})
}
},
socialProviders: {
google: {
redirectURI: `${process.env.BASE_API_URL}/api/auth/callback/google`,
clientId: process.env.GOOGLE_CLIENT_ID || "",
clientSecret: process.env.GOOGLE_CLIENT_SECRET || "",
}
},
plugins: [
openAPI(),
organization(),
emailOTP({
async sendVerificationOTP({ email, otp, type }) {
console.log("---> send otp:", email, otp, type)
await emailQueue.add('emailQueue',
{
emailData: {
to: email,
from: '[email protected]',
subject: 'Verify your email',
html: `Your verification code is: ${otp}`
}
})
},
}),
magicLink({
sendMagicLink: async ({ email, token, url }, request) => {
console.log("---> send magic link:", email, token, url)
await emailQueue.add('emailQueue',
{
emailData: {
to: email,
from: '[email protected]',
subject: 'Verify your email',
html: `Click the link to verify your email: ${url}`
}
})
}
}),
twoFactor()
]
});
let _schema: ReturnType<typeof auth.api.generateOpenAPISchema>
const getSchema = async () => (_schema ??= auth.api.generateOpenAPISchema())
export const OpenAPI = {
getPaths: (prefix = '/auth/secure') =>
getSchema().then(({ paths }) => {
const reference: typeof paths = Object.create(null)
for (const path of Object.keys(paths)) {
const key = prefix + path
reference[key] = paths[path]
for (const method of Object.keys(paths[path])) {
const operation = (reference[key] as any)[method]
operation.tags = ['Better Auth']
}
}
return reference
}) as Promise<any>,
components: getSchema().then(({ components }) => components) as Promise<any>
} as const
-----------------------------------------------------------------
// auth-client.ts
import { createAuthClient } from "better-auth/client";
import { auth } from "@/auth";
import {
inferAdditionalFields,
magicLinkClient,
genericOAuthClient,
organizationClient,
emailOTPClient,
twoFactorClient
} from "better-auth/client/plugins";
export const authClient = createAuthClient({
// baseURL: `${process.env.BASE_API_URL}/auth`,
// basePath: '/',
baseURL: `${process.env.BASE_API_URL}/auth/secure`,
plugins: [
inferAdditionalFields<typeof auth>(),
magicLinkClient(),
genericOAuthClient(),
organizationClient(),
emailOTPClient(),
magicLinkClient(),
twoFactorClient()
],
});
----------------------------------------------------------------
// auth-macros.ts
import { auth } from "@/auth";
import { Elysia } from "elysia";
const betterAuth = new Elysia({ name: "better-auth" })
.macro({
basicAuth: (enabled: boolean) => ({
async resolve({ status, request: { headers } }) {
if (!enabled) return
const session = await auth.api.getSession({
headers,
});
if (!session) return status(401);
return {
user: session.user,
session: session.session,
};
},
}),
requireFlags: (flags: string[]) => ({
async resolve({ status, request: { headers } }) {
if (!flags.length) return
const session = await auth.api.getSession({
headers,
});
if (!session) return status(401);
const hasAllFlags = flags.every(flag => session.user.flags?.includes(flag));
if (!hasAllFlags) {
return status(403);
}
return {
user: session.user,
session: session.session,
};
},
}),
})
export { betterAuth }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment