1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
|
import { Router, Request, Response } from 'express';
import { getAdminDb, getProjectDb } from '../db.js';
import { createBareRepo, setRepoDescription, repoExists, deleteRepo } from '../services/git.service.js';
const router = Router();
interface Project {
id: number;
name: string;
display_name: string;
description: string | null;
repo_name: string;
repo_path: string;
default_branch: string;
created_at: string;
updated_at: string;
}
// GET /api/projects
router.get('/', (_req: Request, res: Response) => {
const db = getAdminDb();
const projects = db.prepare('SELECT * FROM projects ORDER BY created_at DESC').all();
res.json(projects);
});
// POST /api/projects
router.post('/', (req: Request, res: Response) => {
const { name, display_name, description } = req.body;
if (!name || !display_name) {
res.status(400).json({ error: 'name and display_name are required' });
return;
}
// Sanitize name: lowercase, alphanumeric + hyphens
const safeName = name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
if (!safeName) {
res.status(400).json({ error: 'Invalid project name' });
return;
}
const repoName = `${safeName}.git`;
const db = getAdminDb();
// Check for duplicate
const existing = db.prepare('SELECT id FROM projects WHERE name = ?').get(safeName);
if (existing) {
res.status(409).json({ error: 'Project already exists' });
return;
}
// Create bare git repo
const repo = createBareRepo(repoName);
if (description) {
setRepoDescription(repoName, description);
}
// Insert into admin DB
const result = db.prepare(
'INSERT INTO projects (name, display_name, description, repo_name, repo_path) VALUES (?, ?, ?, ?, ?)'
).run(safeName, display_name, description || null, repoName, repo.path);
// Initialize project DB (creates tables)
getProjectDb(safeName);
const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(result.lastInsertRowid);
res.status(201).json(project);
});
// GET /api/projects/:name
router.get('/:name', (req: Request, res: Response) => {
const db = getAdminDb();
const project = db.prepare('SELECT * FROM projects WHERE name = ?').get(req.params.name) as Project | undefined;
if (!project) {
res.status(404).json({ error: 'Project not found' });
return;
}
res.json(project);
});
// PATCH /api/projects/:name
router.patch('/:name', (req: Request, res: Response) => {
const db = getAdminDb();
const project = db.prepare('SELECT * FROM projects WHERE name = ?').get(req.params.name) as Project | undefined;
if (!project) {
res.status(404).json({ error: 'Project not found' });
return;
}
const { display_name, description } = req.body;
const updates: string[] = [];
const params: unknown[] = [];
if (display_name !== undefined) {
updates.push('display_name = ?');
params.push(display_name);
}
if (description !== undefined) {
updates.push('description = ?');
params.push(description);
if (description) {
setRepoDescription(project.repo_name, description);
}
}
if (updates.length === 0) {
res.json(project);
return;
}
updates.push("updated_at = datetime('now')");
params.push(req.params.name);
db.prepare(`UPDATE projects SET ${updates.join(', ')} WHERE name = ?`).run(...params);
const updated = db.prepare('SELECT * FROM projects WHERE name = ?').get(req.params.name);
res.json(updated);
});
// DELETE /api/projects/:name
router.delete('/:name', (req: Request, res: Response) => {
const db = getAdminDb();
const project = db.prepare('SELECT * FROM projects WHERE name = ?').get(req.params.name) as Project | undefined;
if (!project) {
res.status(404).json({ error: 'Project not found' });
return;
}
db.prepare('DELETE FROM projects WHERE name = ?').run(req.params.name);
// Optionally delete repo (controlled by query param)
if (req.query.delete_repo === 'true') {
deleteRepo(project.repo_name);
}
res.status(204).send();
});
export default router;
|