Skip to content
Merged
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
2 changes: 1 addition & 1 deletion docs/depsera-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "depsera",
"version": "1.10.0",
"version": "1.10.1",
"description": "Dependency monitoring and service health dashboard",
"scripts": {
"dev": "concurrently --kill-others \"npm run dev:server\" \"npm run dev:client\"",
Expand Down
3 changes: 3 additions & 0 deletions server/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Silence pino logger output during tests
process.env.LOG_LEVEL = 'silent';

/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
Expand Down
30 changes: 17 additions & 13 deletions server/src/auth/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ jest.mock('openid-client', () => ({
randomState: mockRandomState,
}));

const mockLoggerInfo = jest.fn();
jest.mock('../utils/logger', () => ({
__esModule: true,
default: {
info: mockLoggerInfo,
warn: jest.fn(),
error: jest.fn(),
},
}));

// Store original env values
const originalEnv = { ...process.env };

Expand Down Expand Up @@ -74,7 +84,7 @@ describe('Auth Config', () => {
const mockConfig = { serverMetadata: mockServerMetadata };
mockDiscovery.mockResolvedValue(mockConfig);

const logSpy = jest.spyOn(console, 'log').mockImplementation();
mockLoggerInfo.mockClear();

const { initializeOIDC } = await import('./config');

Expand All @@ -85,14 +95,14 @@ describe('Auth Config', () => {
'test-client',
'test-secret'
);
expect(logSpy).toHaveBeenCalledWith(
expect.stringContaining('Discovering OIDC issuer')
expect(mockLoggerInfo).toHaveBeenCalledWith(
{ issuerUrl: 'https://issuer.example.com' },
'discovering OIDC issuer'
);
expect(logSpy).toHaveBeenCalledWith(
expect.stringContaining('OIDC issuer discovered')
expect(mockLoggerInfo).toHaveBeenCalledWith(
{ issuer: 'https://issuer.example.com' },
'OIDC issuer discovered'
);

logSpy.mockRestore();
});

it('should only initialize once (idempotent)', async () => {
Expand All @@ -104,17 +114,13 @@ describe('Auth Config', () => {
const mockConfig = { serverMetadata: mockServerMetadata };
mockDiscovery.mockResolvedValue(mockConfig);

const logSpy = jest.spyOn(console, 'log').mockImplementation();

const { initializeOIDC } = await import('./config');

await initializeOIDC();
await initializeOIDC();

// Discovery should only be called once
expect(mockDiscovery).toHaveBeenCalledTimes(1);

logSpy.mockRestore();
});
});

Expand All @@ -136,8 +142,6 @@ describe('Auth Config', () => {
const mockConfig = { serverMetadata: mockServerMetadata };
mockDiscovery.mockResolvedValue(mockConfig);

jest.spyOn(console, 'log').mockImplementation();

const { initializeOIDC, getOIDCConfig } = await import('./config');

await initializeOIDC();
Expand Down
5 changes: 3 additions & 2 deletions server/src/auth/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as client from 'openid-client';
import logger from '../utils/logger';

let oidcConfig: client.Configuration | null = null;

Expand All @@ -20,15 +21,15 @@ export async function initializeOIDC(): Promise<void> {
throw new Error('OIDC_CLIENT_SECRET is required');
}

console.log(`Discovering OIDC issuer: ${issuerUrl}`);
logger.info({ issuerUrl }, 'discovering OIDC issuer');

oidcConfig = await client.discovery(
new URL(issuerUrl),
clientId,
clientSecret
);

console.log(`OIDC issuer discovered: ${oidcConfig.serverMetadata().issuer}`);
logger.info({ issuer: oidcConfig.serverMetadata().issuer }, 'OIDC issuer discovered');
}

export function getOIDCConfig(): client.Configuration {
Expand Down
6 changes: 2 additions & 4 deletions server/src/auth/session.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import session from 'express-session';
import SqliteStore from 'better-sqlite3-session-store';
import { db } from '../db';
import logger from '../utils/logger';
import { validateSessionSecret } from './validateSessionSecret';

const BetterSqlite3Store = SqliteStore(session);
Expand Down Expand Up @@ -59,9 +60,6 @@ export function warnInsecureCookies(): void {
// outside dev, the 'auto' secure flag will resolve to false (HTTP), sending
// session cookies over unencrypted connections.
if (!hasHttps && !hasTrustProxy && !hasNativeHttps) {
console.warn(
'[Security] Session cookie "secure" flag will be false — cookies will be sent over HTTP. ' +
'Set REQUIRE_HTTPS=true and/or TRUST_PROXY for production deployments.'
);
logger.warn('session cookie "secure" flag will be false — cookies will be sent over HTTP. Set REQUIRE_HTTPS=true and/or TRUST_PROXY for production deployments.');
}
}
21 changes: 15 additions & 6 deletions server/src/auth/validateSessionSecret.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import { validateSessionSecret } from './validateSessionSecret';
import logger from '../utils/logger';

jest.mock('../utils/logger', () => ({
__esModule: true,
default: {
warn: jest.fn(),
info: jest.fn(),
error: jest.fn(),
},
}));

describe('validateSessionSecret', () => {
const originalEnv = process.env;

beforeEach(() => {
process.env = { ...originalEnv };
jest.spyOn(console, 'warn').mockImplementation(() => {});
(logger.warn as jest.Mock).mockClear();
});

afterEach(() => {
process.env = originalEnv;
jest.restoreAllMocks();
});

describe('production mode', () => {
Expand Down Expand Up @@ -63,23 +72,23 @@ describe('validateSessionSecret', () => {
it('should return fallback when SESSION_SECRET is missing', () => {
delete process.env.SESSION_SECRET;
expect(validateSessionSecret()).toBe('dev-secret-change-in-production');
expect(console.warn).toHaveBeenCalledWith(
expect.stringContaining('Using default session secret')
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining('default session secret')
);
});

it('should warn when SESSION_SECRET is a weak default', () => {
process.env.SESSION_SECRET = 'dev-session-secret-change-in-production';
expect(validateSessionSecret()).toBe('dev-session-secret-change-in-production');
expect(console.warn).toHaveBeenCalledWith(
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining('known weak default')
);
});

it('should return custom secret without warning', () => {
process.env.SESSION_SECRET = 'my-custom-dev-secret';
expect(validateSessionSecret()).toBe('my-custom-dev-secret');
expect(console.warn).not.toHaveBeenCalled();
expect(logger.warn).not.toHaveBeenCalled();
});

it('should allow short secrets in development', () => {
Expand Down
10 changes: 4 additions & 6 deletions server/src/auth/validateSessionSecret.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import logger from '../utils/logger';

const WEAK_DEFAULTS = [
'dev-secret-change-in-production',
'dev-session-secret-change-in-production',
Expand Down Expand Up @@ -36,16 +38,12 @@ export function validateSessionSecret(): string {

// Development mode
if (!secret) {
console.warn(
'[Security] Using default session secret. Set SESSION_SECRET for production.'
);
logger.warn('using default session secret — set SESSION_SECRET for production');
return 'dev-secret-change-in-production';
}

if (WEAK_DEFAULTS.includes(secret)) {
console.warn(
'[Security] SESSION_SECRET is a known weak default. Change for production.'
);
logger.warn('SESSION_SECRET is a known weak default — change for production');
}

return secret;
Expand Down
31 changes: 20 additions & 11 deletions server/src/auth/warnInsecureCookies.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import { warnInsecureCookies } from './session';
import logger from '../utils/logger';

jest.mock('../utils/logger', () => ({
__esModule: true,
default: {
warn: jest.fn(),
info: jest.fn(),
error: jest.fn(),
},
}));

describe('warnInsecureCookies', () => {
const originalEnv = process.env;

beforeEach(() => {
process.env = { ...originalEnv };
jest.spyOn(console, 'warn').mockImplementation(() => {});
(logger.warn as jest.Mock).mockClear();
});

afterEach(() => {
process.env = originalEnv;
jest.restoreAllMocks();
});

it('should not warn in development mode (NODE_ENV=development)', () => {
Expand All @@ -20,7 +29,7 @@ describe('warnInsecureCookies', () => {

warnInsecureCookies();

expect(console.warn).not.toHaveBeenCalled();
expect(logger.warn).not.toHaveBeenCalled();
});

it('should not warn when NODE_ENV is unset (defaults to dev)', () => {
Expand All @@ -30,7 +39,7 @@ describe('warnInsecureCookies', () => {

warnInsecureCookies();

expect(console.warn).not.toHaveBeenCalled();
expect(logger.warn).not.toHaveBeenCalled();
});

it('should warn in production without HTTPS or trust proxy', () => {
Expand All @@ -40,8 +49,8 @@ describe('warnInsecureCookies', () => {

warnInsecureCookies();

expect(console.warn).toHaveBeenCalledWith(
expect.stringContaining('Session cookie "secure" flag will be false')
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining('secure')
);
});

Expand All @@ -52,7 +61,7 @@ describe('warnInsecureCookies', () => {

warnInsecureCookies();

expect(console.warn).toHaveBeenCalledWith(
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining('cookies will be sent over HTTP')
);
});
Expand All @@ -64,7 +73,7 @@ describe('warnInsecureCookies', () => {

warnInsecureCookies();

expect(console.warn).not.toHaveBeenCalled();
expect(logger.warn).not.toHaveBeenCalled();
});

it('should not warn in production with TRUST_PROXY set', () => {
Expand All @@ -74,7 +83,7 @@ describe('warnInsecureCookies', () => {

warnInsecureCookies();

expect(console.warn).not.toHaveBeenCalled();
expect(logger.warn).not.toHaveBeenCalled();
});

it('should not warn in production with both REQUIRE_HTTPS and TRUST_PROXY', () => {
Expand All @@ -84,7 +93,7 @@ describe('warnInsecureCookies', () => {

warnInsecureCookies();

expect(console.warn).not.toHaveBeenCalled();
expect(logger.warn).not.toHaveBeenCalled();
});

it('should not warn in production with ENABLE_HTTPS=true', () => {
Expand All @@ -95,6 +104,6 @@ describe('warnInsecureCookies', () => {

warnInsecureCookies();

expect(console.warn).not.toHaveBeenCalled();
expect(logger.warn).not.toHaveBeenCalled();
});
});
5 changes: 3 additions & 2 deletions server/src/db/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Database, { Database as DatabaseType } from 'better-sqlite3';
import path from 'path';
import logger from '../utils/logger';
import { runMigrations } from './migrate';

const dbPath = process.env.DATABASE_PATH || path.join(__dirname, '../../data/database.sqlite');
Expand All @@ -22,13 +23,13 @@ export function initializeDatabase(): void {
// Run migrations
runMigrations(db);

console.log('Database initialized');
logger.info('database initialized');
}

export function closeDatabase(): void {
if (db.open) {
db.close();
console.log('Database connection closed');
logger.info('database connection closed');
}
}

Expand Down
Loading
Loading