Skip to main content

max / makenotwork

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