Created
May 9, 2025 00:28
-
-
Save lunaperegrina/00cefccb2dbcdc6d6f78b69b759ab6f8 to your computer and use it in GitHub Desktop.
better-auth integration with elysia :)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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