116 lines · 3.2 KB
1 /**
2 * REST API: Repository endpoints.
3 */
4
5 import { requireAuth, authenticate } from '../../middleware/auth.js';
6 import { ValidationError } from '@gitfastr/shared/utils/errors.js';
7 import { RepoService } from '../../services/repo-service.js';
8 import type { HandlerContext } from '../../router.js';
9 import type { DbRepository } from '@gitfastr/shared';
10
11 export async function handleApiRepos(
12 request: Request,
13 hctx: HandlerContext
14 ): Promise<Response> {
15 // List repos — public repos for anonymous, all accessible repos for authenticated users
16 const userId = await authenticate(request, hctx.env.DB);
17
18 let repos: DbRepository[];
19
20 if (userId) {
21 // User's own repos + repos they collaborate on + public repos
22 const result = await hctx.env.DB.prepare(
23 `SELECT DISTINCT r.*, u.username as owner_username
24 FROM repositories r
25 JOIN users u ON r.owner_id = u.id
26 WHERE r.is_private = 0
27 OR r.owner_id = ?
28 OR EXISTS (SELECT 1 FROM collaborators c WHERE c.repo_id = r.id AND c.user_id = ?)
29 ORDER BY r.updated_at DESC
30 LIMIT 50`
31 )
32 .bind(userId, userId)
33 .all();
34
35 repos = result.results as any;
36 } else {
37 const result = await hctx.env.DB.prepare(
38 `SELECT r.*, u.username as owner_username
39 FROM repositories r
40 JOIN users u ON r.owner_id = u.id
41 WHERE r.is_private = 0
42 ORDER BY r.updated_at DESC
43 LIMIT 50`
44 ).all();
45
46 repos = result.results as any;
47 }
48
49 return Response.json(repos);
50 }
51
52 export async function handleApiCreateRepo(
53 request: Request,
54 hctx: HandlerContext
55 ): Promise<Response> {
56 const userId = await requireAuth(request, hctx.env.DB);
57
58 const body = await request.json<{
59 name: string;
60 description?: string;
61 is_private?: boolean;
62 default_branch?: string;
63 }>();
64
65 if (!body.name) {
66 throw new ValidationError('Repository name is required');
67 }
68
69 if (!/^[a-zA-Z0-9._-]{1,64}$/.test(body.name)) {
70 throw new ValidationError(
71 'Repository name must be 1-64 characters, alphanumeric, dots, hyphens, or underscores'
72 );
73 }
74
75 try {
76 const result = await hctx.env.DB.prepare(
77 `INSERT INTO repositories (owner_id, name, description, is_private, default_branch)
78 VALUES (?, ?, ?, ?, ?)
79 RETURNING *`
80 )
81 .bind(
82 userId,
83 body.name,
84 body.description ?? null,
85 body.is_private ? 1 : 0,
86 body.default_branch ?? 'main'
87 )
88 .first<DbRepository>();
89
90 return Response.json(result, { status: 201 });
91 } catch (error: any) {
92 if (error.message?.includes('UNIQUE constraint')) {
93 throw new ValidationError('Repository name already exists');
94 }
95 throw error;
96 }
97 }
98
99 export async function handleApiRepoDetail(
100 request: Request,
101 hctx: HandlerContext
102 ): Promise<Response> {
103 const { owner, repo: repoName } = hctx.params;
104 const repoService = new RepoService(hctx.env.DB);
105 const { repo } = await repoService.getByOwnerAndName(owner, repoName);
106
107 // Check read access
108 const userId = await authenticate(request, hctx.env.DB);
109 const hasAccess = await repoService.hasReadAccess(repo, userId);
110 if (!hasAccess) {
111 return Response.json({ error: 'Not found' }, { status: 404 });
112 }
113
114 return Response.json({ ...repo, owner_username: owner });
115 }
116