From 874ceeb2fb37b07fedfb061a7d1b17e6b02be3dd Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sat, 23 May 2026 20:55:21 -0600 Subject: [PATCH] Adjust disabled logic for admins --- ...da0baad6d1715261d0421e5ea1942d1e8816.json} | 10 +++++-- ...d2717ce8ed1a38d31a9f0652ed4ec9091b419.json | 12 -------- ...cbaf52686e958fc77bfe3be063babdabf4fd.json} | 6 ++-- ...eface93792bad3cd55c8e1e49f4d5053a9236.json | 12 ++++++++ ...6c6a3f18d290d110c41dd832abd86cdf94a8.json} | 20 ++++++++----- ...c2c48f64e4f50a480002f9ec5f0df04f3563.json} | 10 +++++-- ...bc85eb5856032f6890048d51e1eeb6cd25a2.json} | 6 ++-- crates/auth/src/lib.rs | 5 ++++ crates/core/src/models.rs | 6 +++- crates/db/src/lib.rs | 21 +++++++++----- crates/github/src/lib.rs | 8 ++++++ crates/jobs/src/jobs/workflow_run.rs | 18 ++++++++++++ crates/web/src/handlers/manage.rs | 28 +++++++++++++++---- crates/web/src/handlers/report.rs | 2 +- .../22_project_permanently_disabled.sql | 1 + 15 files changed, 121 insertions(+), 44 deletions(-) rename .sqlx/{query-d6b5cfd0b352120901dad884ddae80e6b9e94c78d80511a885a8a336147cc57f.json => query-002db17df22e67f818c3ed30227ada0baad6d1715261d0421e5ea1942d1e8816.json} (84%) delete mode 100644 .sqlx/query-0a643e5596bd37e5ba8018e9decd2717ce8ed1a38d31a9f0652ed4ec9091b419.json rename .sqlx/{query-77fa30390ff028ea9fff086a695e87e65bba44f4344aa4575a081b8d1f6eddb3.json => query-3f80147d7d2c97a4713b2f84bbb4cbaf52686e958fc77bfe3be063babdabf4fd.json} (53%) create mode 100644 .sqlx/query-79caeee92f0cd249bf9326edcf0eface93792bad3cd55c8e1e49f4d5053a9236.json rename .sqlx/{query-86318ebdf4ce242b320f209e79ae7af307f061f92c8d284addfecfe5215476ea.json => query-9d3da451668e4ddfc83fac2bfffe6c6a3f18d290d110c41dd832abd86cdf94a8.json} (72%) rename .sqlx/{query-3d625ccc90b0a896ab906107c60e784df93a969f7a381854eee2777370e90906.json => query-d8009000e3f563e32b08ec811de5c2c48f64e4f50a480002f9ec5f0df04f3563.json} (82%) rename .sqlx/{query-66c72864237c9c44358e1d3a3333a11d00dd4a6d1bb30bc852aaa792089a3403.json => query-f664efe4589ab51c477c2104a878bc85eb5856032f6890048d51e1eeb6cd25a2.json} (65%) create mode 100644 migrations/22_project_permanently_disabled.sql diff --git a/.sqlx/query-d6b5cfd0b352120901dad884ddae80e6b9e94c78d80511a885a8a336147cc57f.json b/.sqlx/query-002db17df22e67f818c3ed30227ada0baad6d1715261d0421e5ea1942d1e8816.json similarity index 84% rename from .sqlx/query-d6b5cfd0b352120901dad884ddae80e6b9e94c78d80511a885a8a336147cc57f.json rename to .sqlx/query-002db17df22e67f818c3ed30227ada0baad6d1715261d0421e5ea1942d1e8816.json index 2211a56..3a98348 100644 --- a/.sqlx/query-d6b5cfd0b352120901dad884ddae80e6b9e94c78d80511a885a8a336147cc57f.json +++ b/.sqlx/query-002db17df22e67f818c3ed30227ada0baad6d1715261d0421e5ea1942d1e8816.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT id AS \"id!\", owner, repo, name, short_name, default_category, default_version, platform, workflow_id, enable_pr_comments, pr_report_style AS \"pr_report_style!\", header_image_id, enabled\n FROM projects\n WHERE id = ?\n ", + "query": "\n SELECT id AS \"id!\", owner, repo, name, short_name, default_category, default_version, platform, workflow_id, enable_pr_comments, pr_report_style AS \"pr_report_style!\", header_image_id, enabled, permanently_disabled\n FROM projects\n WHERE id = ?\n ", "describe": { "columns": [ { @@ -67,6 +67,11 @@ "name": "enabled", "ordinal": 12, "type_info": "Bool" + }, + { + "name": "permanently_disabled", + "ordinal": 13, + "type_info": "Bool" } ], "parameters": { @@ -85,8 +90,9 @@ false, false, true, + false, false ] }, - "hash": "d6b5cfd0b352120901dad884ddae80e6b9e94c78d80511a885a8a336147cc57f" + "hash": "002db17df22e67f818c3ed30227ada0baad6d1715261d0421e5ea1942d1e8816" } diff --git a/.sqlx/query-0a643e5596bd37e5ba8018e9decd2717ce8ed1a38d31a9f0652ed4ec9091b419.json b/.sqlx/query-0a643e5596bd37e5ba8018e9decd2717ce8ed1a38d31a9f0652ed4ec9091b419.json deleted file mode 100644 index 414ae67..0000000 --- a/.sqlx/query-0a643e5596bd37e5ba8018e9decd2717ce8ed1a38d31a9f0652ed4ec9091b419.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO projects (id, owner, repo, name, short_name, default_category, default_version, platform, workflow_id, enable_pr_comments, pr_report_style, header_image_id, enabled, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)\n ON CONFLICT (id) DO NOTHING\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 13 - }, - "nullable": [] - }, - "hash": "0a643e5596bd37e5ba8018e9decd2717ce8ed1a38d31a9f0652ed4ec9091b419" -} diff --git a/.sqlx/query-77fa30390ff028ea9fff086a695e87e65bba44f4344aa4575a081b8d1f6eddb3.json b/.sqlx/query-3f80147d7d2c97a4713b2f84bbb4cbaf52686e958fc77bfe3be063babdabf4fd.json similarity index 53% rename from .sqlx/query-77fa30390ff028ea9fff086a695e87e65bba44f4344aa4575a081b8d1f6eddb3.json rename to .sqlx/query-3f80147d7d2c97a4713b2f84bbb4cbaf52686e958fc77bfe3be063babdabf4fd.json index bfbf46e..82e1360 100644 --- a/.sqlx/query-77fa30390ff028ea9fff086a695e87e65bba44f4344aa4575a081b8d1f6eddb3.json +++ b/.sqlx/query-3f80147d7d2c97a4713b2f84bbb4cbaf52686e958fc77bfe3be063babdabf4fd.json @@ -1,12 +1,12 @@ { "db_name": "SQLite", - "query": "\n INSERT INTO projects (id, owner, repo, name, short_name, default_category, default_version, platform, workflow_id, enable_pr_comments, pr_report_style, header_image_id, enabled, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)\n ", + "query": "\n INSERT INTO projects (id, owner, repo, name, short_name, default_category, default_version, platform, workflow_id, enable_pr_comments, pr_report_style, header_image_id, enabled, permanently_disabled, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)\n ", "describe": { "columns": [], "parameters": { - "Right": 13 + "Right": 14 }, "nullable": [] }, - "hash": "77fa30390ff028ea9fff086a695e87e65bba44f4344aa4575a081b8d1f6eddb3" + "hash": "3f80147d7d2c97a4713b2f84bbb4cbaf52686e958fc77bfe3be063babdabf4fd" } diff --git a/.sqlx/query-79caeee92f0cd249bf9326edcf0eface93792bad3cd55c8e1e49f4d5053a9236.json b/.sqlx/query-79caeee92f0cd249bf9326edcf0eface93792bad3cd55c8e1e49f4d5053a9236.json new file mode 100644 index 0000000..58183a9 --- /dev/null +++ b/.sqlx/query-79caeee92f0cd249bf9326edcf0eface93792bad3cd55c8e1e49f4d5053a9236.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO projects (id, owner, repo, name, short_name, default_category, default_version, platform, workflow_id, enable_pr_comments, pr_report_style, header_image_id, enabled, permanently_disabled, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)\n ON CONFLICT (id) DO NOTHING\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 14 + }, + "nullable": [] + }, + "hash": "79caeee92f0cd249bf9326edcf0eface93792bad3cd55c8e1e49f4d5053a9236" +} diff --git a/.sqlx/query-86318ebdf4ce242b320f209e79ae7af307f061f92c8d284addfecfe5215476ea.json b/.sqlx/query-9d3da451668e4ddfc83fac2bfffe6c6a3f18d290d110c41dd832abd86cdf94a8.json similarity index 72% rename from .sqlx/query-86318ebdf4ce242b320f209e79ae7af307f061f92c8d284addfecfe5215476ea.json rename to .sqlx/query-9d3da451668e4ddfc83fac2bfffe6c6a3f18d290d110c41dd832abd86cdf94a8.json index aa4b17b..aa767ef 100644 --- a/.sqlx/query-86318ebdf4ce242b320f209e79ae7af307f061f92c8d284addfecfe5215476ea.json +++ b/.sqlx/query-9d3da451668e4ddfc83fac2bfffe6c6a3f18d290d110c41dd832abd86cdf94a8.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT\n projects.id AS \"project_id!\",\n owner AS \"owner!\",\n repo AS \"repo!\",\n name,\n short_name,\n default_category,\n default_version,\n platform,\n workflow_id,\n enable_pr_comments AS \"enable_pr_comments!\",\n pr_report_style AS \"pr_report_style!\",\n header_image_id,\n enabled AS \"enabled!\",\n git_commit,\n git_commit_message,\n MAX(timestamp) AS \"timestamp: time::OffsetDateTime\",\n JSON_GROUP_ARRAY(version ORDER BY version)\n FILTER (WHERE version IS NOT NULL) AS versions\n FROM projects LEFT JOIN reports ON (\n reports.project_id = projects.id\n AND reports.timestamp = (\n SELECT MAX(timestamp)\n FROM reports\n WHERE project_id = projects.id\n )\n )\n GROUP BY projects.id\n ORDER BY MAX(timestamp) DESC\n ", + "query": "\n SELECT\n projects.id AS \"project_id!\",\n owner AS \"owner!\",\n repo AS \"repo!\",\n name,\n short_name,\n default_category,\n default_version,\n platform,\n workflow_id,\n enable_pr_comments AS \"enable_pr_comments!\",\n pr_report_style AS \"pr_report_style!\",\n header_image_id,\n enabled AS \"enabled!\",\n permanently_disabled AS \"permanently_disabled!\",\n git_commit,\n git_commit_message,\n MAX(timestamp) AS \"timestamp: time::OffsetDateTime\",\n JSON_GROUP_ARRAY(version ORDER BY version)\n FILTER (WHERE version IS NOT NULL) AS versions\n FROM projects LEFT JOIN reports ON (\n reports.project_id = projects.id\n AND reports.timestamp = (\n SELECT MAX(timestamp)\n FROM reports\n WHERE project_id = projects.id\n )\n )\n GROUP BY projects.id\n ORDER BY MAX(timestamp) DESC\n ", "describe": { "columns": [ { @@ -69,23 +69,28 @@ "type_info": "Bool" }, { - "name": "git_commit", + "name": "permanently_disabled!", "ordinal": 13, - "type_info": "Text" + "type_info": "Bool" }, { - "name": "git_commit_message", + "name": "git_commit", "ordinal": 14, "type_info": "Text" }, { - "name": "timestamp: time::OffsetDateTime", + "name": "git_commit_message", "ordinal": 15, + "type_info": "Text" + }, + { + "name": "timestamp: time::OffsetDateTime", + "ordinal": 16, "type_info": "Datetime" }, { "name": "versions", - "ordinal": 16, + "ordinal": 17, "type_info": "Text" } ], @@ -109,8 +114,9 @@ true, true, true, + true, true ] }, - "hash": "86318ebdf4ce242b320f209e79ae7af307f061f92c8d284addfecfe5215476ea" + "hash": "9d3da451668e4ddfc83fac2bfffe6c6a3f18d290d110c41dd832abd86cdf94a8" } diff --git a/.sqlx/query-3d625ccc90b0a896ab906107c60e784df93a969f7a381854eee2777370e90906.json b/.sqlx/query-d8009000e3f563e32b08ec811de5c2c48f64e4f50a480002f9ec5f0df04f3563.json similarity index 82% rename from .sqlx/query-3d625ccc90b0a896ab906107c60e784df93a969f7a381854eee2777370e90906.json rename to .sqlx/query-d8009000e3f563e32b08ec811de5c2c48f64e4f50a480002f9ec5f0df04f3563.json index 5c0c11b..0b7a2ab 100644 --- a/.sqlx/query-3d625ccc90b0a896ab906107c60e784df93a969f7a381854eee2777370e90906.json +++ b/.sqlx/query-d8009000e3f563e32b08ec811de5c2c48f64e4f50a480002f9ec5f0df04f3563.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT id AS \"id!\", owner, repo, name, short_name, default_category, default_version, platform, workflow_id, enable_pr_comments, pr_report_style AS \"pr_report_style!\", header_image_id, enabled\n FROM projects\n WHERE owner = ? COLLATE NOCASE AND repo = ? COLLATE NOCASE\n ", + "query": "\n SELECT id AS \"id!\", owner, repo, name, short_name, default_category, default_version, platform, workflow_id, enable_pr_comments, pr_report_style AS \"pr_report_style!\", header_image_id, enabled, permanently_disabled\n FROM projects\n WHERE owner = ? COLLATE NOCASE AND repo = ? COLLATE NOCASE\n ", "describe": { "columns": [ { @@ -67,6 +67,11 @@ "name": "enabled", "ordinal": 12, "type_info": "Bool" + }, + { + "name": "permanently_disabled", + "ordinal": 13, + "type_info": "Bool" } ], "parameters": { @@ -85,8 +90,9 @@ false, false, true, + false, false ] }, - "hash": "3d625ccc90b0a896ab906107c60e784df93a969f7a381854eee2777370e90906" + "hash": "d8009000e3f563e32b08ec811de5c2c48f64e4f50a480002f9ec5f0df04f3563" } diff --git a/.sqlx/query-66c72864237c9c44358e1d3a3333a11d00dd4a6d1bb30bc852aaa792089a3403.json b/.sqlx/query-f664efe4589ab51c477c2104a878bc85eb5856032f6890048d51e1eeb6cd25a2.json similarity index 65% rename from .sqlx/query-66c72864237c9c44358e1d3a3333a11d00dd4a6d1bb30bc852aaa792089a3403.json rename to .sqlx/query-f664efe4589ab51c477c2104a878bc85eb5856032f6890048d51e1eeb6cd25a2.json index d264da1..6ed6bc6 100644 --- a/.sqlx/query-66c72864237c9c44358e1d3a3333a11d00dd4a6d1bb30bc852aaa792089a3403.json +++ b/.sqlx/query-f664efe4589ab51c477c2104a878bc85eb5856032f6890048d51e1eeb6cd25a2.json @@ -1,12 +1,12 @@ { "db_name": "SQLite", - "query": "\n UPDATE projects\n SET owner = ?, repo = ?, name = ?, short_name = ?, default_category = ?, default_version = ?, platform = ?, workflow_id = ?, enable_pr_comments = ?, pr_report_style = ?, header_image_id = ?, enabled = ?, updated_at = CURRENT_TIMESTAMP\n WHERE id = ?\n ", + "query": "\n UPDATE projects\n SET owner = ?, repo = ?, name = ?, short_name = ?, default_category = ?, default_version = ?, platform = ?, workflow_id = ?, enable_pr_comments = ?, pr_report_style = ?, header_image_id = ?, enabled = ?, permanently_disabled = ?, updated_at = CURRENT_TIMESTAMP\n WHERE id = ?\n ", "describe": { "columns": [], "parameters": { - "Right": 13 + "Right": 14 }, "nullable": [] }, - "hash": "66c72864237c9c44358e1d3a3333a11d00dd4a6d1bb30bc852aaa792089a3403" + "hash": "f664efe4589ab51c477c2104a878bc85eb5856032f6890048d51e1eeb6cd25a2" } diff --git a/crates/auth/src/lib.rs b/crates/auth/src/lib.rs index a3f7e51..24c5e2d 100644 --- a/crates/auth/src/lib.rs +++ b/crates/auth/src/lib.rs @@ -11,6 +11,7 @@ use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; use decomp_dev_core::{ AppError, config::{Config, GitHubConfig}, + models::Project, }; use decomp_dev_github::graphql::{ CurrentUserResponse, RepositoryPermission, fetch_current_user, fetch_simple_current_user, @@ -87,6 +88,10 @@ impl CurrentUser { pub fn can_manage_repo(&self, id: u64) -> bool { matches!(self.permissions_for_repo(id), RepositoryPermission::Admin) } + + pub fn can_manage_project(&self, project: &Project) -> bool { + self.super_admin || (!project.permanently_disabled && self.can_manage_repo(project.id)) + } } pub fn generate_nonce() -> String { diff --git a/crates/core/src/models.rs b/crates/core/src/models.rs index 7996a5b..6903667 100644 --- a/crates/core/src/models.rs +++ b/crates/core/src/models.rs @@ -62,6 +62,7 @@ pub struct Project { pub pr_report_style: PullReportStyle, pub header_image_id: Option, pub enabled: bool, + pub permanently_disabled: bool, } impl Default for Project { @@ -80,6 +81,7 @@ impl Default for Project { pr_report_style: PullReportStyle::Comment, header_image_id: None, enabled: true, + permanently_disabled: false, } } } @@ -100,6 +102,8 @@ impl Project { pub fn repo_url(&self) -> String { format!("https://github.com/{}/{}", self.owner, self.repo) } pub fn default_category(&self) -> &str { self.default_category.as_deref().unwrap_or("all") } + + pub fn reports_enabled(&self) -> bool { self.enabled && !self.permanently_disabled } } #[derive(Debug, Clone, Eq, PartialEq, Serialize)] @@ -301,7 +305,7 @@ pub enum ProjectVisibility { pub fn project_visibility(project: &Project, measures: Option<&Measures>) -> ProjectVisibility { // Hide projects with less than 0.5% matched code or if the project is disabled - if !project.enabled { + if !project.reports_enabled() { ProjectVisibility::Disabled } else if measures.is_none_or(|m| m.matched_code_percent < 0.5) { ProjectVisibility::Hidden diff --git a/crates/db/src/lib.rs b/crates/db/src/lib.rs index ab68998..6b4319b 100644 --- a/crates/db/src/lib.rs +++ b/crates/db/src/lib.rs @@ -88,8 +88,8 @@ impl Database { let pr_report_style = project.pr_report_style.as_str(); sqlx::query!( r#" - INSERT INTO projects (id, owner, repo, name, short_name, default_category, default_version, platform, workflow_id, enable_pr_comments, pr_report_style, header_image_id, enabled, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + INSERT INTO projects (id, owner, repo, name, short_name, default_category, default_version, platform, workflow_id, enable_pr_comments, pr_report_style, header_image_id, enabled, permanently_disabled, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) ON CONFLICT (id) DO NOTHING "#, project_id, @@ -105,6 +105,7 @@ impl Database { pr_report_style, header_image_id, project.enabled, + project.permanently_disabled, ) .execute(&mut *tx) .await?; @@ -414,7 +415,7 @@ impl Database { ) -> Result> { Ok(sqlx::query!( r#" - SELECT id AS "id!", owner, repo, name, short_name, default_category, default_version, platform, workflow_id, enable_pr_comments, pr_report_style AS "pr_report_style!", header_image_id, enabled + SELECT id AS "id!", owner, repo, name, short_name, default_category, default_version, platform, workflow_id, enable_pr_comments, pr_report_style AS "pr_report_style!", header_image_id, enabled, permanently_disabled FROM projects WHERE owner = ? COLLATE NOCASE AND repo = ? COLLATE NOCASE "#, @@ -438,6 +439,7 @@ impl Database { pr_report_style: row.pr_report_style.parse().unwrap_or_default(), header_image_id: row.header_image_id.and_then(|b| b.try_into().ok()), enabled: row.enabled, + permanently_disabled: row.permanently_disabled, } })) } @@ -450,7 +452,7 @@ impl Database { let project_id_db = project_id as i64; Ok(sqlx::query!( r#" - SELECT id AS "id!", owner, repo, name, short_name, default_category, default_version, platform, workflow_id, enable_pr_comments, pr_report_style AS "pr_report_style!", header_image_id, enabled + SELECT id AS "id!", owner, repo, name, short_name, default_category, default_version, platform, workflow_id, enable_pr_comments, pr_report_style AS "pr_report_style!", header_image_id, enabled, permanently_disabled FROM projects WHERE id = ? "#, @@ -473,6 +475,7 @@ impl Database { pr_report_style: row.pr_report_style.parse().unwrap_or_default(), header_image_id: row.header_image_id.and_then(|b| b.try_into().ok()), enabled: row.enabled, + permanently_disabled: row.permanently_disabled, } })) } @@ -632,6 +635,7 @@ impl Database { pr_report_style AS "pr_report_style!", header_image_id, enabled AS "enabled!", + permanently_disabled AS "permanently_disabled!", git_commit, git_commit_message, MAX(timestamp) AS "timestamp: time::OffsetDateTime", @@ -667,6 +671,7 @@ impl Database { pr_report_style: row.pr_report_style.parse().unwrap_or_default(), header_image_id: row.header_image_id.and_then(|b| b.try_into().ok()), enabled: row.enabled, + permanently_disabled: row.permanently_disabled, }, commit: match (row.git_commit, row.timestamp) { (Some(sha), Some(timestamp)) => Some(Commit { @@ -1025,7 +1030,7 @@ impl Database { sqlx::query!( r#" UPDATE projects - SET owner = ?, repo = ?, name = ?, short_name = ?, default_category = ?, default_version = ?, platform = ?, workflow_id = ?, enable_pr_comments = ?, pr_report_style = ?, header_image_id = ?, enabled = ?, updated_at = CURRENT_TIMESTAMP + SET owner = ?, repo = ?, name = ?, short_name = ?, default_category = ?, default_version = ?, platform = ?, workflow_id = ?, enable_pr_comments = ?, pr_report_style = ?, header_image_id = ?, enabled = ?, permanently_disabled = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? "#, project.owner, @@ -1040,6 +1045,7 @@ impl Database { pr_report_style, header_image_id, project.enabled, + project.permanently_disabled, project_id, ) .execute(&mut *conn) @@ -1054,8 +1060,8 @@ impl Database { let pr_report_style = project.pr_report_style.as_str(); sqlx::query!( r#" - INSERT INTO projects (id, owner, repo, name, short_name, default_category, default_version, platform, workflow_id, enable_pr_comments, pr_report_style, header_image_id, enabled, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + INSERT INTO projects (id, owner, repo, name, short_name, default_category, default_version, platform, workflow_id, enable_pr_comments, pr_report_style, header_image_id, enabled, permanently_disabled, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) "#, project_id, project.owner, @@ -1070,6 +1076,7 @@ impl Database { pr_report_style, header_image_id, project.enabled, + project.permanently_disabled, ) .execute(&mut *conn) .await?; diff --git a/crates/github/src/lib.rs b/crates/github/src/lib.rs index f147377..2d4605c 100644 --- a/crates/github/src/lib.rs +++ b/crates/github/src/lib.rs @@ -250,6 +250,14 @@ pub async fn refresh_project( .await .context("Failed to fetch project info")? .with_context(|| format!("Failed to fetch project info for ID {repo_id}"))?; + if !project_info.project.reports_enabled() { + tracing::info!( + "Skipping disabled project {}/{}", + project_info.project.owner, + project_info.project.repo + ); + return Ok(0); + } let client = match client_override { Some(client) => client.clone(), None => github.client_for(repo_id).await?, diff --git a/crates/jobs/src/jobs/workflow_run.rs b/crates/jobs/src/jobs/workflow_run.rs index d1d9948..9d883a5 100644 --- a/crates/jobs/src/jobs/workflow_run.rs +++ b/crates/jobs/src/jobs/workflow_run.rs @@ -98,6 +98,15 @@ async fn process_workflow_run_push(ctx: &JobContext, job: &ProcessWorkflowRunJob tracing::warn!("No project found for repository ID {}", job.repository_id); return Ok(()); }; + if !project_info.project.reports_enabled() { + tracing::info!( + "Skipping disabled project {}/{} for workflow run {}", + project_info.project.owner, + project_info.project.repo, + job.run_id + ); + return Ok(()); + } // Fetch repository info let client = ctx.github.client_for(job.repository_id.0).await?; @@ -189,6 +198,15 @@ async fn process_workflow_run_pull_request( tracing::warn!("No project found for repository ID {}", job.repository_id); return Ok(()); }; + if !project_info.project.reports_enabled() { + tracing::info!( + "Skipping disabled project {}/{} for workflow run {}", + project_info.project.owner, + project_info.project.repo, + job.run_id + ); + return Ok(()); + } if !project_info.project.enable_pr_comments { return Ok(()); diff --git a/crates/web/src/handlers/manage.rs b/crates/web/src/handlers/manage.rs index 7894e09..ad43cba 100644 --- a/crates/web/src/handlers/manage.rs +++ b/crates/web/src/handlers/manage.rs @@ -42,7 +42,7 @@ pub async fn manage( .get_projects() .await? .into_iter() - .filter(|p| current_user.can_manage_repo(p.project.id)) + .filter(|p| current_user.can_manage_project(&p.project)) .sorted_by(|a, b| lexicmp::natural_lexical_cmp(&a.project.name(), &b.project.name())) .collect::>(); @@ -347,7 +347,7 @@ pub async fn manage_project( let Some(info) = state.db.get_project_info(¶ms.owner, ¶ms.repo, None).await? else { return Err(AppError::Status(StatusCode::NOT_FOUND)); }; - if !current_user.can_manage_repo(info.project.id) { + if !current_user.can_manage_project(&info.project) { return Err(AppError::Status(StatusCode::FORBIDDEN)); } @@ -468,6 +468,15 @@ async fn render_manage_project( br; small.muted { "Disabled projects will not be listed, and reports will not be fetched." } } + @if current_user.super_admin { + label { + input name="permanently_disabled" type="checkbox" role="switch" + checked[project_info.project.permanently_disabled]; + "Permanently disable project" + br; + small.muted { "Project owners cannot manage permanently disabled projects." } + } + } label { "Repository" input type="text" readonly disabled value=(project_info.project.repo_url()); @@ -592,6 +601,7 @@ pub struct ProjectForm { pub header_image: Option, pub clear_header_image: Option, pub enabled: Option, + pub permanently_disabled: Option, } #[derive(Deserialize)] @@ -610,7 +620,7 @@ pub async fn manage_project_save( else { return Err(AppError::Status(StatusCode::NOT_FOUND)); }; - if !current_user.can_manage_repo(project_info.project.id) { + if !current_user.can_manage_project(&project_info.project) { return Err(AppError::Status(StatusCode::FORBIDDEN)); } if !ALL_PLATFORMS.iter().any(|p| p.to_str() == form.platform) { @@ -639,6 +649,11 @@ pub async fn manage_project_save( let short_name = form.short_name.trim(); let platform = form.platform.trim(); let workflow_id = form.workflow_id.trim(); + let permanently_disabled = if current_user.super_admin { + form.permanently_disabled.is_some_and(|v| v == "on") + } else { + project_info.project.permanently_disabled + }; let project = Project { id: project_info.project.id, owner: project_info.project.owner, @@ -661,7 +676,8 @@ pub async fn manage_project_save( project_info.project.pr_report_style }, header_image_id, - enabled: form.enabled.is_some_and(|v| v == "on"), + enabled: form.enabled.is_some_and(|v| v == "on") && !permanently_disabled, + permanently_disabled, }; state.db.update_project(&project).await?; let redirect_url = format!("/{}/{}", params.owner, params.repo); @@ -677,7 +693,7 @@ pub async fn manage_project_refresh( let Some(info) = state.db.get_project_info(¶ms.owner, ¶ms.repo, None).await? else { return Err(AppError::Status(StatusCode::NOT_FOUND)); }; - if !current_user.can_manage_repo(info.project.id) { + if !current_user.can_manage_project(&info.project) { return Err(AppError::Status(StatusCode::FORBIDDEN)); } @@ -713,7 +729,7 @@ pub async fn delete_commit( let Some(info) = state.db.get_project_info(¶ms.owner, ¶ms.repo, None).await? else { return Err(AppError::Status(StatusCode::NOT_FOUND)); }; - if !current_user.can_manage_repo(info.project.id) { + if !current_user.can_manage_project(&info.project) { return Err(AppError::Status(StatusCode::FORBIDDEN)); } let num_reports_deleted = diff --git a/crates/web/src/handlers/report.rs b/crates/web/src/handlers/report.rs index 51ee91f..e4ee8c4 100644 --- a/crates/web/src/handlers/report.rs +++ b/crates/web/src/handlers/report.rs @@ -722,7 +722,7 @@ async fn render_report( let project_manage_path = format!("/manage/{}/{}", project_info.project.owner, project_info.project.repo); let can_manage = - current_user.as_ref().is_some_and(|u| u.can_manage_repo(project_info.project.id)); + current_user.as_ref().is_some_and(|u| u.can_manage_project(&project_info.project)); let default_category = project_info.project.default_category(); let is_default_version = project_info.default_version() == Some(report.version.as_str()); diff --git a/migrations/22_project_permanently_disabled.sql b/migrations/22_project_permanently_disabled.sql new file mode 100644 index 0000000..fa22a3c --- /dev/null +++ b/migrations/22_project_permanently_disabled.sql @@ -0,0 +1 @@ +ALTER TABLE projects ADD COLUMN permanently_disabled BOOLEAN NOT NULL DEFAULT FALSE;