max / makenotwork
4 files changed,
+52 insertions,
-39 deletions
| @@ -7,13 +7,15 @@ use axum::{ | |||
| 7 | 7 | use serde::Deserialize; | |
| 8 | 8 | use tower_sessions::Session; | |
| 9 | 9 | ||
| 10 | + | use sqlx::PgPool; | |
| 11 | + | ||
| 10 | 12 | use crate::{ | |
| 11 | 13 | auth::MaybeUserVerified, | |
| 14 | + | config::Config, | |
| 12 | 15 | db::{self, IssueStatus}, | |
| 13 | 16 | error::{AppError, Result}, | |
| 14 | 17 | helpers::get_csrf_token, | |
| 15 | 18 | templates::*, | |
| 16 | - | AppState, | |
| 17 | 19 | }; | |
| 18 | 20 | ||
| 19 | 21 | use super::default_ref; | |
| @@ -30,13 +32,14 @@ pub(super) struct IssueListQuery { | |||
| 30 | 32 | /// `GET /git/{owner}/{repo}/issues` | |
| 31 | 33 | #[tracing::instrument(skip_all, name = "git_issues::list")] | |
| 32 | 34 | pub(super) async fn issue_list( | |
| 33 | - | State(state): State<AppState>, | |
| 35 | + | State(db): State<PgPool>, | |
| 36 | + | State(config): State<Config>, | |
| 34 | 37 | session: Session, | |
| 35 | 38 | MaybeUserVerified(maybe_user): MaybeUserVerified, | |
| 36 | 39 | Path((owner, repo_name)): Path<(String, String)>, | |
| 37 | 40 | Query(query): Query<IssueListQuery>, | |
| 38 | 41 | ) -> Result<impl IntoResponse> { | |
| 39 | - | let resolved = super::resolve_repo(&state.db, &state.config, &owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?; | |
| 42 | + | let resolved = super::resolve_repo(&db, &config, &owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?; | |
| 40 | 43 | ||
| 41 | 44 | let status_filter = match query.status.as_deref() { | |
| 42 | 45 | Some("closed") => Some(IssueStatus::Closed), | |
| @@ -51,12 +54,12 @@ pub(super) async fn issue_list( | |||
| 51 | 54 | let per_page = 25; | |
| 52 | 55 | ||
| 53 | 56 | let (issues, total) = db::issues::list_issues( | |
| 54 | - | &state.db, resolved.db_repo.id, status_filter, search, page, per_page, | |
| 57 | + | &db, resolved.db_repo.id, status_filter, search, page, per_page, | |
| 55 | 58 | ).await?; | |
| 56 | 59 | ||
| 57 | 60 | 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).await; | |
| 61 | + | let (open_count, closed_count) = db::issues::get_issue_counts(&db, resolved.db_repo.id).await?; | |
| 62 | + | let current_ref = default_ref(&config, &owner, &repo_name).await; | |
| 60 | 63 | let csrf_token = get_csrf_token(&session).await; | |
| 61 | 64 | ||
| 62 | 65 | let is_owner = maybe_user.as_ref().map(|u| u.id) == Some(resolved.db_user.id); | |
| @@ -85,27 +88,28 @@ pub(super) async fn issue_list( | |||
| 85 | 88 | /// `GET /git/{owner}/{repo}/issues/{number}` | |
| 86 | 89 | #[tracing::instrument(skip_all, name = "git_issues::detail")] | |
| 87 | 90 | pub(super) async fn issue_detail( | |
| 88 | - | State(state): State<AppState>, | |
| 91 | + | State(db): State<PgPool>, | |
| 92 | + | State(config): State<Config>, | |
| 89 | 93 | session: Session, | |
| 90 | 94 | MaybeUserVerified(maybe_user): MaybeUserVerified, | |
| 91 | 95 | Path((owner, repo_name, number)): Path<(String, String, i32)>, | |
| 92 | 96 | ) -> Result<impl IntoResponse> { | |
| 93 | - | let resolved = super::resolve_repo(&state.db, &state.config, &owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?; | |
| 97 | + | let resolved = super::resolve_repo(&db, &config, &owner, &repo_name, maybe_user.as_ref().map(|u| u.id)).await?; | |
| 94 | 98 | ||
| 95 | - | let issue = db::issues::get_issue_by_number(&state.db, resolved.db_repo.id, number) | |
| 99 | + | let issue = db::issues::get_issue_by_number(&db, resolved.db_repo.id, number) | |
| 96 | 100 | .await? | |
| 97 | 101 | .ok_or(AppError::NotFound)?; | |
| 98 | 102 | ||
| 99 | - | let author = db::users::get_user_by_id(&state.db, issue.author_user_id) | |
| 103 | + | let author = db::users::get_user_by_id(&db, issue.author_user_id) | |
| 100 | 104 | .await? | |
| 101 | 105 | .ok_or(AppError::NotFound)?; | |
| 102 | 106 | ||
| 103 | - | let comments = db::issues::list_comments(&state.db, issue.id).await?; | |
| 107 | + | let comments = db::issues::list_comments(&db, issue.id).await?; | |
| 104 | 108 | ||
| 105 | 109 | let is_owner = maybe_user.as_ref().map(|u| u.id) == Some(resolved.db_user.id); | |
| 106 | 110 | ||
| 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).await; | |
| 111 | + | let (open_issue_count, _) = db::issues::get_issue_counts(&db, resolved.db_repo.id).await?; | |
| 112 | + | let current_ref = default_ref(&config, &owner, &repo_name).await; | |
| 109 | 113 | let csrf_token = get_csrf_token(&session).await; | |
| 110 | 114 | ||
| 111 | 115 | let email_address = format!("{}+{}@issues.makenot.work", owner, repo_name); |
| @@ -8,6 +8,7 @@ mod settings; | |||
| 8 | 8 | use axum::routing::get; | |
| 9 | 9 | ||
| 10 | 10 | use crate::{ | |
| 11 | + | config::Config, | |
| 11 | 12 | csrf::{post_csrf, post_csrf_skip, CsrfRouter}, | |
| 12 | 13 | routes::git::{resolve_repo, repos_root}, | |
| 13 | 14 | AppState, | |
| @@ -28,8 +29,8 @@ pub fn git_issue_routes() -> CsrfRouter<AppState> { | |||
| 28 | 29 | } | |
| 29 | 30 | ||
| 30 | 31 | /// Get the default branch name for a repo (for nav bar links). | |
| 31 | - | async fn default_ref(state: &AppState, owner: &str, repo_name: &str) -> String { | |
| 32 | - | let root = match repos_root(&state.config) { | |
| 32 | + | async fn default_ref(config: &Config, owner: &str, repo_name: &str) -> String { | |
| 33 | + | let root = match repos_root(config) { | |
| 33 | 34 | Ok(r) => r, | |
| 34 | 35 | Err(_) => return "main".to_string(), | |
| 35 | 36 | }; |
| @@ -10,10 +10,12 @@ use serde::Deserialize; | |||
| 10 | 10 | use std::collections::HashMap; | |
| 11 | 11 | use std::sync::LazyLock; | |
| 12 | 12 | ||
| 13 | + | use sqlx::PgPool; | |
| 14 | + | ||
| 13 | 15 | use crate::{ | |
| 16 | + | config::Config, | |
| 14 | 17 | db::{self, IssueStatus}, | |
| 15 | 18 | error::{AppError, Result, ResultExt}, | |
| 16 | - | AppState, | |
| 17 | 19 | }; | |
| 18 | 20 | ||
| 19 | 21 | use super::repos_root; | |
| @@ -101,12 +103,13 @@ pub(super) struct ProcessPushRequest { | |||
| 101 | 103 | /// Auth: Bearer token (same BUILD_TRIGGER_TOKEN as the build pipeline). | |
| 102 | 104 | #[tracing::instrument(skip_all, name = "git_issues::process_push")] | |
| 103 | 105 | pub(super) async fn process_push( | |
| 104 | - | State(state): State<AppState>, | |
| 106 | + | State(db): State<PgPool>, | |
| 107 | + | State(config): State<Config>, | |
| 105 | 108 | headers: axum::http::HeaderMap, | |
| 106 | 109 | Json(req): Json<ProcessPushRequest>, | |
| 107 | 110 | ) -> Result<impl axum::response::IntoResponse> { | |
| 108 | 111 | // Validate per-repo HMAC (derived from BUILD_TRIGGER_TOKEN, never stored raw) | |
| 109 | - | let trigger_token = state.config.build.trigger_token.as_deref() | |
| 112 | + | let trigger_token = config.build.trigger_token.as_deref() | |
| 110 | 113 | .ok_or(AppError::Internal(anyhow::anyhow!("BUILD_TRIGGER_TOKEN not configured")))?; | |
| 111 | 114 | ||
| 112 | 115 | let auth_header = headers | |
| @@ -122,13 +125,13 @@ pub(super) async fn process_push( | |||
| 122 | 125 | ||
| 123 | 126 | // Look up repo owner + repo | |
| 124 | 127 | let owner_user = db::users::get_user_by_username( | |
| 125 | - | &state.db, | |
| 128 | + | &db, | |
| 126 | 129 | &db::Username::from_trusted(req.repo_owner.clone()), | |
| 127 | 130 | ) | |
| 128 | 131 | .await? | |
| 129 | 132 | .ok_or(AppError::NotFound)?; | |
| 130 | 133 | ||
| 131 | - | let repo = db::git_repos::get_repo_by_user_and_name(&state.db, owner_user.id, &req.repo_name) | |
| 134 | + | let repo = db::git_repos::get_repo_by_user_and_name(&db, owner_user.id, &req.repo_name) | |
| 132 | 135 | .await? | |
| 133 | 136 | .ok_or(AppError::NotFound)?; | |
| 134 | 137 | ||
| @@ -138,7 +141,7 @@ pub(super) async fn process_push( | |||
| 138 | 141 | // it inline parks a tokio worker thread for the duration (Run #2 Perf SERIOUS). | |
| 139 | 142 | // The walk creates and drops all git2 types internally and returns only the | |
| 140 | 143 | // owned, Send `Vec` of collected refs. | |
| 141 | - | let root = repos_root(&state.config)?; | |
| 144 | + | let root = repos_root(&config)?; | |
| 142 | 145 | let repo_owner = req.repo_owner.clone(); | |
| 143 | 146 | let repo_name = req.repo_name.clone(); | |
| 144 | 147 | let after = req.after.clone(); | |
| @@ -197,7 +200,7 @@ pub(super) async fn process_push( | |||
| 197 | 200 | .collect(); | |
| 198 | 201 | numbers.sort_unstable(); | |
| 199 | 202 | numbers.dedup(); | |
| 200 | - | let mut issues = db::issues::get_issues_by_numbers(&state.db, repo.id, &numbers).await?; | |
| 203 | + | let mut issues = db::issues::get_issues_by_numbers(&db, repo.id, &numbers).await?; | |
| 201 | 204 | ||
| 202 | 205 | // Collect the DB writes and flush them in two bulk statements at the end | |
| 203 | 206 | // rather than one status UPDATE + one comment INSERT per referenced commit | |
| @@ -244,10 +247,10 @@ pub(super) async fn process_push( | |||
| 244 | 247 | ||
| 245 | 248 | // Flush: final status per changed issue, then every comment, in two queries. | |
| 246 | 249 | let status_updates: Vec<(db::IssueId, IssueStatus)> = status_changes.into_iter().collect(); | |
| 247 | - | if let Err(e) = db::issues::update_issue_statuses(&state.db, &status_updates).await { | |
| 250 | + | if let Err(e) = db::issues::update_issue_statuses(&db, &status_updates).await { | |
| 248 | 251 | tracing::warn!(error = ?e, "failed to bulk-update issue statuses via push"); | |
| 249 | 252 | } | |
| 250 | - | if let Err(e) = db::issues::create_comments(&state.db, owner_user.id, &comments).await { | |
| 253 | + | if let Err(e) = db::issues::create_comments(&db, owner_user.id, &comments).await { | |
| 251 | 254 | tracing::warn!(error = ?e, "failed to bulk-create issue comments via push"); | |
| 252 | 255 | } | |
| 253 | 256 |
| @@ -9,14 +9,16 @@ use axum::{ | |||
| 9 | 9 | use serde::Deserialize; | |
| 10 | 10 | use tower_sessions::Session; | |
| 11 | 11 | ||
| 12 | + | use sqlx::PgPool; | |
| 13 | + | ||
| 12 | 14 | use crate::{ | |
| 13 | 15 | auth::AuthUser, | |
| 16 | + | config::Config, | |
| 14 | 17 | db::{self, ProjectId, Visibility}, | |
| 15 | 18 | error::{AppError, Result}, | |
| 16 | 19 | helpers::get_csrf_token, | |
| 17 | 20 | templates::*, | |
| 18 | 21 | validation, | |
| 19 | - | AppState, | |
| 20 | 22 | }; | |
| 21 | 23 | ||
| 22 | 24 | use super::default_ref; | |
| @@ -24,21 +26,22 @@ use super::default_ref; | |||
| 24 | 26 | /// `GET /git/{owner}/{repo}/settings`: settings form (owner only). | |
| 25 | 27 | #[tracing::instrument(skip_all, name = "git_issues::repo_settings_form")] | |
| 26 | 28 | pub(super) async fn repo_settings_form( | |
| 27 | - | State(state): State<AppState>, | |
| 29 | + | State(db): State<PgPool>, | |
| 30 | + | State(config): State<Config>, | |
| 28 | 31 | session: Session, | |
| 29 | 32 | AuthUser(user): AuthUser, | |
| 30 | 33 | Path((owner, repo_name)): Path<(String, String)>, | |
| 31 | 34 | ) -> Result<impl IntoResponse> { | |
| 32 | - | let resolved = super::resolve_repo(&state.db, &state.config, &owner, &repo_name, Some(user.id)).await?; | |
| 35 | + | let resolved = super::resolve_repo(&db, &config, &owner, &repo_name, Some(user.id)).await?; | |
| 33 | 36 | ||
| 34 | 37 | if user.id != resolved.db_user.id { | |
| 35 | 38 | return Err(AppError::Forbidden); | |
| 36 | 39 | } | |
| 37 | 40 | ||
| 38 | - | let projects = db::projects::get_projects_by_user(&state.db, user.id).await?; | |
| 41 | + | let projects = db::projects::get_projects_by_user(&db, user.id).await?; | |
| 39 | 42 | let linked_project_id = resolved.db_repo.project_id.map(|pid| pid.to_string()).unwrap_or_default(); | |
| 40 | - | let (open_issue_count, _) = db::issues::get_issue_counts(&state.db, resolved.db_repo.id).await.unwrap_or((0, 0)); | |
| 41 | - | let current_ref = default_ref(&state, &owner, &repo_name).await; | |
| 43 | + | let (open_issue_count, _) = db::issues::get_issue_counts(&db, resolved.db_repo.id).await.unwrap_or((0, 0)); | |
| 44 | + | let current_ref = default_ref(&config, &owner, &repo_name).await; | |
| 42 | 45 | let csrf_token = get_csrf_token(&session).await; | |
| 43 | 46 | ||
| 44 | 47 | Ok(GitRepoSettingsTemplate { | |
| @@ -64,13 +67,14 @@ pub(super) struct RepoSettingsForm { | |||
| 64 | 67 | /// `POST /git/{owner}/{repo}/settings`: save settings (owner only). | |
| 65 | 68 | #[tracing::instrument(skip_all, name = "git_issues::repo_settings_save")] | |
| 66 | 69 | pub(super) async fn repo_settings_save( | |
| 67 | - | State(state): State<AppState>, | |
| 70 | + | State(db): State<PgPool>, | |
| 71 | + | State(config): State<Config>, | |
| 68 | 72 | AuthUser(user): AuthUser, | |
| 69 | 73 | Path((owner, repo_name)): Path<(String, String)>, | |
| 70 | 74 | Form(form): Form<RepoSettingsForm>, | |
| 71 | 75 | ) -> Result<impl IntoResponse> { | |
| 72 | 76 | user.check_not_suspended()?; | |
| 73 | - | let resolved = super::resolve_repo(&state.db, &state.config, &owner, &repo_name, Some(user.id)).await?; | |
| 77 | + | let resolved = super::resolve_repo(&db, &config, &owner, &repo_name, Some(user.id)).await?; | |
| 74 | 78 | ||
| 75 | 79 | if user.id != resolved.db_user.id { | |
| 76 | 80 | return Err(AppError::Forbidden); | |
| @@ -80,7 +84,7 @@ pub(super) async fn repo_settings_save( | |||
| 80 | 84 | validation::validate_repo_description(description)?; | |
| 81 | 85 | ||
| 82 | 86 | // Update description + visibility (enum deserialization handles validation) | |
| 83 | - | db::git_repos::update_repo_settings(&state.db, resolved.db_repo.id, description, form.visibility).await?; | |
| 87 | + | db::git_repos::update_repo_settings(&db, resolved.db_repo.id, description, form.visibility).await?; | |
| 84 | 88 | ||
| 85 | 89 | // Handle project linking | |
| 86 | 90 | let new_project_id: Option<ProjectId> = form | |
| @@ -92,17 +96,17 @@ pub(super) async fn repo_settings_save( | |||
| 92 | 96 | match (resolved.db_repo.project_id, new_project_id) { | |
| 93 | 97 | (Some(_), None) => { | |
| 94 | 98 | // Unlink | |
| 95 | - | db::git_repos::unlink_repo_from_project(&state.db, resolved.db_repo.id).await?; | |
| 99 | + | db::git_repos::unlink_repo_from_project(&db, resolved.db_repo.id).await?; | |
| 96 | 100 | } | |
| 97 | 101 | (None, Some(pid)) | (Some(_), Some(pid)) if resolved.db_repo.project_id != Some(pid) => { | |
| 98 | 102 | // Link or change link — verify the project belongs to this user | |
| 99 | - | let project = db::projects::get_project_by_id(&state.db, pid) | |
| 103 | + | let project = db::projects::get_project_by_id(&db, pid) | |
| 100 | 104 | .await? | |
| 101 | 105 | .ok_or(AppError::validation("Project not found".to_string()))?; | |
| 102 | 106 | if project.user_id != user.id { | |
| 103 | 107 | return Err(AppError::Forbidden); | |
| 104 | 108 | } | |
| 105 | - | db::git_repos::link_repo_to_project(&state.db, resolved.db_repo.id, pid).await?; | |
| 109 | + | db::git_repos::link_repo_to_project(&db, resolved.db_repo.id, pid).await?; | |
| 106 | 110 | } | |
| 107 | 111 | _ => {} // No change | |
| 108 | 112 | } | |
| @@ -113,18 +117,19 @@ pub(super) async fn repo_settings_save( | |||
| 113 | 117 | /// `POST /git/{owner}/{repo}/settings/delete`: delete repo (owner only). | |
| 114 | 118 | #[tracing::instrument(skip_all, name = "git_issues::repo_settings_delete")] | |
| 115 | 119 | pub(super) async fn repo_settings_delete( | |
| 116 | - | State(state): State<AppState>, | |
| 120 | + | State(db): State<PgPool>, | |
| 121 | + | State(config): State<Config>, | |
| 117 | 122 | AuthUser(user): AuthUser, | |
| 118 | 123 | Path((owner, repo_name)): Path<(String, String)>, | |
| 119 | 124 | ) -> Result<impl IntoResponse> { | |
| 120 | 125 | user.check_not_suspended()?; | |
| 121 | - | let resolved = super::resolve_repo(&state.db, &state.config, &owner, &repo_name, Some(user.id)).await?; | |
| 126 | + | let resolved = super::resolve_repo(&db, &config, &owner, &repo_name, Some(user.id)).await?; | |
| 122 | 127 | ||
| 123 | 128 | if user.id != resolved.db_user.id { | |
| 124 | 129 | return Err(AppError::Forbidden); | |
| 125 | 130 | } | |
| 126 | 131 | ||
| 127 | - | db::git_repos::delete_repo(&state.db, resolved.db_repo.id).await?; | |
| 132 | + | db::git_repos::delete_repo(&db, resolved.db_repo.id).await?; | |
| 128 | 133 | ||
| 129 | 134 | Ok(( | |
| 130 | 135 | StatusCode::OK, |