ggdgsdbsdbbb / api_v1_routes.v
801 lines · 751 sloc · 23.2 KB · 9d8beeb8b8e6ea32e5695e1904210470824df5b2
Raw
1// Copyright (c) 2019-2026 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by a GPL license that can be found in the LICENSE file.
3module main
4
5import veb
6
7struct ApiRepoView {
8 id int
9 name string
10 full_name string
11 user_name string
12 description string
13 is_public bool
14 stars int
15 open_issues int
16 open_prs int
17 branches int
18 created_at int
19}
20
21struct ApiIssueView {
22 id int
23 number int
24 repo_id int
25 title string
26 body string
27 author string
28 status string
29 comments_count int
30 created_at int
31}
32
33struct ApiPullView {
34 id int
35 repo_id int
36 title string
37 description string
38 head_branch string
39 base_branch string
40 author string
41 status string
42 comments_count int
43 created_at int
44 merged_at int
45}
46
47struct ApiUserView {
48 id int
49 username string
50 full_name string
51 avatar string
52}
53
54struct ApiCommentView {
55 id int
56 author string
57 text string
58 created_at int
59}
60
61fn (mut app App) repo_to_api(repo Repo) ApiRepoView {
62 return ApiRepoView{
63 id: repo.id
64 name: repo.name
65 full_name: '${repo.user_name}/${repo.name}'
66 user_name: repo.user_name
67 description: repo.description
68 is_public: repo.is_public
69 stars: repo.nr_stars
70 open_issues: repo.nr_open_issues
71 open_prs: repo.nr_open_prs
72 branches: repo.nr_branches
73 created_at: repo.created_at
74 }
75}
76
77fn (mut app App) issue_to_api(issue Issue) ApiIssueView {
78 author := app.get_username_by_id(issue.author_id) or { '' }
79 status := if issue.status == .closed { 'closed' } else { 'open' }
80 return ApiIssueView{
81 id: issue.id
82 number: issue.id
83 repo_id: issue.repo_id
84 title: issue.title
85 body: issue.text
86 author: author
87 status: status
88 comments_count: issue.comments_count
89 created_at: issue.created_at
90 }
91}
92
93fn (mut app App) pr_to_api(pr PullRequest) ApiPullView {
94 author := app.get_username_by_id(pr.author_id) or { '' }
95 status := match unsafe { PrStatus(pr.status) } {
96 .open { 'open' }
97 .closed { 'closed' }
98 .merged { 'merged' }
99 }
100
101 return ApiPullView{
102 id: pr.id
103 repo_id: pr.repo_id
104 title: pr.title
105 description: pr.description
106 head_branch: pr.head_branch
107 base_branch: pr.base_branch
108 author: author
109 status: status
110 comments_count: pr.comments_count
111 created_at: pr.created_at
112 merged_at: pr.merged_at
113 }
114}
115
116fn (ctx &Context) api_bearer_token() string {
117 header := ctx.get_header(.authorization) or { return '' }
118 parts := header.fields()
119 if parts.len != 2 || parts[0] != 'Bearer' {
120 return ''
121 }
122 return parts[1]
123}
124
125fn (mut app App) api_user_from_ctx(ctx &Context) ?User {
126 token := ctx.api_bearer_token()
127 if token == '' {
128 if ctx.logged_in {
129 return ctx.user
130 }
131 return none
132 }
133 return app.user_for_api_token(token)
134}
135
136fn (mut ctx Context) api_unauthorized() veb.Result {
137 ctx.send_custom_error(401, 'Unauthorized')
138 return ctx.json({
139 'success': 'false'
140 'message': 'authentication required'
141 })
142}
143
144fn (mut ctx Context) api_not_found() veb.Result {
145 ctx.send_custom_error(404, 'Not Found')
146 return ctx.json({
147 'success': 'false'
148 'message': 'not found'
149 })
150}
151
152@['/api/v1/me']
153pub fn (mut app App) api_v1_me(mut ctx Context) veb.Result {
154 user := app.api_user_from_ctx(ctx) or { return ctx.api_unauthorized() }
155 return ctx.json(ApiUserView{
156 id: user.id
157 username: user.username
158 full_name: user.full_name
159 avatar: user.avatar
160 })
161}
162
163@['/api/v1/users/:username']
164pub fn (mut app App) api_v1_user(mut ctx Context, username string) veb.Result {
165 user := app.get_user_by_username(username) or { return ctx.api_not_found() }
166 return ctx.json(ApiUserView{
167 id: user.id
168 username: user.username
169 full_name: user.full_name
170 avatar: user.avatar
171 })
172}
173
174@['/api/v1/users/:username/repos']
175pub fn (mut app App) api_v1_user_repos(mut ctx Context, username string) veb.Result {
176 user := app.get_user_by_username(username) or { return ctx.api_not_found() }
177 repos := app.find_user_public_repos(user.id)
178 mut out := []ApiRepoView{cap: repos.len}
179 for r in repos {
180 out << app.repo_to_api(r)
181 }
182 return ctx.json(out)
183}
184
185@['/api/v1/repos/:username/:repo_name']
186pub fn (mut app App) api_v1_repo(mut ctx Context, username string, repo_name string) veb.Result {
187 repo := app.find_repo_by_name_and_username(repo_name, username) or {
188 return ctx.api_not_found()
189 }
190 caller := app.api_user_from_ctx(ctx) or { User{} }
191 if !repo.is_public && !app.has_user_repo_read_access(ctx, caller.id, repo.id) {
192 return ctx.api_not_found()
193 }
194 return ctx.json(app.repo_to_api(repo))
195}
196
197@['/api/v1/repos/:username/:repo_name/issues']
198pub fn (mut app App) api_v1_repo_issues(mut ctx Context, username string, repo_name string) veb.Result {
199 repo := app.find_repo_by_name_and_username(repo_name, username) or {
200 return ctx.api_not_found()
201 }
202 caller := app.api_user_from_ctx(ctx) or { User{} }
203 if !repo.is_public && !app.has_user_repo_read_access(ctx, caller.id, repo.id) {
204 return ctx.api_not_found()
205 }
206 issues := app.find_repo_issues_as_page(repo.id, 0)
207 mut out := []ApiIssueView{cap: issues.len}
208 for i in issues {
209 out << app.issue_to_api(i)
210 }
211 return ctx.json(out)
212}
213
214@['/api/v1/repos/:username/:repo_name/issues/:id']
215pub fn (mut app App) api_v1_repo_issue(mut ctx Context, username string, repo_name string, id string) veb.Result {
216 repo := app.find_repo_by_name_and_username(repo_name, username) or {
217 return ctx.api_not_found()
218 }
219 caller := app.api_user_from_ctx(ctx) or { User{} }
220 if !repo.is_public && !app.has_user_repo_read_access(ctx, caller.id, repo.id) {
221 return ctx.api_not_found()
222 }
223 issue := app.find_issue_by_id(id.int()) or { return ctx.api_not_found() }
224 if issue.repo_id != repo.id {
225 return ctx.api_not_found()
226 }
227 return ctx.json(app.issue_to_api(issue))
228}
229
230@['/api/v1/repos/:username/:repo_name/issues'; post]
231pub fn (mut app App) api_v1_create_issue(mut ctx Context, username string, repo_name string) veb.Result {
232 user := app.api_user_from_ctx(ctx) or { return ctx.api_unauthorized() }
233 repo := app.find_repo_by_name_and_username(repo_name, username) or {
234 return ctx.api_not_found()
235 }
236 if !app.has_user_repo_read_access(ctx, user.id, repo.id) && !repo.is_public {
237 return ctx.api_not_found()
238 }
239 title := ctx.form['title']
240 body := ctx.form['body']
241 if title == '' {
242 ctx.send_custom_error(400, 'Bad Request')
243 return ctx.json({
244 'success': 'false'
245 'message': 'title is required'
246 })
247 }
248 new_id := app.add_issue_returning_id(repo.id, user.id, title, body) or {
249 ctx.send_custom_error(500, 'Internal Server Error')
250 return ctx.json({
251 'success': 'false'
252 'message': 'failed to create issue'
253 })
254 }
255 issue := app.find_issue_by_id(new_id) or { return ctx.api_not_found() }
256 return ctx.json(app.issue_to_api(issue))
257}
258
259@['/api/v1/repos/:username/:repo_name/pulls']
260pub fn (mut app App) api_v1_repo_pulls(mut ctx Context, username string, repo_name string) veb.Result {
261 repo := app.find_repo_by_name_and_username(repo_name, username) or {
262 return ctx.api_not_found()
263 }
264 caller := app.api_user_from_ctx(ctx) or { User{} }
265 if !repo.is_public && !app.has_user_repo_read_access(ctx, caller.id, repo.id) {
266 return ctx.api_not_found()
267 }
268 prs := app.find_repo_pull_requests(repo.id, .open)
269 mut out := []ApiPullView{cap: prs.len}
270 for pr in prs {
271 out << app.pr_to_api(pr)
272 }
273 return ctx.json(out)
274}
275
276@['/api/v1/repos/:username/:repo_name/pulls/:id']
277pub fn (mut app App) api_v1_repo_pull(mut ctx Context, username string, repo_name string, id string) veb.Result {
278 repo := app.find_repo_by_name_and_username(repo_name, username) or {
279 return ctx.api_not_found()
280 }
281 caller := app.api_user_from_ctx(ctx) or { User{} }
282 if !repo.is_public && !app.has_user_repo_read_access(ctx, caller.id, repo.id) {
283 return ctx.api_not_found()
284 }
285 pr := app.find_pull_request_by_id(id.int()) or { return ctx.api_not_found() }
286 if pr.repo_id != repo.id {
287 return ctx.api_not_found()
288 }
289 return ctx.json(app.pr_to_api(pr))
290}
291
292@['/api/v1/repos/:username/:repo_name/pulls/:id/comments']
293pub fn (mut app App) api_v1_pull_comments(mut ctx Context, username string, repo_name string, id string) veb.Result {
294 repo := app.find_repo_by_name_and_username(repo_name, username) or {
295 return ctx.api_not_found()
296 }
297 caller := app.api_user_from_ctx(ctx) or { User{} }
298 if !repo.is_public && !app.has_user_repo_read_access(ctx, caller.id, repo.id) {
299 return ctx.api_not_found()
300 }
301 pr := app.find_pull_request_by_id(id.int()) or { return ctx.api_not_found() }
302 if pr.repo_id != repo.id {
303 return ctx.api_not_found()
304 }
305 comments := app.get_pr_comments(pr.id)
306 mut out := []ApiCommentView{cap: comments.len}
307 for c in comments {
308 author := app.get_username_by_id(c.author_id) or { '' }
309 out << ApiCommentView{
310 id: c.id
311 author: author
312 text: c.text
313 created_at: c.created_at
314 }
315 }
316 return ctx.json(out)
317}
318
319struct ApiDiscussionView {
320 id int
321 repo_id int
322 title string
323 body string
324 category string
325 author string
326 is_locked bool
327 is_answered bool
328 answer_id int
329 comments_count int
330 created_at int
331}
332
333struct ApiMilestoneView {
334 id int
335 repo_id int
336 title string
337 description string
338 due_date int
339 is_closed bool
340 created_at int
341}
342
343struct ApiProjectView {
344 id int
345 repo_id int
346 name string
347 description string
348 created_at int
349}
350
351struct ApiProjectCardView {
352 id int
353 column_id int
354 title string
355 note string
356 position int
357 issue_id int
358 created_at int
359}
360
361struct ApiProjectColumnView {
362 id int
363 project_id int
364 name string
365 position int
366 cards []ApiProjectCardView
367}
368
369struct ApiProjectDetailView {
370 project ApiProjectView
371 columns []ApiProjectColumnView
372}
373
374struct ApiWebhookView {
375 id int
376 repo_id int
377 url string
378 events []string
379 is_active bool
380 last_status int
381 last_delivery int
382 created_at int
383}
384
385fn (mut app App) discussion_to_api(d Discussion) ApiDiscussionView {
386 author := app.get_username_by_id(d.author_id) or { '' }
387 return ApiDiscussionView{
388 id: d.id
389 repo_id: d.repo_id
390 title: d.title
391 body: d.body
392 category: d.category
393 author: author
394 is_locked: d.is_locked
395 is_answered: d.is_answered
396 answer_id: d.answer_id
397 comments_count: d.comments_count
398 created_at: d.created_at
399 }
400}
401
402fn (mut app App) milestone_to_api(m Milestone) ApiMilestoneView {
403 return ApiMilestoneView{
404 id: m.id
405 repo_id: m.repo_id
406 title: m.title
407 description: m.description
408 due_date: m.due_date
409 is_closed: m.is_closed
410 created_at: m.created_at
411 }
412}
413
414fn (mut app App) project_to_api(p Project) ApiProjectView {
415 return ApiProjectView{
416 id: p.id
417 repo_id: p.repo_id
418 name: p.name
419 description: p.description
420 created_at: p.created_at
421 }
422}
423
424fn (mut app App) project_card_to_api(c ProjectCard) ApiProjectCardView {
425 return ApiProjectCardView{
426 id: c.id
427 column_id: c.column_id
428 title: c.title
429 note: c.note
430 position: c.position
431 issue_id: c.issue_id
432 created_at: c.created_at
433 }
434}
435
436fn (w &Webhook) to_api() ApiWebhookView {
437 return ApiWebhookView{
438 id: w.id
439 repo_id: w.repo_id
440 url: w.url
441 events: w.event_list()
442 is_active: w.is_active
443 last_status: w.last_status
444 last_delivery: w.last_delivery
445 created_at: w.created_at
446 }
447}
448
449@['/api/v1/repos/:username/:repo_name/discussions']
450pub fn (mut app App) api_v1_repo_discussions(mut ctx Context, username string, repo_name string) veb.Result {
451 repo := app.find_repo_by_name_and_username(repo_name, username) or {
452 return ctx.api_not_found()
453 }
454 caller := app.api_user_from_ctx(ctx) or { User{} }
455 if !repo.is_public && !app.has_user_repo_read_access(ctx, caller.id, repo.id) {
456 return ctx.api_not_found()
457 }
458 discussions := app.list_repo_discussions(repo.id)
459 mut out := []ApiDiscussionView{cap: discussions.len}
460 for d in discussions {
461 out << app.discussion_to_api(d)
462 }
463 return ctx.json(out)
464}
465
466@['/api/v1/repos/:username/:repo_name/discussions/:id']
467pub fn (mut app App) api_v1_repo_discussion(mut ctx Context, username string, repo_name string, id string) veb.Result {
468 repo := app.find_repo_by_name_and_username(repo_name, username) or {
469 return ctx.api_not_found()
470 }
471 caller := app.api_user_from_ctx(ctx) or { User{} }
472 if !repo.is_public && !app.has_user_repo_read_access(ctx, caller.id, repo.id) {
473 return ctx.api_not_found()
474 }
475 discussion := app.find_discussion(id.int()) or { return ctx.api_not_found() }
476 if discussion.repo_id != repo.id {
477 return ctx.api_not_found()
478 }
479 return ctx.json(app.discussion_to_api(discussion))
480}
481
482@['/api/v1/repos/:username/:repo_name/discussions'; post]
483pub fn (mut app App) api_v1_create_discussion(mut ctx Context, username string, repo_name string) veb.Result {
484 user := app.api_user_from_ctx(ctx) or { return ctx.api_unauthorized() }
485 repo := app.find_repo_by_name_and_username(repo_name, username) or {
486 return ctx.api_not_found()
487 }
488 if !repo.is_public && !app.has_user_repo_read_access(ctx, user.id, repo.id) {
489 return ctx.api_not_found()
490 }
491 title := ctx.form['title']
492 body := ctx.form['body']
493 raw_cat := ctx.form['category']
494 if title == '' {
495 ctx.send_custom_error(400, 'Bad Request')
496 return ctx.json({
497 'success': 'false'
498 'message': 'title is required'
499 })
500 }
501 cat := if raw_cat in ['general', 'qa', 'announcement', 'idea'] { raw_cat } else { 'general' }
502 new_id := app.add_discussion(repo.id, user.id, title, body, cat) or {
503 ctx.send_custom_error(500, 'Internal Server Error')
504 return ctx.json({
505 'success': 'false'
506 'message': 'failed to create discussion'
507 })
508 }
509 discussion := app.find_discussion(new_id) or { return ctx.api_not_found() }
510 return ctx.json(app.discussion_to_api(discussion))
511}
512
513@['/api/v1/repos/:username/:repo_name/discussions/:id/comments']
514pub fn (mut app App) api_v1_discussion_comments(mut ctx Context, username string, repo_name string, id string) veb.Result {
515 repo := app.find_repo_by_name_and_username(repo_name, username) or {
516 return ctx.api_not_found()
517 }
518 caller := app.api_user_from_ctx(ctx) or { User{} }
519 if !repo.is_public && !app.has_user_repo_read_access(ctx, caller.id, repo.id) {
520 return ctx.api_not_found()
521 }
522 discussion := app.find_discussion(id.int()) or { return ctx.api_not_found() }
523 if discussion.repo_id != repo.id {
524 return ctx.api_not_found()
525 }
526 comments := app.get_discussion_comments(discussion.id)
527 mut out := []ApiCommentView{cap: comments.len}
528 for c in comments {
529 author := app.get_username_by_id(c.author_id) or { '' }
530 out << ApiCommentView{
531 id: c.id
532 author: author
533 text: c.text
534 created_at: c.created_at
535 }
536 }
537 return ctx.json(out)
538}
539
540@['/api/v1/repos/:username/:repo_name/discussions/:id/comments'; post]
541pub fn (mut app App) api_v1_create_discussion_comment(mut ctx Context, username string, repo_name string, id string) veb.Result {
542 user := app.api_user_from_ctx(ctx) or { return ctx.api_unauthorized() }
543 repo := app.find_repo_by_name_and_username(repo_name, username) or {
544 return ctx.api_not_found()
545 }
546 if !repo.is_public && !app.has_user_repo_read_access(ctx, user.id, repo.id) {
547 return ctx.api_not_found()
548 }
549 discussion := app.find_discussion(id.int()) or { return ctx.api_not_found() }
550 if discussion.repo_id != repo.id {
551 return ctx.api_not_found()
552 }
553 if discussion.is_locked {
554 ctx.send_custom_error(403, 'Forbidden')
555 return ctx.json({
556 'success': 'false'
557 'message': 'discussion is locked'
558 })
559 }
560 text := ctx.form['text']
561 if text == '' {
562 ctx.send_custom_error(400, 'Bad Request')
563 return ctx.json({
564 'success': 'false'
565 'message': 'text is required'
566 })
567 }
568 app.add_discussion_comment(discussion.id, user.id, text) or {
569 ctx.send_custom_error(500, 'Internal Server Error')
570 return ctx.json({
571 'success': 'false'
572 'message': 'failed to add comment'
573 })
574 }
575 return ctx.json({
576 'success': 'true'
577 })
578}
579
580@['/api/v1/repos/:username/:repo_name/milestones']
581pub fn (mut app App) api_v1_repo_milestones(mut ctx Context, username string, repo_name string) veb.Result {
582 repo := app.find_repo_by_name_and_username(repo_name, username) or {
583 return ctx.api_not_found()
584 }
585 caller := app.api_user_from_ctx(ctx) or { User{} }
586 if !repo.is_public && !app.has_user_repo_read_access(ctx, caller.id, repo.id) {
587 return ctx.api_not_found()
588 }
589 milestones := app.list_repo_milestones(repo.id)
590 mut out := []ApiMilestoneView{cap: milestones.len}
591 for m in milestones {
592 out << app.milestone_to_api(m)
593 }
594 return ctx.json(out)
595}
596
597@['/api/v1/repos/:username/:repo_name/milestones/:id']
598pub fn (mut app App) api_v1_repo_milestone(mut ctx Context, username string, repo_name string, id string) veb.Result {
599 repo := app.find_repo_by_name_and_username(repo_name, username) or {
600 return ctx.api_not_found()
601 }
602 caller := app.api_user_from_ctx(ctx) or { User{} }
603 if !repo.is_public && !app.has_user_repo_read_access(ctx, caller.id, repo.id) {
604 return ctx.api_not_found()
605 }
606 milestone := app.find_milestone(id.int()) or { return ctx.api_not_found() }
607 if milestone.repo_id != repo.id {
608 return ctx.api_not_found()
609 }
610 return ctx.json(app.milestone_to_api(milestone))
611}
612
613@['/api/v1/repos/:username/:repo_name/milestones'; post]
614pub fn (mut app App) api_v1_create_milestone(mut ctx Context, username string, repo_name string) veb.Result {
615 user := app.api_user_from_ctx(ctx) or { return ctx.api_unauthorized() }
616 repo := app.find_repo_by_name_and_username(repo_name, username) or {
617 return ctx.api_not_found()
618 }
619 if repo.user_id != user.id {
620 ctx.send_custom_error(403, 'Forbidden')
621 return ctx.json({
622 'success': 'false'
623 'message': 'only the repo owner can create milestones'
624 })
625 }
626 title := ctx.form['title']
627 desc := ctx.form['description']
628 due := parse_yyyy_mm_dd(ctx.form['due_date'])
629 if title == '' {
630 ctx.send_custom_error(400, 'Bad Request')
631 return ctx.json({
632 'success': 'false'
633 'message': 'title is required'
634 })
635 }
636 new_id := app.add_milestone(repo.id, title, desc, due) or {
637 ctx.send_custom_error(500, 'Internal Server Error')
638 return ctx.json({
639 'success': 'false'
640 'message': 'failed to create milestone'
641 })
642 }
643 milestone := app.find_milestone(new_id) or { return ctx.api_not_found() }
644 return ctx.json(app.milestone_to_api(milestone))
645}
646
647@['/api/v1/repos/:username/:repo_name/projects']
648pub fn (mut app App) api_v1_repo_projects(mut ctx Context, username string, repo_name string) veb.Result {
649 repo := app.find_repo_by_name_and_username(repo_name, username) or {
650 return ctx.api_not_found()
651 }
652 caller := app.api_user_from_ctx(ctx) or { User{} }
653 if !repo.is_public && !app.has_user_repo_read_access(ctx, caller.id, repo.id) {
654 return ctx.api_not_found()
655 }
656 projects := app.list_repo_projects(repo.id)
657 mut out := []ApiProjectView{cap: projects.len}
658 for p in projects {
659 out << app.project_to_api(p)
660 }
661 return ctx.json(out)
662}
663
664@['/api/v1/repos/:username/:repo_name/projects/:id']
665pub fn (mut app App) api_v1_repo_project(mut ctx Context, username string, repo_name string, id string) veb.Result {
666 repo := app.find_repo_by_name_and_username(repo_name, username) or {
667 return ctx.api_not_found()
668 }
669 caller := app.api_user_from_ctx(ctx) or { User{} }
670 if !repo.is_public && !app.has_user_repo_read_access(ctx, caller.id, repo.id) {
671 return ctx.api_not_found()
672 }
673 project := app.find_project(id.int()) or { return ctx.api_not_found() }
674 if project.repo_id != repo.id {
675 return ctx.api_not_found()
676 }
677 columns := app.list_project_columns(project.id)
678 mut col_views := []ApiProjectColumnView{cap: columns.len}
679 for col in columns {
680 cards := app.list_project_cards(col.id)
681 mut card_views := []ApiProjectCardView{cap: cards.len}
682 for c in cards {
683 card_views << app.project_card_to_api(c)
684 }
685 col_views << ApiProjectColumnView{
686 id: col.id
687 project_id: col.project_id
688 name: col.name
689 position: col.position
690 cards: card_views
691 }
692 }
693 return ctx.json(ApiProjectDetailView{
694 project: app.project_to_api(project)
695 columns: col_views
696 })
697}
698
699@['/api/v1/repos/:username/:repo_name/projects'; post]
700pub fn (mut app App) api_v1_create_project(mut ctx Context, username string, repo_name string) veb.Result {
701 user := app.api_user_from_ctx(ctx) or { return ctx.api_unauthorized() }
702 repo := app.find_repo_by_name_and_username(repo_name, username) or {
703 return ctx.api_not_found()
704 }
705 if repo.user_id != user.id {
706 ctx.send_custom_error(403, 'Forbidden')
707 return ctx.json({
708 'success': 'false'
709 'message': 'only the repo owner can create projects'
710 })
711 }
712 name := ctx.form['name']
713 desc := ctx.form['description']
714 if name == '' {
715 ctx.send_custom_error(400, 'Bad Request')
716 return ctx.json({
717 'success': 'false'
718 'message': 'name is required'
719 })
720 }
721 new_id := app.add_project(repo.id, name, desc) or {
722 ctx.send_custom_error(500, 'Internal Server Error')
723 return ctx.json({
724 'success': 'false'
725 'message': 'failed to create project'
726 })
727 }
728 project := app.find_project(new_id) or { return ctx.api_not_found() }
729 return ctx.json(app.project_to_api(project))
730}
731
732@['/api/v1/repos/:username/:repo_name/webhooks']
733pub fn (mut app App) api_v1_repo_webhooks(mut ctx Context, username string, repo_name string) veb.Result {
734 user := app.api_user_from_ctx(ctx) or { return ctx.api_unauthorized() }
735 repo := app.find_repo_by_name_and_username(repo_name, username) or {
736 return ctx.api_not_found()
737 }
738 if repo.user_id != user.id {
739 return ctx.api_not_found()
740 }
741 hooks := app.list_repo_webhooks(repo.id)
742 mut out := []ApiWebhookView{cap: hooks.len}
743 for w in hooks {
744 out << w.to_api()
745 }
746 return ctx.json(out)
747}
748
749@['/api/v1/repos/:username/:repo_name/webhooks/:id']
750pub fn (mut app App) api_v1_repo_webhook(mut ctx Context, username string, repo_name string, id string) veb.Result {
751 user := app.api_user_from_ctx(ctx) or { return ctx.api_unauthorized() }
752 repo := app.find_repo_by_name_and_username(repo_name, username) or {
753 return ctx.api_not_found()
754 }
755 if repo.user_id != user.id {
756 return ctx.api_not_found()
757 }
758 wh := app.find_webhook_by_id(id.int()) or { return ctx.api_not_found() }
759 if wh.repo_id != repo.id {
760 return ctx.api_not_found()
761 }
762 return ctx.json(wh.to_api())
763}
764
765@['/api/v1/repos/:username/:repo_name/webhooks'; post]
766pub fn (mut app App) api_v1_create_webhook(mut ctx Context, username string, repo_name string) veb.Result {
767 user := app.api_user_from_ctx(ctx) or { return ctx.api_unauthorized() }
768 repo := app.find_repo_by_name_and_username(repo_name, username) or {
769 return ctx.api_not_found()
770 }
771 if repo.user_id != user.id {
772 ctx.send_custom_error(403, 'Forbidden')
773 return ctx.json({
774 'success': 'false'
775 'message': 'only the repo owner can create webhooks'
776 })
777 }
778 url := ctx.form['url'].trim_space()
779 secret := ctx.form['secret']
780 events := ctx.form['events'].trim_space()
781 if url == '' || !(url.starts_with('http://') || url.starts_with('https://')) {
782 ctx.send_custom_error(400, 'Bad Request')
783 return ctx.json({
784 'success': 'false'
785 'message': 'valid http(s) url is required'
786 })
787 }
788 events_str := if events == '' { 'push,issue,pr,comment,release' } else { events }
789 app.add_webhook(repo.id, url, secret, events_str) or {
790 ctx.send_custom_error(500, 'Internal Server Error')
791 return ctx.json({
792 'success': 'false'
793 'message': 'failed to create webhook'
794 })
795 }
796 hooks := app.list_repo_webhooks(repo.id)
797 if hooks.len == 0 {
798 return ctx.api_not_found()
799 }
800 return ctx.json(hooks[0].to_api())
801}
802