feat: création de projet (nouveau dossier + session) & en-tête de session enrichi
- POST /api/v1/projects : crée <root>/<name> (mkdir non récursif, 409 si existant), git init optionnel, puis lance une session dedans. Anti-traversal sur le nom (core/project.ts). - Web : NewProjectModal (racine préremplie scanRoots[0], case git init) + bouton dans SessionsListView. - SessionContextBar : badge du groupe (cliquable) + repos couverts avec leur branche dans l'en-tête de session ; session simple = repo+branche déduits du cwd. - 14 tests (unitaires + route).
This commit is contained in:
33
packages/server/src/core/project.ts
Normal file
33
packages/server/src/core/project.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
// Création d'un nouveau projet : un dossier `<root>/<name>` créé sous une racine existante, dans
|
||||
// lequel on lance ensuite une session. Couche PURE (testable sans fs) — la création réelle du
|
||||
// dossier, le `git init` et le spawn vivent dans `routes/projects.ts`.
|
||||
import { join } from 'node:path';
|
||||
import { isSafeAbsolutePath } from './git.js';
|
||||
|
||||
/**
|
||||
* Valide un nom de projet : UN SEUL segment de dossier. Anti-traversal de base — refuse les noms
|
||||
* vides, trop longs, `.`/`..`, et tout caractère de séparation (`/`, `\`) ou NUL. La résolution du
|
||||
* chemin complet (et sa re-validation) est faite par {@link resolveProjectDir}.
|
||||
*/
|
||||
export function isSafeProjectName(name: string): boolean {
|
||||
const n = name.trim();
|
||||
if (n.length === 0 || n.length > 255) return false;
|
||||
if (n === '.' || n === '..') return false;
|
||||
return !/[/\\\0]/.test(n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Résout le dossier de projet `<root>/<name>`. Suppose `root` déjà validé absolu et existant par
|
||||
* l'appelant ; re-valide le chemin final via {@link isSafeAbsolutePath} (défense en profondeur).
|
||||
* Lève si `name` est invalide ou si le chemin résultant échappe l'arborescence.
|
||||
*/
|
||||
export function resolveProjectDir(root: string, name: string): string {
|
||||
if (!isSafeProjectName(name)) {
|
||||
throw Object.assign(new Error(`Invalid project name: ${name}`), { statusCode: 400 });
|
||||
}
|
||||
const dir = join(root, name.trim());
|
||||
if (!isSafeAbsolutePath(dir)) {
|
||||
throw Object.assign(new Error(`Unsafe project path: ${dir}`), { statusCode: 400 });
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
Reference in New Issue
Block a user