//! Mute or unmute one repository's issue notifications. //! //! Step 6 of the mailing-list plan (wiki `mnw-mailing-lists`). Issue mail used //! to be gated by one account-wide bool, so leaving one noisy repository meant //! leaving all of them. This is the per-repository control that replaces that //! choice. //! //! It lives in the API tree rather than beside the git pages because //! `git_routes` is a bare `Router` with no CSRF envelope, and a POST there //! would silently skip it. use axum::{Form, extract::State, response::IntoResponse}; use serde::Deserialize; use sqlx::PgPool; use crate::{ auth::AuthUser, db::{self, GitRepoId}, error::{AppError, Result}, }; #[derive(Deserialize)] pub(in crate::routes) struct MuteForm { repo_id: String, /// The owner's username, so the redirect lands back where the button was. /// A collaborator is not the owner, so deriving it from the caller would /// send them to a repository that is not theirs. owner: String, /// "true" mutes. Absent or anything else unmutes, so a stray value fails /// toward receiving mail rather than silently dropping it. muted: Option, } /// `POST /api/repos/notifications`: set the caller's mute state for a repo. /// /// Only ever writes the caller's own subscription, so there is nothing to /// authorise beyond being able to see the repository at all. That check is not /// about the mute (muting a repo you cannot see would change nothing); it is so /// this endpoint cannot be used to probe whether a private repository exists. #[tracing::instrument(skip_all, name = "api::set_repo_notifications")] pub(in crate::routes) async fn set_repo_notifications( State(db): State, AuthUser(user): AuthUser, Form(form): Form, ) -> Result { let repo_id: GitRepoId = form .repo_id .parse() .map_err(|_| AppError::BadRequest("Invalid repository".to_string()))?; let Some(repo) = db::git_repos::get_repo_by_id(&db, repo_id).await? else { return Err(AppError::NotFound); }; if repo.visibility == db::Visibility::Private && repo.user_id != user.id && !db::repo_collaborators::is_collaborator(&db, repo.id, user.id).await? { // Same answer a stranger gets for a repo that does not exist. return Err(AppError::NotFound); } let muted = form.muted.as_deref() == Some("true"); db::lists::set_repo_muted( &db, *repo.id.as_uuid(), user.id, db::ListKind::Issues, muted, ) .await?; Ok(axum::response::Redirect::to(&format!( "/git/{}/{}", form.owner, repo.name ))) }