Skip to main content

max / makenotwork

4.0 KB · 127 lines History Blame Raw
1 //! Issue read-only views: list and detail.
2
3 use axum::{
4 extract::{Path, Query, State},
5 response::IntoResponse,
6 };
7 use serde::Deserialize;
8 use tower_sessions::Session;
9
10 use crate::{
11 auth::MaybeUserVerified,
12 db::{self, IssueStatus},
13 error::{AppError, Result},
14 helpers::get_csrf_token,
15 templates::*,
16 AppState,
17 };
18
19 use super::default_ref;
20
21 // ── Issue List ──
22
23 #[derive(Deserialize)]
24 pub(super) struct IssueListQuery {
25 status: Option<String>,
26 search: Option<String>,
27 page: Option<i64>,
28 }
29
30 /// `GET /git/{owner}/{repo}/issues`
31 #[tracing::instrument(skip_all, name = "git_issues::list")]
32 pub(super) async fn issue_list(
33 State(state): State<AppState>,
34 session: Session,
35 MaybeUserVerified(maybe_user): MaybeUserVerified,
36 Path((owner, repo_name)): Path<(String, String)>,
37 Query(query): Query<IssueListQuery>,
38 ) -> Result<impl IntoResponse> {
39 let resolved = super::resolve_repo(&state, &owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?;
40
41 let status_filter = match query.status.as_deref() {
42 Some("closed") => Some(IssueStatus::Closed),
43 _ => Some(IssueStatus::Open), // default to open
44 };
45 let current_status = query.status.as_deref().unwrap_or("open").to_string();
46 let search = query.search.as_deref().filter(|s| !s.is_empty());
47 // Upper-clamp page so `OFFSET = (page-1)*per_page` in list_issues can't
48 // overflow i64 on a hostile `?page=` (1e9 pages × 25 is already 25B rows,
49 // far past any real repo). Mirrors the admin user-list clamp.
50 let page = query.page.unwrap_or(1).clamp(1, 1_000_000_000);
51 let per_page = 25;
52
53 let (issues, total) = db::issues::list_issues(
54 &state.db, resolved.db_repo.id, status_filter, search, page, per_page,
55 ).await?;
56
57 let total_pages = (total + per_page - 1) / per_page;
58 let (open_count, closed_count) = db::issues::get_issue_counts(&state.db, resolved.db_repo.id).await?;
59 let current_ref = default_ref(&state, &owner, &repo_name);
60 let csrf_token = get_csrf_token(&session).await;
61
62 let is_owner = maybe_user.as_ref().map(|u| u.id) == Some(resolved.db_user.id);
63 let email_address = format!("{}+{}@issues.makenot.work", owner, repo_name);
64
65 Ok(GitIssueListTemplate {
66 csrf_token,
67 session_user: maybe_user,
68 owner,
69 repo_name,
70 current_ref,
71 issues,
72 open_count,
73 closed_count,
74 current_status,
75 search_query: query.search.unwrap_or_default(),
76 current_page: page,
77 total_pages,
78 is_owner,
79 email_address,
80 })
81 }
82
83 // ── Issue Detail ──
84
85 /// `GET /git/{owner}/{repo}/issues/{number}`
86 #[tracing::instrument(skip_all, name = "git_issues::detail")]
87 pub(super) async fn issue_detail(
88 State(state): State<AppState>,
89 session: Session,
90 MaybeUserVerified(maybe_user): MaybeUserVerified,
91 Path((owner, repo_name, number)): Path<(String, String, i32)>,
92 ) -> Result<impl IntoResponse> {
93 let resolved = super::resolve_repo(&state, &owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?;
94
95 let issue = db::issues::get_issue_by_number(&state.db, resolved.db_repo.id, number)
96 .await?
97 .ok_or(AppError::NotFound)?;
98
99 let author = db::users::get_user_by_id(&state.db, issue.author_user_id)
100 .await?
101 .ok_or(AppError::NotFound)?;
102
103 let comments = db::issues::list_comments(&state.db, issue.id).await?;
104
105 let is_owner = maybe_user.as_ref().map(|u| u.id) == Some(resolved.db_user.id);
106
107 let (open_issue_count, _) = db::issues::get_issue_counts(&state.db, resolved.db_repo.id).await?;
108 let current_ref = default_ref(&state, &owner, &repo_name);
109 let csrf_token = get_csrf_token(&session).await;
110
111 let email_address = format!("{}+{}@issues.makenot.work", owner, repo_name);
112
113 Ok(GitIssueDetailTemplate {
114 csrf_token,
115 session_user: maybe_user,
116 owner,
117 repo_name,
118 current_ref,
119 issue,
120 author_username: author.username.to_string(),
121 comments,
122 is_owner,
123 open_issue_count,
124 email_address,
125 })
126 }
127