/**
* Admin Middleware
*
* Protects admin routes by checking:
* 1. Valid better-auth session exists
* 2. User has admin role OR email is in ADMIN_EMAILS list
*
* Uses better-auth for session management with fallback to ADMIN_EMAILS
* environment variable for initial admin bootstrap.
*/
import type { Context, Next } from 'hono';
import type { Env } from '../types';
import { createAuth } from '../lib/auth';
// Admin user info from better-auth session
export interface AdminUser {
id: string;
email: string;
name: string;
image?: string | null;
role?: string | null;
}
// Extend Hono context with admin user
declare module 'hono' {
interface ContextVariableMap {
adminUser: AdminUser;
}
}
/**
* Middleware that requires admin authentication via better-auth
*
* Checks:
* 1. User has valid better-auth session
* 2. User has 'admin' role OR email is in ADMIN_EMAILS (bootstrap)
*/
export async function requireAdmin(
c: Context<{ Bindings: Env }>,
next: Next
): Promise<Response | void> {
const auth = createAuth(c.env);
const session = await auth.api.getSession({ headers: c.req.raw.headers });
if (!session) {
return c.json({ error: 'Unauthorized: No valid session' }, 401);
}
const { user } = session;
// Check admin access: role-based OR email-based (bootstrap)
const isAdminByRole = user.role === 'admin';
const isAdminByEmail = isAdmin(user.email, c.env.ADMIN_EMAILS);
if (!isAdminByRole && !isAdminByEmail) {
return c.json({ error: 'Forbidden: Not an admin' }, 403);
}
// Store admin user in context for route handlers
c.set('adminUser', {
id: user.id,
email: user.email,
name: user.name || user.email,
image: user.image,
role: user.role,
});
await next();
}
/**
* Parse ADMIN_EMAILS env var (comma-separated, case-insensitive)
*/
export function parseAdminEmails(adminEmailsEnv: string | undefined): string[] {
if (!adminEmailsEnv) return [];
return adminEmailsEnv
.split(',')
.map((email) => email.trim().toLowerCase())
.filter((email) => email.length > 0);
}
/**
* Check if an email is in the ADMIN_EMAILS list
*/
export function isAdmin(email: string, adminEmailsEnv: string | undefined): boolean {
const adminEmails = parseAdminEmails(adminEmailsEnv);
return adminEmails.includes(email.toLowerCase());
}
/**
* Get better-auth session (for non-admin routes that still need auth)
* Uses generic type to accept any context with Env bindings
*/
export async function getSession<T extends { Bindings: Env }>(c: Context<T>) {
const auth = createAuth(c.env);
return auth.api.getSession({ headers: c.req.raw.headers });
}