Skip to content

Commit

Permalink
♻️ Chore: Rewrite dashboard middleware (#178)
Browse files Browse the repository at this point in the history
* Create draft PR for #177
[skip ci]

* Reimplement middleware

* Fix packages

* Refactor to use dashboardRoute

---------

Co-authored-by: Adam Matthiesen <30383579+Adammatthiesen@users.noreply.github.com>
Co-authored-by: Jacob Jenkins <7649031+jdtjenkins@users.noreply.github.com>
Co-authored-by: Reuben Tier <64310361+TheOtterlord@users.noreply.github.com>
Co-authored-by: Paul Valladares <85648028+dreyfus92@users.noreply.github.com>
  • Loading branch information
5 people authored Jul 22, 2024
1 parent 8e9d881 commit 73beed8
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 85 deletions.
20 changes: 13 additions & 7 deletions packages/studioCMS/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
"type": "git",
"url": "git+https://github.com/astrolicious/studiocms.git"
},
"contributors": ["Adammatthiesen", "jdtjenkins", "dreyfus92"],
"contributors": [
"Adammatthiesen",
"jdtjenkins",
"dreyfus92"
],
"license": "MIT",
"keywords": [
"astro",
Expand Down Expand Up @@ -43,11 +47,11 @@
"dependencies": {
"@astrojs/markdown-remark": "catalog:",
"@cloudinary/url-gen": "catalog:studiocms",
"@iconify-json/mdi": "catalog:studiocms",
"@iconify-json/logos": "catalog:studiocms",
"@iconify-json/mdi": "catalog:studiocms",
"@iconify/utils": "catalog:studiocms",
"@inox-tools/sitemap-ext": "catalog:studiocms",
"@inox-tools/runtime-logger": "catalog:studiocms",
"@inox-tools/sitemap-ext": "catalog:studiocms",
"@markdoc/markdoc": "catalog:studiocms",
"@matthiesenxyz/astrolace": "catalog:studiocms",
"@matthiesenxyz/unocss-preset-daisyui": "catalog:studiocms",
Expand All @@ -68,20 +72,22 @@
"marked-footnote": "catalog:studiocms",
"marked-shiki": "catalog:studiocms",
"marked-smartypants": "catalog:studiocms",
"micromatch": "catalog:studiocms",
"mrmime": "catalog:studiocms",
"oslo": "catalog:studiocms",
"shiki": "catalog:studiocms",
"unpic": "catalog:studiocms",
"unocss": "catalog:studiocms"
"unocss": "catalog:studiocms",
"unpic": "catalog:studiocms"
},
"peerDependencies": {
"@astrojs/db": "catalog:min",
"astro": "catalog:min"
},
"devDependencies": {
"@iconify/types": "catalog:studiocms",
"@types/micromatch": "catalog:studiocms",
"@types/node": "catalog:",
"typescript": "catalog:",
"vite": "catalog:",
"@iconify/types": "catalog:studiocms"
"vite": "catalog:"
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { User, db, eq } from 'astro:db';
import { defineMiddleware } from 'astro/middleware';
import { verifyRequestOrigin } from 'lucia';
import { lucia } from 'studiocms-dashboard:auth';
import type { Locals } from 'studiocms:helpers';
import Config from 'virtual:studiocms/config';
import { logger } from '@it-astro:logger:StudioCMS';
import { defineMiddlewareRouter } from './router';
import type { MiddlewareHandler } from 'astro';

const {
dashboardConfig: {
Expand All @@ -15,87 +16,89 @@ const {

/**
* This function is used to remove any trailing and leading slashes from a string
*
*
* @param {string} str - The string to remove slashes from
*
*
* @returns {string} - The string without any trailing or leading slashes
*
*
* @example
* stripSlashes('/dashboard/') // 'dashboard'
*/
const stripSlashes = (str: string): string => str.replace(/^\/+|\/+$/g, '');

export const onRequest = defineMiddleware(async (context, next) => {
if (context.request.method !== 'GET') {
const originHeader = context.request.headers.get('Origin');
const hostHeader = context.request.headers.get('Host');
if (!originHeader || !hostHeader || !verifyRequestOrigin(originHeader, [hostHeader])) {
return new Response(null, {
status: 403,
});
}
}

const sessionId = context.cookies.get(lucia.sessionCookieName)?.value ?? null;

const locals = context.locals as Locals;

locals.isLoggedIn = false;
if (!sessionId) {
locals.user = null;
locals.session = null;
return next();
}

const { session, user } = await lucia.validateSession(sessionId);

if (!session || session === null) {
const sessionCookie = lucia.createBlankSessionCookie();
context.cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
return next();
}

const isSessionFresh = session.expiresAt.getTime() > new Date().getTime();
session.fresh = isSessionFresh;

if (session.fresh) {
const sessionCookie = lucia.createSessionCookie(session.id);
context.cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
const dbUser = await db.select().from(User).where(eq(User.id, user.id)).get();

locals.dbUser = dbUser;
locals.isLoggedIn = true;
} else if (session && !session.fresh) {
const sessionCookie = lucia.createBlankSessionCookie();
await lucia.invalidateSession(session.id);
context.cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
}

locals.session = session;
locals.user = user;
const urlPathname = context.url.pathname;

const dashboardRoute = dashboardRouteOverride ? stripSlashes(dashboardRouteOverride) : 'dashboard';

if (
urlPathname.startsWith(`/${dashboardRoute}`) &&
(!urlPathname.startsWith(`/${dashboardRoute}/login`) ||
!urlPathname.startsWith(`/${dashboardRoute}/signup`))
) {
if (!testingAndDemoMode) {
if (!locals.isLoggedIn) {
logger.info('User is not logged in... Redirecting to login page');
return new Response(null, {
status: 302,
headers: {
Location: `/${dashboardRoute}/login`,
},
});
}
} else {
logger.info('Testing and Demo mode is enabled. Skipping login check');
}
}

return next();
});
const dashboardRoute = dashboardRouteOverride ? stripSlashes(dashboardRouteOverride) : 'dashboard';

const router: Record<string, MiddlewareHandler> = {}
router['/**'] = async (context, next) => {
if (context.request.method !== 'GET') {
const originHeader = context.request.headers.get('Origin');
const hostHeader = context.request.headers.get('Host');
if (!originHeader || !hostHeader || !verifyRequestOrigin(originHeader, [hostHeader])) {
return new Response(null, {
status: 403,
});
}
}

const sessionId = context.cookies.get(lucia.sessionCookieName)?.value ?? null;

const locals = context.locals as Locals;

locals.isLoggedIn = false;
if (!sessionId) {
locals.user = null;
locals.session = null;
return next();
}

const { session, user } = await lucia.validateSession(sessionId);

if (!session || session === null) {
const sessionCookie = lucia.createBlankSessionCookie();
context.cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
return next();
}

const isSessionFresh = session.expiresAt.getTime() > new Date().getTime();
session.fresh = isSessionFresh;

if (session.fresh) {
const sessionCookie = lucia.createSessionCookie(session.id);
context.cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
const dbUser = await db.select().from(User).where(eq(User.id, user.id)).get();

locals.dbUser = dbUser;
locals.isLoggedIn = true;
} else if (session && !session.fresh) {
const sessionCookie = lucia.createBlankSessionCookie();
await lucia.invalidateSession(session.id);
context.cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
}

locals.session = session;
locals.user = user;

return next();
}

router[`/${dashboardRoute}/!(login|signup)**`] = async (context, next) => {
const locals = context.locals as Locals;

if (!testingAndDemoMode) {
if (!locals.isLoggedIn) {
logger.info('User is not logged in... Redirecting to login page');
return new Response(null, {
status: 302,
headers: {
Location: `/${dashboardRoute}/login`,
},
});
}
} else {
logger.info('Testing and Demo mode is enabled. Skipping login check');
}

return next();
}

export const onRequest = defineMiddlewareRouter(router);
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { MiddlewareHandler } from 'astro';
import { defineMiddleware, sequence } from 'astro/middleware';
import micromatch from 'micromatch';

export function defineMiddlewareRouter(router: Record<string, MiddlewareHandler>) {
const entries = Object.entries(router);
return defineMiddleware((context, next) => {
return sequence(
...entries
.filter(([path]) => micromatch.isMatch(context.url.pathname, path))
.map(([_, handler]) => handler)
)(context, next);
});
}
25 changes: 25 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ catalogs:
'@noble/hashes': ^1.4.0
'@shikijs/transformers': ^1.10.3
'@shoelace-style/shoelace': ^2.15.1
'@types/micromatch': ^4.0.9
'@unocss/astro': ^0.61.0
'@unocss/reset': ^0.61.0
unocss: ^0.61.0
Expand All @@ -67,6 +68,7 @@ catalogs:
marked-footnote: ^1.2.2
marked-shiki: ^1.1.0
marked-smartypants: ^1.1.7
micromatch: ^4.0.7
mrmime: ^2.0.0
oslo: ^1.2.1
shiki: ^1.10.3
Expand Down

0 comments on commit 73beed8

Please sign in to comment.