Some checks failed
CI / Build & test (Node 24) (push) Has been cancelled
CI / Pack & boot smoke (Node 22) (push) Has been cancelled
CI / No em/en dashes (push) Has been cancelled
CI / Build & test (Node 22) (push) Has been cancelled
Deploy site (production) / build-and-deploy (push) Successful in 20s
Remplace les 547 tirets cadratins (U+2014) et demi-cadratins (U+2013) des fichiers versionnés par la ponctuation contextuelle adaptée (point médian, deux-points, virgule, parenthèses ; tiret simple pour les plages), sur 122 fichiers (appli, vitrine, doc, tests, workflows, scripts). Ajoute le job CI « lint-dashes » (git grep -P) qui échoue si un tiret cadratin/demi-cadratin réapparaît, hors logo binaire et captures brutes du terminal (fidélité des fixtures de détection de dialogue).
127 lines
5.0 KiB
TypeScript
127 lines
5.0 KiB
TypeScript
// Réglages exposés à l'UI (onglet Réglages). Frontière de sécurité CENTRALE : la table `settings`
|
|
// contient aussi des SECRETS (server_secret, vapid_private). Ces routes n'exposent QUE des champs
|
|
// non sensibles et n'écrivent QUE des clés explicitement allow-listées : jamais les secrets.
|
|
import type { FastifyInstance } from 'fastify';
|
|
import type { ServerInfo, SettingsResponse, UpdateSettingsRequest } from '@arboretum/shared';
|
|
import type { Config } from '../config.js';
|
|
import { type Db, setSetting } from '../db/index.js';
|
|
import type { PushService } from '../core/push-service.js';
|
|
import type { SettingsBus } from '../core/settings-bus.js';
|
|
import { recordAudit } from '../core/audit-log.js';
|
|
import { diagnoseClaudeBin } from '../core/claude-launcher.js';
|
|
import {
|
|
SCAN_INTERVAL_KEY,
|
|
SCAN_ROOTS_KEY,
|
|
normalizeScanIntervalMin,
|
|
normalizeScanRoots,
|
|
readScanIntervalMin,
|
|
readScanRoots,
|
|
} from '../core/scan-settings.js';
|
|
import {
|
|
CLAUDE_BIN_PATH_KEY,
|
|
CLAUDE_HOME_KEY,
|
|
normalizeClaudeBinPath,
|
|
normalizeClaudeHome,
|
|
readClaudeBinPath,
|
|
readClaudeHome,
|
|
} from '../core/claude-settings.js';
|
|
import {
|
|
PURGE_DAYS_KEY,
|
|
RETENTION_DAYS_KEY,
|
|
normalizePurgeDays,
|
|
normalizeRetentionDays,
|
|
readPurgeDays,
|
|
readRetentionDays,
|
|
} from '../core/retention-settings.js';
|
|
|
|
// Réglages modifiables via l'API (allow-list stricte). Les secrets ne figurent JAMAIS ici.
|
|
export function registerSettingsRoutes(
|
|
app: FastifyInstance,
|
|
db: Db,
|
|
config: Config,
|
|
serverVersion: string,
|
|
push: PushService,
|
|
settingsBus: SettingsBus,
|
|
): void {
|
|
const serverInfo = (): ServerInfo => ({
|
|
version: serverVersion,
|
|
port: config.port,
|
|
bind: config.bind,
|
|
allowedOrigins: config.allowedOrigins,
|
|
dataDir: config.dataDir,
|
|
vapidPublicKey: push.publicKey() || null,
|
|
vapidContact: config.vapidContact,
|
|
claudeHome: config.claudeHome,
|
|
// Diagnostic recalculé à chaque GET : reflète l'état réel (override exécutable ? `which claude` ?).
|
|
claudeBin: diagnoseClaudeBin(readClaudeBinPath(db)),
|
|
});
|
|
const snapshot = (): SettingsResponse => ({
|
|
settings: {
|
|
scanRoots: readScanRoots(db),
|
|
scanIntervalMin: readScanIntervalMin(db),
|
|
claudeBinPath: readClaudeBinPath(db),
|
|
claudeHome: readClaudeHome(db),
|
|
retentionDays: readRetentionDays(db),
|
|
purgeDays: readPurgeDays(db),
|
|
},
|
|
server: serverInfo(),
|
|
});
|
|
|
|
app.get('/api/v1/settings', async (): Promise<SettingsResponse> => snapshot());
|
|
|
|
app.patch('/api/v1/settings', async (req, reply) => {
|
|
const body = (req.body as Partial<UpdateSettingsRequest> | null) ?? {};
|
|
if ('scanRoots' in body) {
|
|
const roots = normalizeScanRoots(body.scanRoots);
|
|
if (!roots) {
|
|
return reply.status(400).send({ error: { code: 'BAD_REQUEST', message: 'scanRoots must be an array of ≤16 absolute, normalized paths (never "/")' } });
|
|
}
|
|
setSetting(db, SCAN_ROOTS_KEY, JSON.stringify(roots));
|
|
}
|
|
if ('scanIntervalMin' in body) {
|
|
const interval = normalizeScanIntervalMin(body.scanIntervalMin);
|
|
if (interval === null) {
|
|
return reply.status(400).send({ error: { code: 'BAD_REQUEST', message: 'scanIntervalMin must be an integer between 0 and 1440' } });
|
|
}
|
|
setSetting(db, SCAN_INTERVAL_KEY, String(interval));
|
|
}
|
|
if ('claudeBinPath' in body) {
|
|
const value = normalizeClaudeBinPath(body.claudeBinPath);
|
|
if (value === null) {
|
|
return reply.status(400).send({ error: { code: 'BAD_REQUEST', message: 'claudeBinPath must be an absolute path to an executable file (or "" to reset)' } });
|
|
}
|
|
setSetting(db, CLAUDE_BIN_PATH_KEY, value);
|
|
}
|
|
if ('claudeHome' in body) {
|
|
const value = normalizeClaudeHome(body.claudeHome);
|
|
if (value === null) {
|
|
return reply.status(400).send({ error: { code: 'BAD_REQUEST', message: 'claudeHome must be an absolute path to an existing directory (or "" to reset)' } });
|
|
}
|
|
setSetting(db, CLAUDE_HOME_KEY, value);
|
|
}
|
|
if ('retentionDays' in body) {
|
|
const value = normalizeRetentionDays(body.retentionDays);
|
|
if (value === null) {
|
|
return reply.status(400).send({ error: { code: 'BAD_REQUEST', message: 'retentionDays must be 0 (never) or an integer between 1 and 3650' } });
|
|
}
|
|
setSetting(db, RETENTION_DAYS_KEY, String(value));
|
|
}
|
|
if ('purgeDays' in body) {
|
|
const value = normalizePurgeDays(body.purgeDays);
|
|
if (value === null) {
|
|
return reply.status(400).send({ error: { code: 'BAD_REQUEST', message: 'purgeDays must be 0 (disabled) or an integer between 1 and 3650' } });
|
|
}
|
|
setSetting(db, PURGE_DAYS_KEY, String(value));
|
|
}
|
|
recordAudit(db, {
|
|
actor: req.authContext?.tokenId ?? 'unknown',
|
|
action: 'settings.update',
|
|
details: { keys: Object.keys(body) },
|
|
});
|
|
const snap = snapshot();
|
|
// P11 · diffuse le nouvel état non sensible à tous les clients abonnés au topic 'settings'.
|
|
settingsBus.emit('settings_update', snap.settings);
|
|
return reply.send(snap);
|
|
});
|
|
}
|