Skip to content

Instantly share code, notes, and snippets.

@rphlmr
Last active March 22, 2024 20:45
Show Gist options
  • Save rphlmr/c7a4aa64ad47e30a99b58394db7ee2e4 to your computer and use it in GitHub Desktop.
Save rphlmr/c7a4aa64ad47e30a99b58394db7ee2e4 to your computer and use it in GitHub Desktop.
Protected routes middleware with HonoJS with Remix-Hono
import { getSession, session } from "remix-hono/session";
import { pathToRegexp } from "path-to-regexp";
/**
* Add protected routes middleware
*
*/
app.use(
protect({
onFailRedirectTo: "/login",
publicPath: [
"/login/:path*",
"/onboard/:path*",
"/refresh-session",
"/healthcheck",
],
})
);
/**
* Protected routes middleware
*
*/
function protect({
publicPath,
onFailRedirectTo,
}: {
publicPath: string[];
onFailRedirectTo: string;
}): MiddlewareHandler {
return async function middleware(context, next) {
const isPublic = pathMatch(publicPath, context.req.path);
if (isPublic) {
return next();
}
const session = getSession<SessionData, FlashData>(context);
const auth = session.get("auth");
if (!auth) {
session.flash(
"errorMessage",
"This content is only available to logged in users."
);
return context.redirect(
`${onFailRedirectTo}?redirectTo=${context.req.path}`
);
}
return next();
};
}
function pathMatch(paths: string[], requestPath: string) {
for (const path of paths) {
const regex = pathToRegexp(path);
if (regex.test(requestPath)) {
return true;
}
}
return false;
}
type SessionData = { auth: AuthSession };
type FlashData = { errorMessage: string };
/**
* Add remix middleware to Hono server
*/
app.use(
remix({
build,
mode,
getLoadContext(context) {
const session = getSession<SessionData, FlashData>(context);
return {
appVersion: build.assets.version,
isAuthenticated: session.has(authSessionKey),
// we could ensure that session.get() match a specific shape
// let's trust our system for now
getSession: () => {
const auth = session.get(authSessionKey);
if (!auth) {
throw new AppError({
cause: null,
message:
"There is no session here. This should not happen because if you require it, this route should be mark as protected and catch by the protect middleware.",
status: 403,
additionalData: {
path: context.req.path,
},
label: "Dev error 🤦‍♂️",
});
}
return auth;
},
setSession: (auth) => {
session.set(authSessionKey, auth);
},
destroySession: () => {
session.unset(authSessionKey);
},
errorMessage: session.get("errorMessage") || null,
} satisfies AppLoadContext;
},
}),
);
/**
* Declare our loaders and actions context type
*/
declare module "@remix-run/node" {
interface AppLoadContext {
/**
* The app version from the build assets
*/
readonly appVersion: string;
/**
* Whether the user is authenticated or not
*/
isAuthenticated: boolean;
/**
* Get the current session
*
* If the user is not logged it will throw an error
*
* @returns The session
*/
getSession(): SessionData["auth"];
/**
* Set the session to the session storage
*
* It will then be automatically handled by the session middleware
*
* @param session - The auth session to commit
*/
setSession(session: SessionData["auth"]): void;
/**
* Destroy the session from the session storage middleware
*
* It will then be automatically handled by the session middleware
*/
destroySession(): void;
/**
* The flash error message related to session
*/
errorMessage: string | null;
}
}
export const authSessionKey = "auth";
export type SessionData = {
[authSessionKey]: AuthSession & { deviceId?: string };
};
export type FlashData = { errorMessage: string };
/**
* Add refresh session middleware
*
*/
app.use(refreshSession());
/**
* Refresh session middleware
*
*/
function isExpiringSoon(expiresAt: number | undefined) {
if (!expiresAt) {
return true;
}
return (expiresAt - 60 * 0.1) * 1000 < Date.now(); // 1 minute left before token expires
}
/**
* Refresh access token middleware
*
*/
export function refreshSession(): MiddlewareHandler<any> {
return async function middleware(context, next) {
const session = getSession<SessionData, FlashData>(context);
const auth = session.get(authSessionKey);
if (!auth || !isExpiringSoon(auth.expiresAt)) {
return next();
}
try {
session.set(
authSessionKey,
await refreshAccessToken(auth.refreshToken),
);
} catch (cause) {
session.flash(
"errorMessage",
"You have been logged out. Please log in again.",
);
session.unset(authSessionKey);
}
return next();
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment