Initial commit: Setup Bun, Elysia, Vite, React, TanStack Router, Mantine, and Biome
This commit is contained in:
95
src/middleware/apiMiddleware.tsx
Normal file
95
src/middleware/apiMiddleware.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import type Elysia from "elysia";
|
||||
import { auth } from "@/utils/auth";
|
||||
import { prisma } from "@/utils/db";
|
||||
import logger from "@/utils/logger";
|
||||
|
||||
export function apiMiddleware(app: Elysia) {
|
||||
return app
|
||||
.derive(async ({ request }) => {
|
||||
const headers = request.headers;
|
||||
|
||||
// First, try to get user from session (Better Auth)
|
||||
const userSession = await auth.api.getSession({
|
||||
headers,
|
||||
});
|
||||
|
||||
if (userSession?.user) {
|
||||
// Return user data from session if authenticated via session
|
||||
return {
|
||||
user: {
|
||||
...userSession.user,
|
||||
id: userSession.user.id,
|
||||
email: userSession.user.email,
|
||||
name: userSession.user.name,
|
||||
image: userSession.user.image,
|
||||
emailVerified: userSession.user.emailVerified,
|
||||
role: userSession.user.role || "user",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// If no session, try API key authentication
|
||||
let apiKey = headers.get("x-api-key");
|
||||
|
||||
if (!apiKey) {
|
||||
// Also check Authorization header for API key
|
||||
const authHeader =
|
||||
headers.get("authorization") || headers.get("Authorization");
|
||||
if (authHeader?.startsWith("Bearer ")) {
|
||||
apiKey = authHeader.substring(7);
|
||||
}
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
return { user: null };
|
||||
}
|
||||
|
||||
try {
|
||||
// Look up the API key in the database
|
||||
const apiKeyRecord = await prisma.apiKey.findFirst({
|
||||
where: {
|
||||
key: apiKey,
|
||||
isActive: true,
|
||||
},
|
||||
include: {
|
||||
user: true, // Include the associated user
|
||||
},
|
||||
});
|
||||
|
||||
if (!apiKeyRecord) {
|
||||
return { user: null };
|
||||
}
|
||||
|
||||
// Check if API key has expired
|
||||
if (
|
||||
apiKeyRecord.expiresAt &&
|
||||
new Date(apiKeyRecord.expiresAt) < new Date()
|
||||
) {
|
||||
logger.info({ keyId: apiKeyRecord.id }, "[AUTH] API key expired");
|
||||
return { user: null };
|
||||
}
|
||||
|
||||
// Return the associated user data
|
||||
return {
|
||||
user: {
|
||||
id: apiKeyRecord.user.id,
|
||||
email: apiKeyRecord.user.email,
|
||||
name: apiKeyRecord.user.name,
|
||||
image: apiKeyRecord.user.image,
|
||||
emailVerified: apiKeyRecord.user.emailVerified,
|
||||
role: apiKeyRecord.user.role || "user",
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
logger.warn({ err }, "[AUTH] Error verifying API key");
|
||||
return { user: null };
|
||||
}
|
||||
})
|
||||
.onBeforeHandle(({ user, set, request }) => {
|
||||
if (!user) {
|
||||
logger.warn(`[AUTH] Unauthorized: ${request.method} ${request.url}`);
|
||||
set.status = 401;
|
||||
return { message: "Unauthorized" };
|
||||
}
|
||||
});
|
||||
}
|
||||
125
src/middleware/authMiddleware.tsx
Normal file
125
src/middleware/authMiddleware.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import { redirect } from "@tanstack/react-router";
|
||||
|
||||
/* ================================
|
||||
* Types
|
||||
* ================================ */
|
||||
|
||||
type UserRole = "user" | "admin";
|
||||
|
||||
type SessionUser = {
|
||||
id: string;
|
||||
role: UserRole;
|
||||
};
|
||||
|
||||
type SessionResponse = {
|
||||
user?: SessionUser;
|
||||
};
|
||||
|
||||
/* ================================
|
||||
* Session Fetcher
|
||||
* ================================ */
|
||||
|
||||
async function fetchSession(): Promise<SessionResponse | null> {
|
||||
try {
|
||||
const baseURL =
|
||||
import.meta.env.VITE_PUBLIC_URL || window.location.origin;
|
||||
const res = await fetch(`${baseURL}/api/session`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (!res.ok) return null;
|
||||
|
||||
const { data } = await res.json();
|
||||
return data as SessionResponse;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/* ================================
|
||||
* Redirect Helper
|
||||
* ================================ */
|
||||
|
||||
function redirectToLogin(to: string, currentHref: string) {
|
||||
throw redirect({
|
||||
to,
|
||||
search: { redirect: currentHref },
|
||||
});
|
||||
}
|
||||
|
||||
/* ================================
|
||||
* Route Rules (Pattern Based)
|
||||
* ================================ */
|
||||
|
||||
type RouteRule = {
|
||||
match: (pathname: string) => boolean;
|
||||
requireAuth?: boolean;
|
||||
requiredRole?: UserRole;
|
||||
redirectTo?: string;
|
||||
};
|
||||
|
||||
const routeRules: RouteRule[] = [
|
||||
{
|
||||
match: (p) => p === "/profile" || p.startsWith("/profile/"),
|
||||
requireAuth: true,
|
||||
redirectTo: "/signin",
|
||||
},
|
||||
{
|
||||
match: (p) => p === "/dashboard" || p.startsWith("/dashboard/"),
|
||||
requireAuth: true,
|
||||
requiredRole: "admin",
|
||||
redirectTo: "/profile",
|
||||
},
|
||||
];
|
||||
|
||||
/* ================================
|
||||
* Rule Resolver
|
||||
* ================================ */
|
||||
|
||||
function findRouteRule(pathname: string): RouteRule | undefined {
|
||||
return routeRules.find((rule) => rule.match(pathname));
|
||||
}
|
||||
|
||||
/* ================================
|
||||
* Protected Route Factory
|
||||
* ================================ */
|
||||
|
||||
export interface ProtectedRouteOptions {
|
||||
redirectTo?: string;
|
||||
}
|
||||
|
||||
export function createProtectedRoute(options: ProtectedRouteOptions = {}) {
|
||||
const { redirectTo = "/signin" } = options;
|
||||
|
||||
return async ({
|
||||
location,
|
||||
}: {
|
||||
location: { pathname: string; href: string };
|
||||
}) => {
|
||||
const rule = findRouteRule(location.pathname);
|
||||
if (!rule) return;
|
||||
|
||||
const session = await fetchSession();
|
||||
const user = session?.user;
|
||||
|
||||
if (rule.requireAuth && !user) {
|
||||
redirectToLogin(rule.redirectTo ?? redirectTo, location.href);
|
||||
}
|
||||
|
||||
if (rule.requiredRole && user?.role !== rule.requiredRole) {
|
||||
redirectToLogin(rule.redirectTo ?? redirectTo, location.href);
|
||||
}
|
||||
|
||||
return {
|
||||
session,
|
||||
user,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/* ================================
|
||||
* Default Middleware Export
|
||||
* ================================ */
|
||||
|
||||
export const protectedRouteMiddleware = createProtectedRoute();
|
||||
Reference in New Issue
Block a user