//! Issue read-only views: list and detail. use crate::extractors::ValidatedQuery; use axum::{ extract::{Path, State}, response::IntoResponse, }; use serde::Deserialize; use tower_sessions::Session; use sqlx::PgPool; use crate::{ auth::MaybeUserVerified, config::Config, db::{self, IssueStatus}, error::{AppError, Result}, helpers::get_csrf_token, templates::{GitIssueDetailTemplate, GitIssueListTemplate}, }; 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(db): State, State(config): State, session: Session, MaybeUserVerified(maybe_user): MaybeUserVerified, Path((owner, repo_name)): Path<(String, String)>, ValidatedQuery(query): ValidatedQuery, ) -> Result { let resolved = super::resolve_repo( &db, &config, &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( &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(&db, resolved.db_repo.id).await?; let current_ref = default_ref(&config, &owner, &repo_name).await; 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!("{owner}+{repo_name}@issues.makenot.work"); 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(db): State, State(config): State, session: Session, MaybeUserVerified(maybe_user): MaybeUserVerified, Path((owner, repo_name, number)): Path<(String, String, i32)>, ) -> Result { let resolved = super::resolve_repo( &db, &config, &owner, &repo_name, maybe_user.as_ref().map(|u| u.id), ) .await?; let issue = db::issues::get_issue_by_number(&db, resolved.db_repo.id, number) .await? .ok_or(AppError::NotFound)?; let author = db::users::get_user_by_id(&db, issue.author_user_id) .await? .ok_or(AppError::NotFound)?; let comments = db::issues::list_comments(&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(&db, resolved.db_repo.id).await?; let current_ref = default_ref(&config, &owner, &repo_name).await; let csrf_token = get_csrf_token(&session).await; let email_address = format!("{owner}+{repo_name}@issues.makenot.work"); 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, }) }