//! Repo settings: description, visibility, project linking, deletion. use axum::{ extract::{Path, State}, http::StatusCode, response::{IntoResponse, Redirect}, Form, }; use serde::Deserialize; use tower_sessions::Session; use crate::{ auth::AuthUser, db::{self, ProjectId, Visibility}, error::{AppError, Result}, helpers::get_csrf_token, templates::*, validation, AppState, }; use super::default_ref; /// `GET /git/{owner}/{repo}/settings`: settings form (owner only). #[tracing::instrument(skip_all, name = "git_issues::repo_settings_form")] pub(super) async fn repo_settings_form( State(state): State, session: Session, AuthUser(user): AuthUser, Path((owner, repo_name)): Path<(String, String)>, ) -> Result { let resolved = super::resolve_repo(&state, &owner, &repo_name, Some(user.id)).await?; if user.id != resolved.db_user.id { return Err(AppError::Forbidden); } let projects = db::projects::get_projects_by_user(&state.db, user.id).await?; let linked_project_id = resolved.db_repo.project_id.map(|pid| pid.to_string()).unwrap_or_default(); let (open_issue_count, _) = db::issues::get_issue_counts(&state.db, resolved.db_repo.id).await.unwrap_or((0, 0)); let current_ref = default_ref(&state, &owner, &repo_name); let csrf_token = get_csrf_token(&session).await; Ok(GitRepoSettingsTemplate { csrf_token, session_user: Some(user), owner, repo_name, current_ref, repo: resolved.db_repo, open_issue_count, projects, linked_project_id, }) } #[derive(Deserialize)] pub(super) struct RepoSettingsForm { description: String, visibility: Visibility, project_id: Option, } /// `POST /git/{owner}/{repo}/settings`: save settings (owner only). #[tracing::instrument(skip_all, name = "git_issues::repo_settings_save")] pub(super) async fn repo_settings_save( State(state): State, AuthUser(user): AuthUser, Path((owner, repo_name)): Path<(String, String)>, Form(form): Form, ) -> Result { user.check_not_suspended()?; let resolved = super::resolve_repo(&state, &owner, &repo_name, Some(user.id)).await?; if user.id != resolved.db_user.id { return Err(AppError::Forbidden); } let description = form.description.trim(); validation::validate_repo_description(description)?; // Update description + visibility (enum deserialization handles validation) db::git_repos::update_repo_settings(&state.db, resolved.db_repo.id, description, form.visibility).await?; // Handle project linking let new_project_id: Option = form .project_id .as_deref() .filter(|s| !s.is_empty()) .and_then(|s| s.parse::().ok()); match (resolved.db_repo.project_id, new_project_id) { (Some(_), None) => { // Unlink db::git_repos::unlink_repo_from_project(&state.db, resolved.db_repo.id).await?; } (None, Some(pid)) | (Some(_), Some(pid)) if resolved.db_repo.project_id != Some(pid) => { // Link or change link — verify the project belongs to this user let project = db::projects::get_project_by_id(&state.db, pid) .await? .ok_or(AppError::validation("Project not found".to_string()))?; if project.user_id != user.id { return Err(AppError::Forbidden); } db::git_repos::link_repo_to_project(&state.db, resolved.db_repo.id, pid).await?; } _ => {} // No change } Ok(Redirect::to(&format!("/git/{}/{}/settings", owner, repo_name))) } /// `POST /git/{owner}/{repo}/settings/delete`: delete repo (owner only). #[tracing::instrument(skip_all, name = "git_issues::repo_settings_delete")] pub(super) async fn repo_settings_delete( State(state): State, AuthUser(user): AuthUser, Path((owner, repo_name)): Path<(String, String)>, ) -> Result { user.check_not_suspended()?; let resolved = super::resolve_repo(&state, &owner, &repo_name, Some(user.id)).await?; if user.id != resolved.db_user.id { return Err(AppError::Forbidden); } db::git_repos::delete_repo(&state.db, resolved.db_repo.id).await?; Ok(( StatusCode::OK, [("HX-Redirect", format!("/git/{}", owner))], "", )) }