Compare commits

...

3 commits

Author SHA1 Message Date
ad491d69e8
meta: move config files to typescript
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I60de4abb5b81c4b14d95d7b6f1da8aa66a6a6964
2026-02-01 17:17:34 +03:00
7d8bc6943d
config: bind to localhost by default
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I00aca92a09291ce12f09da68917f56c06a6a6964
2026-02-01 17:17:33 +03:00
2db5fa502f
dashboard: add authentication middleware
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I9910548c65a11f2c83dfeb4fef3c93f06a6a6964
2026-02-01 17:17:32 +03:00
9 changed files with 158 additions and 56 deletions

View file

@ -3,8 +3,24 @@ import type { Config } from './src/types';
const config: Config = { const config: Config = {
server: { server: {
port: 3000, port: 3000,
// host: '0.0.0.0', // Uncomment to bind to all interfaces (default is localhost only)
}, },
// Dashboard configuration (optional)
// dashboard: {
// enabled: true,
// // Authentication options (choose one):
// auth: {
// // Option 1: Basic HTTP authentication
// type: 'basic',
// username: 'admin',
// password: 'changeme',
// // Option 2: Bearer token authentication
// // type: 'token',
// // token: 'your-secret-token-here',
// },
// },
repositories: [ repositories: [
// Leave empty to accept webhooks from any repo. // Leave empty to accept webhooks from any repo.
// { owner: "myorg", repo: "myrepo" }, // { owner: "myorg", repo: "myrepo" },

View file

@ -2,9 +2,9 @@ import tseslint from '@typescript-eslint/eslint-plugin';
import tsparser from '@typescript-eslint/parser'; import tsparser from '@typescript-eslint/parser';
export default [ export default [
{ ignores: ['dist/**', 'node_modules/**', '**/*.d.ts'] },
{ {
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], files: ['src/**/*.ts', '**/*.js', 'config.example.ts', 'tsup.config.ts'],
ignores: ['dist/**', 'node_modules/**'],
languageOptions: { languageOptions: {
ecmaVersion: 2022, ecmaVersion: 2022,
sourceType: 'module', sourceType: 'module',
@ -19,8 +19,9 @@ export default [
}, },
}, },
plugins: { plugins: {
'@typescript-eslint': tseslint, '@typescript-eslint': tseslint as unknown as Record<string, unknown>,
}, },
rules: { rules: {
...tseslint.configs.recommended.rules, ...tseslint.configs.recommended.rules,
'@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/no-explicit-any': 'warn',

View file

@ -1,4 +1,6 @@
export default { import type { Config } from 'prettier';
const config: Config = {
printWidth: 100, printWidth: 100,
tabWidth: 2, tabWidth: 2,
useTabs: false, useTabs: false,
@ -13,3 +15,5 @@ export default {
endOfLine: 'lf', endOfLine: 'lf',
plugins: [], plugins: [],
}; };
export default config;

View file

@ -97,7 +97,10 @@ export function loadConfig(): Config {
); );
} }
const config = deepMerge(defaults, fileConfig); const config = deepMerge(
defaults as unknown as Record<string, unknown>,
fileConfig as unknown as Record<string, unknown>
) as unknown as Config;
// Environment variable overrides // Environment variable overrides
if (process.env.PORT) { if (process.env.PORT) {
@ -108,6 +111,31 @@ export function loadConfig(): Config {
config.server.port = parsed; config.server.port = parsed;
} }
if (process.env.HOST) {
config.server.host = process.env.HOST;
}
if (process.env.DASHBOARD_TOKEN) {
config.dashboard = {
...(config.dashboard || { enabled: true }),
auth: {
type: 'token',
token: process.env.DASHBOARD_TOKEN,
},
};
}
if (process.env.DASHBOARD_USERNAME && process.env.DASHBOARD_PASSWORD) {
config.dashboard = {
...(config.dashboard || { enabled: true }),
auth: {
type: 'basic',
username: process.env.DASHBOARD_USERNAME,
password: process.env.DASHBOARD_PASSWORD,
},
};
}
const validLogLevels = ['debug', 'info', 'warn', 'error']; const validLogLevels = ['debug', 'info', 'warn', 'error'];
if (process.env.LOG_LEVEL) { if (process.env.LOG_LEVEL) {
if (!validLogLevels.includes(process.env.LOG_LEVEL)) { if (!validLogLevels.includes(process.env.LOG_LEVEL)) {

View file

@ -9,6 +9,43 @@ export function createDashboardRouter(config: Config): express.Router {
router.use(express.json()); router.use(express.json());
// Authentication middleware
if (config.dashboard?.auth) {
router.use((req, res, next) => {
const auth = config.dashboard!.auth!;
if (auth.type === 'basic') {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Basic ')) {
res.setHeader('WWW-Authenticate', 'Basic realm="Troutbot Dashboard"');
res.status(401).json({ error: 'Authentication required' });
return;
}
const credentials = Buffer.from(authHeader.slice(6), 'base64').toString('utf8');
const [username, password] = credentials.split(':');
if (username !== auth.username || password !== auth.password) {
res.setHeader('WWW-Authenticate', 'Basic realm="Troutbot Dashboard"');
res.status(401).json({ error: 'Invalid credentials' });
return;
}
} else if (auth.type === 'token') {
const authHeader = req.headers.authorization;
const token = req.query.token as string | undefined;
const providedToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : token;
if (!providedToken || providedToken !== auth.token) {
res.status(401).json({ error: 'Invalid or missing token' });
return;
}
}
next();
});
}
// API routes // API routes
router.get('/api/status', (_req, res) => { router.get('/api/status', (_req, res) => {
const enabledBackends = Object.entries(config.engine.backends) const enabledBackends = Object.entries(config.engine.backends)
@ -41,9 +78,9 @@ export function createDashboardRouter(config: Config): express.Router {
try { try {
const partial = req.body as Partial<Config>; const partial = req.body as Partial<Config>;
const merged = deepMerge( const merged = deepMerge(
config as Record<string, unknown>, config as unknown as Record<string, unknown>,
partial as Record<string, unknown> partial as unknown as Record<string, unknown>
) as Config; ) as unknown as Config;
validate(merged); validate(merged);
// Apply in-place // Apply in-place
@ -55,8 +92,7 @@ export function createDashboardRouter(config: Config): express.Router {
} }
}); });
// --- Dashboard HTML --- // Dashboard HTML
router.get('/dashboard', (_req, res) => { router.get('/dashboard', (_req, res) => {
res.type('html').send(dashboardHTML()); res.type('html').send(dashboardHTML());
}); });

View file

@ -27,12 +27,13 @@ async function analyzeOne(target: string) {
initLogger(config.logging); initLogger(config.logging);
const logger = getLogger(); const logger = getLogger();
initGitHub(process.env.GITHUB_TOKEN);
if (!process.env.GITHUB_TOKEN) { if (!process.env.GITHUB_TOKEN) {
logger.error('GITHUB_TOKEN is required for analyze mode'); logger.error('GITHUB_TOKEN is required for analyze mode');
process.exit(1); process.exit(1);
} }
initGitHub(process.env.GITHUB_TOKEN);
const prData = await fetchPR(owner, repo, prNumber); const prData = await fetchPR(owner, repo, prNumber);
if (!prData) { if (!prData) {
logger.error(`Could not fetch PR ${owner}/${repo}#${prNumber}`); logger.error(`Could not fetch PR ${owner}/${repo}#${prNumber}`);
@ -108,8 +109,9 @@ function serve() {
.filter(([, v]) => v.enabled) .filter(([, v]) => v.enabled)
.map(([k]) => k); .map(([k]) => k);
const server = app.listen(port, async () => { const host = config.server.host || '127.0.0.1';
logger.info(`Troutbot listening on port ${port}`); const server = app.listen(port, host, async () => {
logger.info(`Troutbot listening on ${host}:${port}`);
logger.info(`Enabled backends: ${enabledBackends.join(', ')}`); logger.info(`Enabled backends: ${enabledBackends.join(', ')}`);
// Watched repos // Watched repos
@ -140,7 +142,8 @@ function serve() {
// Comment update mode // Comment update mode
logger.info(`Comment updates: ${config.response.allowUpdates ? 'enabled' : 'disabled'}`); logger.info(`Comment updates: ${config.response.allowUpdates ? 'enabled' : 'disabled'}`);
logger.info(`Dashboard available at http://localhost:${port}/dashboard`); const displayHost = host === '0.0.0.0' ? 'localhost' : host;
logger.info(`Dashboard available at http://${displayHost}:${port}/dashboard`);
// Start polling if enabled // Start polling if enabled
await startPolling(config); await startPolling(config);

View file

@ -1,5 +1,6 @@
export interface Config { export interface Config {
server: ServerConfig; server: ServerConfig;
dashboard?: DashboardConfig;
repositories: RepoConfig[]; repositories: RepoConfig[];
filters: FiltersConfig; filters: FiltersConfig;
engine: EngineConfig; engine: EngineConfig;
@ -16,9 +17,22 @@ export interface PollingConfig {
export interface ServerConfig { export interface ServerConfig {
port: number; port: number;
host?: string;
rateLimit?: number; rateLimit?: number;
} }
export interface DashboardConfig {
enabled: boolean;
auth?: DashboardAuthConfig;
}
export interface DashboardAuthConfig {
type: 'basic' | 'token';
username?: string;
password?: string;
token?: string;
}
export interface RepoConfig { export interface RepoConfig {
owner: string; owner: string;
repo: string; repo: string;