Skip to main content

max / makenotwork

4.9 KB · 156 lines History Blame Raw
1 //! Repo settings: description, visibility, project linking, deletion.
2
3 use axum::{
4 Form,
5 extract::{Path, State},
6 http::StatusCode,
7 response::{IntoResponse, Redirect},
8 };
9 use serde::Deserialize;
10 use tower_sessions::Session;
11
12 use sqlx::PgPool;
13
14 use crate::{
15 auth::AuthUser,
16 config::Config,
17 db::{self, ProjectId, Visibility},
18 error::{AppError, Result},
19 helpers::get_csrf_token,
20 templates::GitRepoSettingsTemplate,
21 validation,
22 };
23
24 use super::default_ref;
25
26 /// `GET /git/{owner}/{repo}/settings`: settings form (owner only).
27 #[tracing::instrument(skip_all, name = "git_issues::repo_settings_form")]
28 pub(super) async fn repo_settings_form(
29 State(db): State<PgPool>,
30 State(config): State<Config>,
31 session: Session,
32 AuthUser(user): AuthUser,
33 Path((owner, repo_name)): Path<(String, String)>,
34 ) -> Result<impl IntoResponse> {
35 let resolved = super::resolve_repo(&db, &config, &owner, &repo_name, Some(user.id)).await?;
36
37 if user.id != resolved.db_user.id {
38 return Err(AppError::Forbidden);
39 }
40
41 let projects = db::projects::get_projects_by_user(&db, user.id).await?;
42 let linked_project_id = resolved
43 .db_repo
44 .project_id
45 .map(|pid| pid.to_string())
46 .unwrap_or_default();
47 let (open_issue_count, _) = db::issues::get_issue_counts(&db, resolved.db_repo.id)
48 .await
49 .unwrap_or((0, 0));
50 let current_ref = default_ref(&config, &owner, &repo_name).await;
51 let csrf_token = get_csrf_token(&session).await;
52
53 Ok(GitRepoSettingsTemplate {
54 csrf_token,
55 session_user: Some(user),
56 owner,
57 repo_name,
58 current_ref,
59 repo: resolved.db_repo,
60 open_issue_count,
61 projects,
62 linked_project_id,
63 })
64 }
65
66 #[derive(Deserialize)]
67 pub(super) struct RepoSettingsForm {
68 description: String,
69 visibility: Visibility,
70 project_id: Option<String>,
71 }
72
73 /// `POST /git/{owner}/{repo}/settings`: save settings (owner only).
74 #[tracing::instrument(skip_all, name = "git_issues::repo_settings_save")]
75 pub(super) async fn repo_settings_save(
76 State(db): State<PgPool>,
77 State(config): State<Config>,
78 AuthUser(user): AuthUser,
79 Path((owner, repo_name)): Path<(String, String)>,
80 Form(form): Form<RepoSettingsForm>,
81 ) -> Result<impl IntoResponse> {
82 user.check_not_suspended()?;
83 let resolved = super::resolve_repo(&db, &config, &owner, &repo_name, Some(user.id)).await?;
84
85 if user.id != resolved.db_user.id {
86 return Err(AppError::Forbidden);
87 }
88
89 // Private permanently: publishing a set of annotations is a moderation and
90 // consent decision, not a toggle on a settings page.
91 if db::git_repos::is_annotation_repo(&resolved.db_repo)
92 && form.visibility != Visibility::Private
93 {
94 return Err(AppError::validation(
95 "Your annotations repository is private permanently.".to_string(),
96 ));
97 }
98
99 let description = form.description.trim();
100 validation::validate_repo_description(description)?;
101
102 // Update description + visibility (enum deserialization handles validation)
103 db::git_repos::update_repo_settings(&db, resolved.db_repo.id, description, form.visibility)
104 .await?;
105
106 // Handle project linking
107 let new_project_id: Option<ProjectId> = form
108 .project_id
109 .as_deref()
110 .filter(|s| !s.is_empty())
111 .and_then(|s| s.parse::<ProjectId>().ok());
112
113 match (resolved.db_repo.project_id, new_project_id) {
114 (Some(_), None) => {
115 db::git_repos::unlink_repo_from_project(&db, resolved.db_repo.id).await?;
116 }
117 (None | Some(_), Some(pid)) if resolved.db_repo.project_id != Some(pid) => {
118 // Link or change link, verify the project belongs to this user
119 let project = db::projects::get_project_by_id(&db, pid)
120 .await?
121 .ok_or(AppError::validation("Project not found".to_string()))?;
122 if project.user_id != user.id {
123 return Err(AppError::Forbidden);
124 }
125 db::git_repos::link_repo_to_project(&db, resolved.db_repo.id, pid).await?;
126 }
127 _ => {} // No change
128 }
129
130 Ok(Redirect::to(&format!("/git/{owner}/{repo_name}/settings")))
131 }
132
133 /// `POST /git/{owner}/{repo}/settings/delete`: delete repo (owner only).
134 #[tracing::instrument(skip_all, name = "git_issues::repo_settings_delete")]
135 pub(super) async fn repo_settings_delete(
136 State(db): State<PgPool>,
137 State(config): State<Config>,
138 AuthUser(user): AuthUser,
139 Path((owner, repo_name)): Path<(String, String)>,
140 ) -> Result<impl IntoResponse> {
141 user.check_not_suspended()?;
142 let resolved = super::resolve_repo(&db, &config, &owner, &repo_name, Some(user.id)).await?;
143
144 if user.id != resolved.db_user.id {
145 return Err(AppError::Forbidden);
146 }
147
148 db::git_repos::delete_repo(&db, resolved.db_repo.id).await?;
149
150 Ok((
151 StatusCode::OK,
152 [("HX-Redirect", format!("/git/{owner}"))],
153 "",
154 ))
155 }
156