fix: prevent cookie manager from racing env init

this fixes a bug where using LOAD_ENV_FILE did not work with COOKIE_SECRET
This commit is contained in:
Aarnav Tale 2025-01-06 08:11:45 +05:30
parent a4bb3cce5f
commit 7217659720
No known key found for this signature in database
15 changed files with 97 additions and 46 deletions

View File

@ -11,7 +11,7 @@ import { Form } from 'react-router';
import { cn } from '~/utils/cn';
import type { HeadplaneContext } from '~/utils/config/headplane';
import type { SessionData } from '~/utils/sessions';
import type { SessionData } from '~/utils/sessions.server';
import Menu from './Menu';
import TabLink from './TabLink';

View File

@ -9,7 +9,7 @@ import Link from '~/components/Link';
import { cn } from '~/utils/cn';
import { loadContext } from '~/utils/config/headplane';
import { HeadscaleError, pull } from '~/utils/headscale';
import { destroySession, getSession } from '~/utils/sessions';
import { destroySession, getSession } from '~/utils/sessions.server';
export async function loader({ request }: LoaderFunctionArgs) {
const session = await getSession(request.headers.get('Cookie'));

View File

@ -21,7 +21,7 @@ import { cn } from '~/utils/cn';
import { loadContext } from '~/utils/config/headplane';
import { loadConfig } from '~/utils/config/headscale';
import { HeadscaleError, pull, put } from '~/utils/headscale';
import { getSession } from '~/utils/sessions';
import { getSession } from '~/utils/sessions.server';
import { send } from '~/utils/res';
import log from '~/utils/log';

View File

@ -10,7 +10,7 @@ import type { Key } from '~/types';
import { loadContext } from '~/utils/config/headplane';
import { pull } from '~/utils/headscale';
import { startOidc } from '~/utils/oidc';
import { commitSession, getSession } from '~/utils/sessions';
import { commitSession, getSession } from '~/utils/sessions.server';
export async function loader({ request }: LoaderFunctionArgs) {
const session = await getSession(request.headers.get('Cookie'));

View File

@ -1,5 +1,5 @@
import { type ActionFunctionArgs, redirect } from 'react-router';
import { destroySession, getSession } from '~/utils/sessions';
import { destroySession, getSession } from '~/utils/sessions.server';
export async function loader() {
return redirect('/machines');

View File

@ -1,11 +1,11 @@
import type { ActionFunctionArgs } from 'react-router';
import { json, useLoaderData } from 'react-router';
import { data, useLoaderData } from 'react-router';
import Code from '~/components/Code';
import Notice from '~/components/Notice';
import { loadContext } from '~/utils/config/headplane';
import { loadConfig, patchConfig } from '~/utils/config/headscale';
import { getSession } from '~/utils/sessions';
import { getSession } from '~/utils/sessions.server';
import { useLiveData } from '~/utils/useLiveData';
import DNS from './components/dns';

View File

@ -1,6 +1,6 @@
import type { ActionFunctionArgs } from 'react-router';
import { del, post } from '~/utils/headscale';
import { getSession } from '~/utils/sessions';
import { getSession } from '~/utils/sessions.server';
import { send } from '~/utils/res';
import log from '~/utils/log';

View File

@ -20,7 +20,7 @@ import { cn } from '~/utils/cn';
import { loadContext } from '~/utils/config/headplane';
import { loadConfig } from '~/utils/config/headscale';
import { pull } from '~/utils/headscale';
import { getSession } from '~/utils/sessions';
import { getSession } from '~/utils/sessions.server';
import { useLiveData } from '~/utils/useLiveData';
import Link from '~/components/Link';

View File

@ -9,7 +9,7 @@ import { cn } from '~/utils/cn';
import { loadContext } from '~/utils/config/headplane';
import { loadConfig } from '~/utils/config/headscale';
import { pull } from '~/utils/headscale';
import { getSession } from '~/utils/sessions';
import { getSession } from '~/utils/sessions.server';
import { useLiveData } from '~/utils/useLiveData';
import type { Machine, Route, User } from '~/types';

View File

@ -1,7 +1,7 @@
import type { LoaderFunctionArgs, ActionFunctionArgs } from 'react-router';
import { useLoaderData } from 'react-router';
import { useLiveData } from '~/utils/useLiveData';
import { getSession } from '~/utils/sessions';
import { getSession } from '~/utils/sessions.server';
import { Link as RemixLink } from 'react-router';
import type { PreAuthKey, User } from '~/types';
import { pull, post } from '~/utils/headscale';

View File

@ -14,7 +14,7 @@ import { cn } from '~/utils/cn';
import { loadContext } from '~/utils/config/headplane';
import { loadConfig } from '~/utils/config/headscale';
import { del, post, pull } from '~/utils/headscale';
import { getSession } from '~/utils/sessions';
import { getSession } from '~/utils/sessions.server';
import { useLiveData } from '~/utils/useLiveData';
import { send } from '~/utils/res';

View File

@ -8,10 +8,11 @@ import { resolve } from 'node:path';
import { parse } from 'yaml';
import { type IntegrationFactory, loadIntegration } from '~/integration';
import { type HeadscaleConfig, loadConfig } from '~/utils/config/headscale';
import { IntegrationFactory, loadIntegration } from '~/integration';
import { HeadscaleConfig, loadConfig } from '~/utils/config/headscale';
import { testOidc } from '~/utils/oidc';
import log from '~/utils/log';
import { initSessionManager } from '~/utils/sessions.server';
export interface HeadplaneContext {
debug: boolean;
@ -36,12 +37,25 @@ export interface HeadplaneContext {
}
let context: HeadplaneContext | undefined;
let loadLock = false;
export async function loadContext(): Promise<HeadplaneContext> {
if (context) {
return context;
}
if (loadLock) {
return new Promise((resolve) => {
const interval = setInterval(() => {
if (context) {
clearInterval(interval);
resolve(context);
}
}, 100);
});
}
loadLock = true;
const envFile = process.env.LOAD_ENV_FILE === 'true';
if (envFile) {
log.info('CTXT', 'Loading environment variables from .env');
@ -68,7 +82,7 @@ export async function loadContext(): Promise<HeadplaneContext> {
headscaleUrl = headscaleUrl ?? config.server_url;
if (!headscalePublicUrl) {
// Fallback to the config value if the env var is not set
headscalePublicUrl = config.public_url;
headscalePublicUrl = config.server_url;
}
}
@ -81,6 +95,9 @@ export async function loadContext(): Promise<HeadplaneContext> {
throw new Error('COOKIE_SECRET not set');
}
// Initialize Session Management
initSessionManager();
context = {
debug,
headscaleUrl,
@ -107,6 +124,7 @@ export async function loadContext(): Promise<HeadplaneContext> {
);
log.info('CTXT', 'OIDC: %s', context.oidc ? 'Configured' : 'Unavailable');
loadLock = false;
return context;
}
@ -235,7 +253,7 @@ async function checkOidc(config?: HeadscaleConfig) {
return;
}
if (config.oidc.only_start_if_oidc_is_available) {
if (config?.oidc?.only_start_if_oidc_is_available) {
log.debug('CTXT', 'Validating OIDC configuration from headscale config');
const result = await testOidc(issuer, client, secret);
if (!result) {

View File

@ -16,7 +16,7 @@ import {
} from 'oauth4webapi';
import { post } from '~/utils/headscale';
import { commitSession, getSession } from '~/utils/sessions';
import { commitSession, getSession } from '~/utils/sessions.server';
import log from '~/utils/log';
import type { HeadplaneContext } from './config/headplane';

View File

@ -0,0 +1,62 @@
import { Session, SessionStorage, createCookieSessionStorage } from 'react-router';
export type SessionData = {
hsApiKey: string;
authState: string;
authNonce: string;
authVerifier: string;
user: {
name: string;
email?: string;
};
};
type SessionFlashData = {
error: string;
};
type SessionStore = SessionStorage<SessionData, SessionFlashData>;
// TODO: Add args to this function to allow custom domain/config
let sessionStorage: SessionStore | null = null;
export function initSessionManager() {
if (sessionStorage) {
throw new Error('Session manager already initialized');
}
sessionStorage = createCookieSessionStorage<SessionData, SessionFlashData>({
cookie: {
name: 'hp_sess',
httpOnly: true,
maxAge: 60 * 60 * 24, // 24 hours
path: '/',
sameSite: 'lax',
secrets: [process.env.COOKIE_SECRET!],
secure: process.env.COOKIE_SECURE !== 'false',
},
});
}
export function getSession(cookie: string | null) {
if (!sessionStorage) {
throw new Error('Session manager not initialized');
}
return sessionStorage.getSession(cookie);
}
export function destroySession(session: Session) {
if (!sessionStorage) {
throw new Error('Session manager not initialized');
}
return sessionStorage.destroySession(session);
}
export function commitSession(session: Session, opts?: { maxAge?: number }) {
if (!sessionStorage) {
throw new Error('Session manager not initialized');
}
return sessionStorage.commitSession(session, opts);
}

View File

@ -1,29 +0,0 @@
import { createCookieSessionStorage } from 'react-router'; // Or cloudflare/deno
export type SessionData = {
hsApiKey: string;
authState: string;
authNonce: string;
authVerifier: string;
user: {
name: string;
email?: string;
};
};
type SessionFlashData = {
error: string;
};
export const { getSession, commitSession, destroySession } =
createCookieSessionStorage<SessionData, SessionFlashData>({
cookie: {
name: 'hp_sess',
httpOnly: true,
maxAge: 60 * 60 * 24, // 24 hours
path: '/',
sameSite: 'lax',
secrets: [process.env.COOKIE_SECRET!],
secure: process.env.COOKIE_SECURE !== 'false',
},
});