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 |