Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

api.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

api.tsBlame261 lines · 1 contributor
fc1817aClaude1/**
2 * REST API routes for repository management.
3 */
4
5import { Hono } from "hono";
7437605Claude6import { eq, and, isNull } from "drizzle-orm";
fc1817aClaude7import { db } from "../db";
7437605Claude8import { users, repositories, organizations, orgMembers } from "../db/schema";
fc1817aClaude9import { initBareRepo, repoExists } from "../git/repository";
06d5ffeClaude10import { hashPassword } from "../lib/auth";
7437605Claude11import { orgRoleAtLeast } from "../lib/orgs";
eb3b81aClaude12import { softAuth } from "../middleware/auth";
fc1817aClaude13
14const api = new Hono().basePath("/api");
15
16// Create repository
eb3b81aClaude17api.post("/repos", softAuth, async (c) => {
63807e5Claude18 let body: {
fc1817aClaude19 name: string;
20 owner: string;
7437605Claude21 orgSlug?: string;
fc1817aClaude22 description?: string;
23 isPrivate?: boolean;
63807e5Claude24 };
25 try {
26 body = await c.req.json();
27 } catch {
28 return c.json({ error: "Invalid JSON body" }, 400);
29 }
fc1817aClaude30
31 if (!body.name || !body.owner) {
32 return c.json({ error: "name and owner are required" }, 400);
33 }
34
35 // Validate repo name
36 if (!/^[a-zA-Z0-9._-]+$/.test(body.name)) {
37 return c.json({ error: "Invalid repository name" }, 400);
38 }
39
eb3b81aClaude40 // Auth check after input validation so bad requests still get 400
41 const authUser = c.get("user");
42 if (!authUser) {
43 return c.json({ error: "Unauthorized" }, 401);
44 }
45
63807e5Claude46 try {
7437605Claude47 // Find creator (user who is performing the action)
63807e5Claude48 const [owner] = await db
49 .select()
50 .from(users)
51 .where(eq(users.username, body.owner));
fc1817aClaude52
63807e5Claude53 if (!owner) {
54 return c.json({ error: "Owner not found" }, 404);
55 }
fc1817aClaude56
eb3b81aClaude57 // Verify the authenticated user is the requested owner
58 if (authUser.id !== owner.id) {
59 return c.json({ error: "Forbidden: cannot create repos for another user" }, 403);
60 }
61
7437605Claude62 // B2: if orgSlug supplied, place the repo in the org namespace.
63 // Requires the creator to be an admin+ of the org.
64 let orgId: string | null = null;
65 let namespaceSlug = body.owner;
66 if (body.orgSlug) {
67 const [org] = await db
68 .select({ id: organizations.id, slug: organizations.slug })
69 .from(organizations)
70 .where(eq(organizations.slug, body.orgSlug))
71 .limit(1);
72 if (!org) return c.json({ error: "Organization not found" }, 404);
73
74 const [mem] = await db
75 .select({ role: orgMembers.role })
76 .from(orgMembers)
77 .where(
78 and(eq(orgMembers.orgId, org.id), eq(orgMembers.userId, owner.id))
79 )
80 .limit(1);
81 if (!mem || !orgRoleAtLeast(mem.role, "admin")) {
82 return c.json({ error: "Admin rights required on org" }, 403);
83 }
84 orgId = org.id;
85 namespaceSlug = org.slug;
86 }
87
88 // Duplicate check: scoped to the right namespace.
89 if (orgId) {
90 const [existing] = await db
91 .select({ id: repositories.id })
92 .from(repositories)
93 .where(
94 and(eq(repositories.orgId, orgId), eq(repositories.name, body.name))
95 )
96 .limit(1);
97 if (existing) {
98 return c.json({ error: "Repository already exists" }, 409);
99 }
100 } else {
101 const [existing] = await db
102 .select({ id: repositories.id })
103 .from(repositories)
104 .where(
105 and(
106 eq(repositories.ownerId, owner.id),
107 eq(repositories.name, body.name),
108 isNull(repositories.orgId)
109 )
110 )
111 .limit(1);
112 if (existing) {
113 return c.json({ error: "Repository already exists" }, 409);
114 }
115 }
116 if (await repoExists(namespaceSlug, body.name)) {
63807e5Claude117 return c.json({ error: "Repository already exists" }, 409);
118 }
fc1817aClaude119
7437605Claude120 // Init bare repo on disk, keyed by the namespace slug (user or org).
121 const diskPath = await initBareRepo(namespaceSlug, body.name);
63807e5Claude122
123 // Insert into DB
124 const [repo] = await db
125 .insert(repositories)
126 .values({
127 name: body.name,
128 ownerId: owner.id,
7437605Claude129 orgId,
63807e5Claude130 description: body.description || null,
131 isPrivate: body.isPrivate || false,
132 diskPath,
133 })
134 .returning();
3ef4c9dClaude135
63807e5Claude136 // Green-ecosystem bootstrap: settings, protection, labels, welcome issue
137 if (repo) {
138 const { bootstrapRepository } = await import("../lib/repo-bootstrap");
139 await bootstrapRepository({
140 repositoryId: repo.id,
141 ownerUserId: owner.id,
142 });
143 }
fc1817aClaude144
63807e5Claude145 return c.json(repo, 201);
146 } catch (err) {
147 console.error("[api] POST /repos:", err);
148 return c.json({ error: "Service unavailable" }, 503);
fc1817aClaude149 }
150});
151
152// List user's repositories
153api.get("/users/:username/repos", async (c) => {
154 const { username } = c.req.param();
63807e5Claude155 try {
156 const [owner] = await db
157 .select()
158 .from(users)
159 .where(eq(users.username, username));
160 if (!owner) return c.json({ error: "User not found" }, 404);
161
162 const repos = await db
163 .select()
164 .from(repositories)
165 .where(eq(repositories.ownerId, owner.id));
166
167 return c.json(repos);
168 } catch (err) {
169 console.error("[api] /users/:username/repos:", err);
170 return c.json({ error: "Service unavailable" }, 503);
171 }
fc1817aClaude172});
173
7437605Claude174// Get single repository (resolves both user- and org-owned namespaces)
fc1817aClaude175api.get("/repos/:owner/:name", async (c) => {
176 const { owner: ownerName, name } = c.req.param();
63807e5Claude177 try {
7437605Claude178 const { loadRepoByPath } = await import("../lib/namespace");
179 const repo = await loadRepoByPath(ownerName, name);
63807e5Claude180 if (!repo) return c.json({ error: "Not found" }, 404);
181 return c.json(repo);
182 } catch (err) {
183 console.error("[api] /repos/:owner/:name:", err);
184 return c.json({ error: "Service unavailable" }, 503);
185 }
fc1817aClaude186});
187
eb3b81aClaude188// Quick-setup: create user + repo in one call (dev convenience, disabled in production)
fc1817aClaude189api.post("/setup", async (c) => {
eb3b81aClaude190 if (!process.env.ALLOW_SETUP_ENDPOINT) {
191 return c.json({ error: "Endpoint disabled" }, 403);
192 }
63807e5Claude193 let body: {
fc1817aClaude194 username: string;
195 email: string;
196 repoName: string;
197 description?: string;
63807e5Claude198 };
199 try {
200 body = await c.req.json();
201 } catch {
202 return c.json({ error: "Invalid JSON body" }, 400);
203 }
204 if (!body.username || !body.email || !body.repoName) {
205 return c.json(
206 { error: "username, email, and repoName are required" },
207 400
208 );
fc1817aClaude209 }
210
63807e5Claude211 try {
212 // Upsert user
213 let [user] = await db
214 .select()
215 .from(users)
216 .where(eq(users.username, body.username));
fc1817aClaude217
63807e5Claude218 if (!user) {
219 [user] = await db
220 .insert(users)
221 .values({
222 username: body.username,
223 email: body.email,
224 passwordHash: await hashPassword("changeme"),
225 })
226 .returning();
227 }
fc1817aClaude228
63807e5Claude229 // Create repo if not exists
230 if (!(await repoExists(body.username, body.repoName))) {
231 const diskPath = await initBareRepo(body.username, body.repoName);
232 const [repo] = await db
233 .insert(repositories)
234 .values({
235 name: body.repoName,
236 ownerId: user.id,
237 description: body.description || null,
238 diskPath,
239 })
240 .returning();
241 if (repo) {
242 const { bootstrapRepository } = await import("../lib/repo-bootstrap");
243 await bootstrapRepository({
244 repositoryId: repo.id,
245 ownerUserId: user.id,
246 });
247 }
3ef4c9dClaude248 }
fc1817aClaude249
63807e5Claude250 return c.json({
251 user: user.username,
252 repo: body.repoName,
253 status: "ready",
fc1817aClaude254 });
63807e5Claude255 } catch (err) {
256 console.error("[api] POST /setup:", err);
257 return c.json({ error: "Service unavailable" }, 503);
fc1817aClaude258 }
259});
260
261export default api;