studiocms
Version:
Astro Native CMS for AstroDB. Built from the ground up by the Astro community.
184 lines (163 loc) • 6.37 kB
text/typescript
/* v8 ignore start */
/*
This file is excluded from v8 coverage reports because it is not
feasible to test the middleware in a unit test environment.
The middleware relies on the full application context and
interactions that are better suited for integration or end-to-end testing.
*/
import crypto from 'node:crypto';
import { User, VerifyEmail } from 'studiocms:auth/lib';
import { dashboardConfig } from 'studiocms:config';
import { defaultLang } from 'studiocms:i18n';
import { StudioCMSRoutes } from 'studiocms:lib';
import { SDKCore } from 'studiocms:sdk';
import SCMSUiVersion from 'studiocms:ui/version';
import SCMSVersion from 'studiocms:version';
import { STUDIOCMS_EDITOR_CSRF_COOKIE_NAME } from '../consts.js';
import { defineMiddlewareRouter, Effect } from '../effect.js';
import { getUserPermissions, makeFallbackSiteConfig, SetLocal, setLocals } from './utils.js';
// Import the dashboard route override from the configuration
// If no override is set, it defaults to 'dashboard'
// This allows for flexibility in the dashboard route without hardcoding it
const dashboardRoute = dashboardConfig.dashboardRouteOverride || 'dashboard';
/**
* Main middleware sequence for StudioCMS.
*
* This middleware sets up the base context locals for StudioCMS, including the generator version,
* site configuration, route map, and default language. It also handles user session data,
* email verification status, and user permission levels for the dashboard routes.
* Additionally, it manages CSRF token setup for the editor and ensures that the user is authenticated
* for dashboard routes, redirecting to the login page if not authenticated.
*/
export const onRequest = defineMiddlewareRouter([
{
/**
* Middleware function to handle the main locals setup for StudioCMS.
* This middleware sets the base context locals for StudioCMS, including the generator version,
* site configuration, route map, and default language.
*/
includePaths: ['/**'],
excludePaths: ['/_studiocms-devapps/**', '/_web-vitals**'],
priority: 1,
handler: Effect.fn(function* (context, next) {
const {
GET,
MIDDLEWARES: { verifyCache },
} = yield* SDKCore;
if (!['/studiocms_api/dashboard/verify-session'].includes(context.url.pathname)) {
yield* verifyCache();
}
const [latestVersion, siteConfig] = yield* Effect.all([
GET.latestVersion(),
GET.siteConfig(),
]);
// Set the StudioCMS base context locals
yield* setLocals(context, SetLocal.GENERAL, {
SCMSGenerator: `StudioCMS v${SCMSVersion}`,
SCMSUiGenerator: `StudioCMS UI v${SCMSUiVersion}`,
siteConfig: siteConfig ?? makeFallbackSiteConfig(),
routeMap: StudioCMSRoutes,
defaultLang,
latestVersion,
});
return next();
}),
},
{
/**
* Middleware function to handle the main route for the dashboard.
* This middleware sets up the user session data, email verification status,
* and user permission levels for the dashboard routes.
*/
includePaths: [`/${dashboardRoute}/**`, '/studiocms_api/**'],
priority: 2,
handler: Effect.fn(function* (context, next) {
const { getUserData, isEmailVerificationEnabled } = yield* Effect.gen(function* () {
const [{ getUserData }, { isEmailVerificationEnabled }] = yield* Effect.all([
User,
VerifyEmail,
]);
return { getUserData, isEmailVerificationEnabled };
}).pipe(VerifyEmail.Provide);
// Retrieve the user session data from the context locals or fetch it
const [userSessionData, emailVerificationEnabled] = yield* Effect.all([
getUserData(context),
isEmailVerificationEnabled(),
]);
// Get the user permission levels based on the session data
const userPermissionLevel = yield* getUserPermissions(userSessionData);
// Set the security-related data in the context locals
yield* setLocals(context, SetLocal.SECURITY, {
userSessionData,
emailVerificationEnabled,
userPermissionLevel,
});
// Continue to the next middleware
return next();
}),
},
{
/**
* Middleware function to handle user authentication for the dashboard.
* This middleware checks if the user is logged in and redirects to the login page if not
* authenticated. It also excludes certain paths from this check, such as login, signup,
* logout, and forgot password routes.
*/
includePaths: [`/${dashboardRoute}/**`],
excludePaths: [
`/${dashboardRoute}/login`,
`/${dashboardRoute}/login/**`,
`/${dashboardRoute}/signup`,
`/${dashboardRoute}/signup/**`,
`/${dashboardRoute}/logout`,
`/${dashboardRoute}/logout/**`,
`/${dashboardRoute}/forgot-password`,
`/${dashboardRoute}/forgot-password/**`,
],
priority: 3,
handler: Effect.fn(function* (context, next) {
const getUserData = yield* Effect.gen(function* () {
const { getUserData } = yield* User;
return getUserData;
});
// Retrieve the user session data from the context locals or fetch it
const userSessionData =
context.locals.StudioCMS.security?.userSessionData ?? (yield* getUserData(context));
// Check if the user is logged in and redirect to the login page if not
if (!userSessionData.isLoggedIn) return context.redirect(StudioCMSRoutes.authLinks.loginURL);
// Else, Continue to the next middleware
return next();
}),
},
{
/**
* Middleware function to handle the CSRF token setup for the editor.
* This middleware generates a CSRF token, sets it as a cookie, and updates the context locals
* with the CSRF token for use in the editor.
*/
includePaths: [`/${dashboardRoute}/content-management/edit/**`],
priority: 4,
handler: Effect.fn(function* (context, next) {
const csrfToken = crypto.randomBytes(32).toString('hex');
context.cookies.set(STUDIOCMS_EDITOR_CSRF_COOKIE_NAME, csrfToken, {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: (() => {
if (context.url.protocol === 'https:') return true;
const xfp = context.request.headers.get('x-forwarded-proto')?.toLowerCase() ?? '';
return xfp
.split(',')
.map((s) => s.trim())
.includes('https');
})(),
});
// Update the context locals with the CSRF token for the editor
yield* setLocals(context, SetLocal.PLUGINS, {
editorCSRFToken: csrfToken,
});
return next();
}),
},
]);
/* v8 ignore stop */