diff --git a/app/api/[[...slugs]]/route.ts b/app/api/[[...slugs]]/route.ts index e16d1bd..48dd0a5 100644 --- a/app/api/[[...slugs]]/route.ts +++ b/app/api/[[...slugs]]/route.ts @@ -1,6 +1,26 @@ import { Elysia } from "elysia" +import { auth } from "@/lib/auth" 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" }) .onError(({ code, error, set, request }) => { 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", } }) + .use(betterAuth) .use(healthRoutes) .get("/", () => ({ name: "DeckyVault API", diff --git a/lib/auth/guard.ts b/lib/auth/guard.ts new file mode 100644 index 0000000..011c0d1 --- /dev/null +++ b/lib/auth/guard.ts @@ -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 { + 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 { + 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 { + return requireRole(headers, ["admin"]) +} + +export async function requireContributorOrAdmin( + headers: Headers, +): Promise { + return requireRole(headers, ["contributor", "admin"]) +}