Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions graphql/server/src/middleware/__tests__/cookie.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,23 @@ describe('cookie utilities', () => {
expect(config.maxAge).toBe(2592000);
});

it('uses PostgreSQL interval cookieMaxAge values', () => {
const authSettings: AuthSettings = {
cookieMaxAge: { days: 14 },
};
const config = getSessionCookieConfig(authSettings);
expect(config.maxAge).toBe(14 * 24 * 60 * 60);
});

it('uses PostgreSQL interval rememberMeDuration when rememberMe is true', () => {
const authSettings: AuthSettings = {
cookieMaxAge: { hours: 1 },
rememberMeDuration: { days: 30 },
};
const config = getSessionCookieConfig(authSettings, true);
expect(config.maxAge).toBe(30 * 24 * 60 * 60);
});

it('uses cookieMaxAge when rememberMe is false', () => {
const authSettings: AuthSettings = {
cookieMaxAge: '3600',
Expand All @@ -73,6 +90,15 @@ describe('cookie utilities', () => {
const config = getSessionCookieConfig(authSettings, true);
expect(config.maxAge).toBe(7200);
});

it('falls back to cookieMaxAge when rememberMeDuration is invalid', () => {
const authSettings: AuthSettings = {
cookieMaxAge: { hours: 2 },
rememberMeDuration: 'not-an-interval',
};
const config = getSessionCookieConfig(authSettings, true);
expect(config.maxAge).toBe(2 * 60 * 60);
});
});

describe('getDeviceTokenCookieConfig', () => {
Expand Down
16 changes: 10 additions & 6 deletions graphql/server/src/middleware/cookie.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Request, Response } from 'express';
import type { AuthSettings } from '../types';
import { pgIntervalToSeconds } from '../utils/pg-interval';

export const SESSION_COOKIE_NAME = 'constructive_session';
export const DEVICE_TOKEN_COOKIE_NAME = 'constructive_device_token';
Expand All @@ -24,12 +25,15 @@ export const getSessionCookieConfig = (
): CookieConfig => {
const DEFAULT_MAX_AGE = 86400; // 24 hours
let maxAge = DEFAULT_MAX_AGE;
if (rememberMe && authSettings?.rememberMeDuration) {
const parsed = parseInt(authSettings.rememberMeDuration, 10);
if (!isNaN(parsed)) maxAge = parsed;
} else if (authSettings?.cookieMaxAge) {
const parsed = parseInt(authSettings.cookieMaxAge, 10);
if (!isNaN(parsed)) maxAge = parsed;

const configuredMaxAge =
rememberMe
? pgIntervalToSeconds(authSettings?.rememberMeDuration) ??
pgIntervalToSeconds(authSettings?.cookieMaxAge)
: pgIntervalToSeconds(authSettings?.cookieMaxAge);

if (configuredMaxAge !== null) {
maxAge = configuredMaxAge;
}

return {
Expand Down
1 change: 1 addition & 0 deletions graphql/server/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type {
CorsModuleData,
DatabaseSettings,
GenericModuleData,
PgInterval,
PubkeyChallengeSettings,
PublicKeyChallengeData,
RlsModule,
Expand Down
35 changes: 35 additions & 0 deletions graphql/server/src/utils/__tests__/pg-interval.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {
pgIntervalToMilliseconds,
pgIntervalToSeconds,
} from '../pg-interval';

describe('pg interval utilities', () => {
describe('pgIntervalToSeconds', () => {
it('treats numeric strings as seconds', () => {
expect(pgIntervalToSeconds('3600')).toBe(3600);
});

it('converts PostgreSQL interval objects to seconds', () => {
expect(pgIntervalToSeconds({ days: 1, hours: 2, minutes: 3, seconds: 4 })).toBe(
93784,
);
});

it('returns null for missing or invalid values', () => {
expect(pgIntervalToSeconds(null)).toBeNull();
expect(pgIntervalToSeconds(undefined)).toBeNull();
expect(pgIntervalToSeconds('not-an-interval')).toBeNull();
});

it('preserves zero-second values', () => {
expect(pgIntervalToSeconds('0')).toBe(0);
expect(pgIntervalToSeconds({ seconds: 0 })).toBe(0);
});
});

describe('pgIntervalToMilliseconds', () => {
it('converts parsed seconds to milliseconds', () => {
expect(pgIntervalToMilliseconds({ minutes: 10 })).toBe(10 * 60 * 1000);
});
});
});
36 changes: 36 additions & 0 deletions graphql/server/src/utils/pg-interval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { PgInterval } from '@constructive-io/express-context';

/**
* Convert a PostgreSQL interval value from auth settings into seconds.
*
* Numeric string values are treated as seconds, matching the existing auth
* settings cookie parser behavior.
*/
export function pgIntervalToSeconds(
interval: string | PgInterval | null | undefined,
): number | null {
if (!interval) return null;

if (typeof interval === 'string') {
const seconds = parseInt(interval, 10);
return Number.isNaN(seconds) ? null : seconds;
}

let totalSeconds = 0;
if (interval.years) totalSeconds += interval.years * 365 * 24 * 60 * 60;
if (interval.months) totalSeconds += interval.months * 30 * 24 * 60 * 60;
if (interval.days) totalSeconds += interval.days * 24 * 60 * 60;
if (interval.hours) totalSeconds += interval.hours * 60 * 60;
if (interval.minutes) totalSeconds += interval.minutes * 60;
if (interval.seconds) totalSeconds += interval.seconds;
if (interval.milliseconds) totalSeconds += interval.milliseconds / 1000;

return totalSeconds;
}

export function pgIntervalToMilliseconds(
interval: string | PgInterval | null | undefined,
): number | null {
const seconds = pgIntervalToSeconds(interval);
return seconds === null ? null : seconds * 1000;
}
1 change: 1 addition & 0 deletions packages/express-context/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export type {
InferenceLogConfig,
LlmConfig,
PublicKeyChallengeData,
PgInterval,
PubkeyChallengeSettings,
RlsModule,
WebauthnSettings,
Expand Down
6 changes: 3 additions & 3 deletions packages/express-context/src/loaders/auth-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* database rather than the services database.
*/

import type { AuthSettings } from '../types';
import type { AuthSettings, PgInterval } from '../types';
import type { LoaderContext, ModuleLoader } from './types';
import { createModuleLoader } from './create-loader';

Expand Down Expand Up @@ -45,9 +45,9 @@ interface AuthSettingsRow {
cookie_samesite: string;
cookie_domain: string | null;
cookie_httponly: boolean;
cookie_max_age: string | null;
cookie_max_age: string | PgInterval | null;
cookie_path: string;
remember_me_duration: string | null;
remember_me_duration: string | PgInterval | null;
enable_captcha: boolean;
captcha_site_key: string | null;
}
Expand Down
14 changes: 12 additions & 2 deletions packages/express-context/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,24 @@ export interface RlsModule {
currentUserAgent: string;
}

export interface PgInterval {
years?: number;
months?: number;
days?: number;
hours?: number;
minutes?: number;
seconds?: number;
milliseconds?: number;
}

export interface AuthSettings {
cookieSecure?: boolean;
cookieSamesite?: string;
cookieDomain?: string | null;
cookieHttponly?: boolean;
cookieMaxAge?: string | null;
cookieMaxAge?: string | PgInterval | null;
cookiePath?: string;
rememberMeDuration?: string | null;
rememberMeDuration?: string | PgInterval | null;
enableCaptcha?: boolean;
captchaSiteKey?: string | null;
}
Expand Down