Skip to main content

max / makenotwork

4.5 KB · 146 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 let description = form.description.trim();
90 validation::validate_repo_description(description)?;
91
92 // Update description + visibility (enum deserialization handles validation)
93 db::git_repos::update_repo_settings(&db, resolved.db_repo.id, description, form.visibility)
94 .await?;
95
96 // Handle project linking
97 let new_project_id: Option<ProjectId> = form
98 .project_id
99 .as_deref()
100 .filter(|s| !s.is_empty())
101 .and_then(|s| s.parse::<ProjectId>().ok());
102
103 match (resolved.db_repo.project_id, new_project_id) {
104 (Some(_), None) => {
105 db::git_repos::unlink_repo_from_project(&db, resolved.db_repo.id).await?;
106 }
107 (None | Some(_), Some(pid)) if resolved.db_repo.project_id != Some(pid) => {
108 // Link or change link, verify the project belongs to this user
109 let project = db::projects::get_project_by_id(&db, pid)
110 .await?
111 .ok_or(AppError::validation("Project not found".to_string()))?;
112 if project.user_id != user.id {
113 return Err(AppError::Forbidden);
114 }
115 db::git_repos::link_repo_to_project(&db, resolved.db_repo.id, pid).await?;
116 }
117 _ => {} // No change
118 }
119
120 Ok(Redirect::to(&format!("/git/{owner}/{repo_name}/settings")))
121 }
122
123 /// `POST /git/{owner}/{repo}/settings/delete`: delete repo (owner only).
124 #[tracing::instrument(skip_all, name = "git_issues::repo_settings_delete")]
125 pub(super) async fn repo_settings_delete(
126 State(db): State<PgPool>,
127 State(config): State<Config>,
128 AuthUser(user): AuthUser,
129 Path((owner, repo_name)): Path<(String, String)>,
130 ) -> Result<impl IntoResponse> {
131 user.check_not_suspended()?;
132 let resolved = super::resolve_repo(&db, &config, &owner, &repo_name, Some(user.id)).await?;
133
134 if user.id != resolved.db_user.id {
135 return Err(AppError::Forbidden);
136 }
137
138 db::git_repos::delete_repo(&db, resolved.db_repo.id).await?;
139
140 Ok((
141 StatusCode::OK,
142 [("HX-Redirect", format!("/git/{owner}"))],
143 "",
144 ))
145 }
146