Last active
August 31, 2024 22:06
-
-
Save rphlmr/c7a4aa64ad47e30a99b58394db7ee2e4 to your computer and use it in GitHub Desktop.
Protected routes middleware with HonoJS with Remix-Hono
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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