Adjust disabled logic for admins

This commit is contained in:
Luke Street
2026-05-23 20:55:21 -06:00
parent 95bd534e97
commit 874ceeb2fb
15 changed files with 121 additions and 44 deletions
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
+5
View File
@@ -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 {
+5 -1
View File
@@ -62,6 +62,7 @@ pub struct Project {
pub pr_report_style: PullReportStyle,
pub header_image_id: Option<ImageId>,
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
+14 -7
View File
@@ -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<Option<Project>> {
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?;
+8
View File
@@ -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?,
+18
View File
@@ -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(());
+22 -6
View File
@@ -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::<Vec<_>>();
@@ -347,7 +347,7 @@ pub async fn manage_project(
let Some(info) = state.db.get_project_info(&params.owner, &params.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<Bytes>,
pub clear_header_image: Option<String>,
pub enabled: Option<String>,
pub permanently_disabled: Option<String>,
}
#[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(&params.owner, &params.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(&params.owner, &params.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 =
+1 -1
View File
@@ -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());
@@ -0,0 +1 @@
ALTER TABLE projects ADD COLUMN permanently_disabled BOOLEAN NOT NULL DEFAULT FALSE;