feat(auth): mount Better-Auth on Elysia server with session macro and role guards

This commit is contained in:
2026-04-25 13:42:07 +08:00
parent 8d058f02dd
commit f8aa64bd32
2 changed files with 76 additions and 0 deletions
+21
View File
@@ -1,6 +1,26 @@
import { Elysia } from "elysia" import { Elysia } from "elysia"
import { auth } from "@/lib/auth"
import { healthRoutes } from "@/lib/api/health" import { healthRoutes } from "@/lib/api/health"
const betterAuth = new Elysia({ name: "better-auth" })
.mount(auth.handler)
.macro({
auth: {
async resolve({ status, request: { headers } }) {
const session = await auth.api.getSession({
headers,
})
if (!session) return status(401)
return {
user: session.user,
session: session.session,
}
},
},
})
export const app = new Elysia({ prefix: "/api" }) export const app = new Elysia({ prefix: "/api" })
.onError(({ code, error, set, request }) => { .onError(({ code, error, set, request }) => {
console.error(`[API Error] ${code} ${request.url}`, console.error(`[API Error] ${code} ${request.url}`,
@@ -11,6 +31,7 @@ export const app = new Elysia({ prefix: "/api" })
error: code === "NOT_FOUND" ? "Not found" : "Internal server error", error: code === "NOT_FOUND" ? "Not found" : "Internal server error",
} }
}) })
.use(betterAuth)
.use(healthRoutes) .use(healthRoutes)
.get("/", () => ({ .get("/", () => ({
name: "DeckyVault API", name: "DeckyVault API",
+55
View File
@@ -0,0 +1,55 @@
import { auth } from "@/lib/auth"
import type { Session } from "better-auth"
/**
* Imperative permission guards for use inside route handlers or non-Elysia
* contexts. For blanket authentication on an Elysia route, prefer the `auth`
* macro defined in `app/api/[[...slugs]]/route.ts`.
*/
type User = typeof auth.$Infer.Session.user
type GuardResult =
| { ok: true; user: User; session: Session }
| { ok: false; error: string; status: number }
export async function requireAuth(headers: Headers): Promise<GuardResult> {
const session = await auth.api.getSession({ headers })
if (!session) {
return { ok: false, error: "Unauthorized", status: 401 }
}
return {
ok: true,
user: session.user,
session: session.session,
}
}
export async function requireRole(
headers: Headers,
roles: string[],
): Promise<GuardResult> {
const authResult = await requireAuth(headers)
if (!authResult.ok) return authResult
const userRole = authResult.user.role ?? "user"
if (!roles.includes(userRole)) {
return { ok: false, error: "Forbidden", status: 403 }
}
return authResult
}
export async function requireAdmin(headers: Headers): Promise<GuardResult> {
return requireRole(headers, ["admin"])
}
export async function requireContributorOrAdmin(
headers: Headers,
): Promise<GuardResult> {
return requireRole(headers, ["contributor", "admin"])
}