//! Issue read-only views: list and detail. use axum::{ extract::{Path, Query, State}, response::IntoResponse, }; use serde::Deserialize; use tower_sessions::Session; use crate::{ auth::MaybeUserVerified, db::{self, IssueStatus}, error::{AppError, Result}, helpers::get_csrf_token, templates::*, AppState, }; use super::default_ref; // ── Issue List ── #[derive(Deserialize)] pub(super) struct IssueListQuery { status: Option, search: Option, page: Option, } /// `GET /git/{owner}/{repo}/issues` #[tracing::instrument(skip_all, name = "git_issues::list")] pub(super) async fn issue_list( State(state): State, session: Session, MaybeUserVerified(maybe_user): MaybeUserVerified, Path((owner, repo_name)): Path<(String, String)>, Query(query): Query, ) -> Result { let resolved = super::resolve_repo(&state, &owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?; let status_filter = match query.status.as_deref() { Some("closed") => Some(IssueStatus::Closed), _ => Some(IssueStatus::Open), // default to open }; let current_status = query.status.as_deref().unwrap_or("open").to_string(); let search = query.search.as_deref().filter(|s| !s.is_empty()); // Upper-clamp page so `OFFSET = (page-1)*per_page` in list_issues can't // overflow i64 on a hostile `?page=` (1e9 pages × 25 is already 25B rows, // far past any real repo). Mirrors the admin user-list clamp. let page = query.page.unwrap_or(1).clamp(1, 1_000_000_000); let per_page = 25; let (issues, total) = db::issues::list_issues( &state.db, resolved.db_repo.id, status_filter, search, page, per_page, ).await?; let total_pages = (total + per_page - 1) / per_page; let (open_count, closed_count) = db::issues::get_issue_counts(&state.db, resolved.db_repo.id).await?; let current_ref = default_ref(&state, &owner, &repo_name); let csrf_token = get_csrf_token(&session).await; let is_owner = maybe_user.as_ref().map(|u| u.id) == Some(resolved.db_user.id); let email_address = format!("{}+{}@issues.makenot.work", owner, repo_name); Ok(GitIssueListTemplate { csrf_token, session_user: maybe_user, owner, repo_name, current_ref, issues, open_count, closed_count, current_status, search_query: query.search.unwrap_or_default(), current_page: page, total_pages, is_owner, email_address, }) } // ── Issue Detail ── /// `GET /git/{owner}/{repo}/issues/{number}` #[tracing::instrument(skip_all, name = "git_issues::detail")] pub(super) async fn issue_detail( State(state): State, session: Session, MaybeUserVerified(maybe_user): MaybeUserVerified, Path((owner, repo_name, number)): Path<(String, String, i32)>, ) -> Result { let resolved = super::resolve_repo(&state, &owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?; let issue = db::issues::get_issue_by_number(&state.db, resolved.db_repo.id, number) .await? .ok_or(AppError::NotFound)?; let author = db::users::get_user_by_id(&state.db, issue.author_user_id) .await? .ok_or(AppError::NotFound)?; let comments = db::issues::list_comments(&state.db, issue.id).await?; let is_owner = maybe_user.as_ref().map(|u| u.id) == Some(resolved.db_user.id); let (open_issue_count, _) = db::issues::get_issue_counts(&state.db, resolved.db_repo.id).await?; let current_ref = default_ref(&state, &owner, &repo_name); let csrf_token = get_csrf_token(&session).await; let email_address = format!("{}+{}@issues.makenot.work", owner, repo_name); Ok(GitIssueDetailTemplate { csrf_token, session_user: maybe_user, owner, repo_name, current_ref, issue, author_username: author.username.to_string(), comments, is_owner, open_issue_count, email_address, }) }