Files
WRNexusJS/packages/auth/src/middleware.ts
T
2026-07-30 13:36:29 +05:30

117 lines
3.7 KiB
TypeScript

import type { Context, Middleware } from "@wrnexus/core";
import type { AuthEngine } from "./engine.ts";
import { publicUser } from "./normalize.ts";
import type { AuthenticatedContext, AuthPublicUser, AuthSession } from "./types.ts";
export const AUTH_SESSION_KEY = "wrnexus.auth.session";
function wantsJson(ctx: Context): boolean {
if (ctx.url.pathname.startsWith("/api/") || ctx.url.pathname.startsWith("/__wrnexus/"))
return true;
const accept = ctx.req.headers.get("accept") ?? "";
return accept.includes("application/json") && !accept.includes("text/html");
}
export interface AuthSessionOptions {
/** Optional shared SSO cookie containing an AuthEngine session id. */
cookieName?: string;
}
export function authSession(engine: AuthEngine, options: AuthSessionOptions = {}): Middleware {
return async (ctx, next) => {
const sessionId =
(options.cookieName ? ctx.cookies.get(options.cookieName) : undefined) ??
ctx.session.get<string>(AUTH_SESSION_KEY);
if (!sessionId) {
ctx.user = null;
ctx.locals.authUser = null;
delete ctx.locals.authSession;
return next();
}
const session = await engine.validateSession(sessionId);
if (!session) {
ctx.session.delete(AUTH_SESSION_KEY);
ctx.user = null;
ctx.locals.authUser = null;
delete ctx.locals.authSession;
return next();
}
const user = await engine.getUser(session.userId);
if (!user || user.status !== "active") {
ctx.session.delete(AUTH_SESSION_KEY);
ctx.user = null;
ctx.locals.authUser = null;
delete ctx.locals.authSession;
return next();
}
const safe = publicUser(user);
ctx.user = safe;
ctx.locals.authUser = safe;
ctx.locals.authSession = session;
return next();
};
}
export function establishAuthSession(
ctx: Context,
session: AuthSession,
user: AuthPublicUser,
): void {
ctx.session.regenerate();
ctx.session.set(AUTH_SESSION_KEY, session.id);
ctx.user = user;
ctx.locals.authUser = user;
ctx.locals.authSession = session;
}
export function clearAuthSession(ctx: Context): void {
ctx.session.delete(AUTH_SESSION_KEY);
ctx.user = null;
delete ctx.locals.authUser;
delete ctx.locals.authSession;
}
export function getAuthUser(ctx: Context): AuthPublicUser | null {
return (
(ctx.locals.authUser as AuthPublicUser | null | undefined) ??
(ctx.user as AuthPublicUser | null | undefined) ??
null
);
}
export function getAuthSession(ctx: Context): AuthSession | null {
return (ctx.locals.authSession as AuthSession | undefined) ?? null;
}
export interface RequireAuthOptions {
loginPath?: string;
returnToParam?: string;
roles?: string[];
status?: AuthPublicUser["status"][];
}
export function requireAuth(options: RequireAuthOptions = {}): Middleware {
const loginPath = options.loginPath ?? "/sign-in";
const returnToParam = options.returnToParam ?? "returnTo";
return (ctx, next) => {
const user = getAuthUser(ctx);
const allowedStatus = options.status ?? ["active"];
const allowedRole =
!options.roles?.length || options.roles.some((role) => user?.roles.includes(role));
if (user && allowedStatus.includes(user.status) && allowedRole) return next();
if (wantsJson(ctx)) {
return Response.json(
{ ok: false, error: user ? "Forbidden" : "Unauthorized" },
{ status: user ? 403 : 401 },
);
}
const redirect = new URL(loginPath, ctx.url);
redirect.searchParams.set(returnToParam, `${ctx.url.pathname}${ctx.url.search}`);
return Response.redirect(redirect, 302);
};
}
export function isAuthenticatedContext(ctx: Context): ctx is AuthenticatedContext {
return Boolean(getAuthUser(ctx));
}