Réglages : préférences (langue, notifications push + test), gestion complète des tokens d'accès (liste/création/révocation, garde anti lock-out sur le dernier token), URL Gitea configurable, infos serveur en lecture seule (port/bind/origines/VAPID + flags CLI). Aide : documentation bilingue EN/FR de toutes les fonctionnalités, avec recherche. Icône Gitea (lien externe) dans la nav (sidebar + MoreSheet mobile). Backend : routes /api/v1/auth/tokens (GET/POST/DELETE) + tokenId dans /me ; routes/settings.ts (GET/PATCH, allow-list stricte gitea_url, aucun secret exposé, URL validée http/https anti-XSS) ; AuthService.listTokens/revokeToken (transaction). Front : NavItem gère les liens externes, nav primaire/secondaire, store settings, vues SettingsView/HelpView. 236 tests verts (+15 nouveaux : auth-tokens, settings-routes).
124 lines
5.0 KiB
TypeScript
124 lines
5.0 KiB
TypeScript
import Fastify, { type FastifyInstance, type FastifyRequest } from 'fastify';
|
|
import fastifyCookie from '@fastify/cookie';
|
|
import fastifyWebsocket from '@fastify/websocket';
|
|
import fastifyStatic from '@fastify/static';
|
|
import { existsSync } from 'node:fs';
|
|
import { join, dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import type { Config } from './config.js';
|
|
import type { Db } from './db/index.js';
|
|
import { AuthService, LoginRateLimiter, type AuthContext } from './auth/service.js';
|
|
import { PtyManager } from './core/pty-manager.js';
|
|
import { DiscoveryService } from './core/discovery-service.js';
|
|
import { WorktreeManager } from './core/worktree-manager.js';
|
|
import { GroupManager } from './core/group-manager.js';
|
|
import { PushService } from './core/push-service.js';
|
|
import { registerAuthRoutes } from './routes/auth.js';
|
|
import { registerSessionRoutes } from './routes/sessions.js';
|
|
import { registerRepoRoutes } from './routes/repos.js';
|
|
import { registerGroupRoutes } from './routes/groups.js';
|
|
import { registerWorktreeRoutes } from './routes/worktrees.js';
|
|
import { registerPushRoutes } from './routes/push.js';
|
|
import { registerSettingsRoutes } from './routes/settings.js';
|
|
import { registerFsRoutes } from './routes/fs.js';
|
|
import { registerWsGateway } from './ws/gateway.js';
|
|
|
|
declare module 'fastify' {
|
|
interface FastifyRequest {
|
|
authContext: AuthContext | null;
|
|
}
|
|
interface FastifyContextConfig {
|
|
public?: boolean;
|
|
}
|
|
}
|
|
|
|
export interface AppBundle {
|
|
app: FastifyInstance;
|
|
auth: AuthService;
|
|
manager: PtyManager;
|
|
discovery: DiscoveryService;
|
|
worktrees: WorktreeManager;
|
|
groups: GroupManager;
|
|
push: PushService;
|
|
}
|
|
|
|
export function buildApp(config: Config, db: Db, serverVersion: string): AppBundle {
|
|
const app = Fastify({ logger: { level: process.env.ARBORETUM_LOG ?? 'info' } });
|
|
const auth = new AuthService(db);
|
|
const limiter = new LoginRateLimiter();
|
|
const push = new PushService(db, config.vapidContact);
|
|
const manager = new PtyManager(db, config.claudeSessionsDir, push);
|
|
const discovery = new DiscoveryService({
|
|
ptyManager: manager,
|
|
projectsDir: config.claudeProjectsDir,
|
|
sessionsDir: config.claudeSessionsDir,
|
|
});
|
|
const worktrees = new WorktreeManager(db, manager, discovery);
|
|
const groups = new GroupManager(db);
|
|
|
|
void app.register(fastifyCookie);
|
|
void app.register(fastifyWebsocket, {
|
|
options: { maxPayload: 1024 * 1024 },
|
|
});
|
|
|
|
const allowedOrigins = new Set<string>([
|
|
`http://127.0.0.1:${config.port}`,
|
|
`http://localhost:${config.port}`,
|
|
...config.allowedOrigins,
|
|
]);
|
|
|
|
const authenticate = (req: FastifyRequest): AuthContext | null => {
|
|
const bearer = req.headers.authorization;
|
|
if (bearer?.startsWith('Bearer ')) {
|
|
const ctx = auth.verifyRawToken(bearer.slice(7));
|
|
if (ctx) return ctx;
|
|
}
|
|
return auth.verifyCookie(req.cookies[auth.cookieName]);
|
|
};
|
|
|
|
// Garde globale : auth sur tout /api/** et /ws ; check Origin strict quand l'en-tête est présent
|
|
// (anti cross-site WebSocket hijacking — le cookie SameSite=Strict ne suffit pas pour les upgrades).
|
|
app.addHook('preValidation', async (req, reply) => {
|
|
const isApi = req.url.startsWith('/api/');
|
|
const isWs = req.url.startsWith('/ws');
|
|
if (!isApi && !isWs) return; // statique : public (la SPA gère son écran de login)
|
|
const origin = req.headers.origin;
|
|
if (origin && !allowedOrigins.has(origin)) {
|
|
return reply.status(403).send({ error: { code: 'BAD_ORIGIN', message: `Origin not allowed: ${origin}` } });
|
|
}
|
|
req.authContext = authenticate(req);
|
|
if (req.routeOptions.config.public) return;
|
|
if (!req.authContext) {
|
|
return reply.status(401).send({ error: { code: 'UNAUTHORIZED', message: 'Authentication required' } });
|
|
}
|
|
});
|
|
|
|
registerAuthRoutes(app, auth, limiter, serverVersion);
|
|
registerSessionRoutes(app, manager, discovery);
|
|
registerRepoRoutes(app, worktrees);
|
|
registerGroupRoutes(app, groups);
|
|
registerWorktreeRoutes(app, worktrees);
|
|
registerPushRoutes(app, push);
|
|
registerSettingsRoutes(app, db, config, serverVersion, push);
|
|
registerFsRoutes(app);
|
|
// La route websocket doit être déclarée APRÈS le chargement du plugin (contexte
|
|
// encapsulé) — sinon le handler reçoit la signature REST (request, reply).
|
|
void app.register(async (scoped) => {
|
|
registerWsGateway(scoped, manager, discovery, worktrees, groups, serverVersion);
|
|
});
|
|
|
|
// SPA buildée embarquée dans le paquet npm (public/) — absente en dev (vite dev sert le front)
|
|
const publicDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'public');
|
|
if (existsSync(publicDir)) {
|
|
void app.register(fastifyStatic, { root: publicDir, wildcard: false });
|
|
app.setNotFoundHandler((req, reply) => {
|
|
if (req.url.startsWith('/api/') || req.url.startsWith('/ws')) {
|
|
return reply.status(404).send({ error: { code: 'NOT_FOUND', message: 'Route not found' } });
|
|
}
|
|
return reply.sendFile('index.html');
|
|
});
|
|
}
|
|
|
|
return { app, auth, manager, discovery, worktrees, groups, push };
|
|
}
|