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:
parent
a4bb3cce5f
commit
7217659720
@ -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';
|
||||
|
||||
@ -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'));
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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'));
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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';
|
||||
|
||||
62
app/utils/sessions.server.ts
Normal file
62
app/utils/sessions.server.ts
Normal 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);
|
||||
}
|
||||
@ -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',
|
||||
},
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user