Skip to main content

max / makenotwork

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