max / makenotwork
13 files changed,
+477 insertions,
-1 deletion
| @@ -179,6 +179,8 @@ | |||
| 179 | 179 | pub created_at: DateTime<Utc>, | |
| 180 | 180 | /// When the project was last modified. | |
| 181 | 181 | pub updated_at: DateTime<Utc>, | |
| 182 | + | /// Optional linked git repository name (bare name, not full path). | |
| 183 | + | pub git_repo_name: Option<String>, | |
| 182 | 184 | } | |
| 183 | 185 | ||
| 184 | 186 | /// A purchasable or free item within a project. |
| @@ -164,6 +164,38 @@ | |||
| 164 | 164 | Ok(projects) | |
| 165 | 165 | } | |
| 166 | 166 | ||
| 167 | + | /// Set or clear a project's linked git repository name. | |
| 168 | + | pub async fn set_project_git_repo( | |
| 169 | + | pool: &PgPool, | |
| 170 | + | id: ProjectId, | |
| 171 | + | repo_name: Option<&str>, | |
| 172 | + | ) -> Result<()> { | |
| 173 | + | sqlx::query("UPDATE projects SET git_repo_name = $2 WHERE id = $1") | |
| 174 | + | .bind(id) | |
| 175 | + | .bind(repo_name) | |
| 176 | + | .execute(pool) | |
| 177 | + | .await?; | |
| 178 | + | ||
| 179 | + | Ok(()) | |
| 180 | + | } | |
| 181 | + | ||
| 182 | + | /// Look up a project by its linked git repo name and owning user. | |
| 183 | + | pub async fn get_project_by_git_repo( | |
| 184 | + | pool: &PgPool, | |
| 185 | + | user_id: UserId, | |
| 186 | + | repo_name: &str, | |
| 187 | + | ) -> Result<Option<DbProject>> { | |
| 188 | + | let project = sqlx::query_as::<_, DbProject>( | |
| 189 | + | "SELECT * FROM projects WHERE user_id = $1 AND git_repo_name = $2 LIMIT 1", | |
| 190 | + | ) | |
| 191 | + | .bind(user_id) | |
| 192 | + | .bind(repo_name) | |
| 193 | + | .fetch_optional(pool) | |
| 194 | + | .await?; | |
| 195 | + | ||
| 196 | + | Ok(project) | |
| 197 | + | } | |
| 198 | + | ||
| 167 | 199 | /// Fetch a public project by its URL slug. Returns `None` if not found or not public. | |
| 168 | 200 | pub async fn get_public_project_by_slug( | |
| 169 | 201 | pool: &PgPool, |
| @@ -14,10 +14,12 @@ | |||
| 14 | 14 | use crate::{ | |
| 15 | 15 | auth::MaybeUser, | |
| 16 | 16 | constants, | |
| 17 | + | db::{self, Username}, | |
| 17 | 18 | error::{AppError, Result}, | |
| 18 | 19 | git, | |
| 19 | 20 | helpers::get_csrf_token, | |
| 20 | 21 | templates::*, | |
| 22 | + | types::*, | |
| 21 | 23 | AppState, | |
| 22 | 24 | }; | |
| 23 | 25 | ||
| @@ -105,6 +107,51 @@ | |||
| 105 | 107 | } | |
| 106 | 108 | } | |
| 107 | 109 | ||
| 110 | + | // ============================================================================ | |
| 111 | + | // Linked project + releases | |
| 112 | + | // ============================================================================ | |
| 113 | + | ||
| 114 | + | /// Look up the project linked to a git repo and fetch its public items with versions. | |
| 115 | + | async fn fetch_linked_releases( | |
| 116 | + | state: &AppState, | |
| 117 | + | owner: &str, | |
| 118 | + | repo_name: &str, | |
| 119 | + | ) -> (Option<Project>, Vec<ReleaseItem>) { | |
| 120 | + | let username = Username::from_trusted(owner.to_string()); | |
| 121 | + | let db_user = match db::users::get_user_by_username(&state.db, &username).await { | |
| 122 | + | Ok(Some(u)) => u, | |
| 123 | + | _ => return (None, Vec::new()), | |
| 124 | + | }; | |
| 125 | + | ||
| 126 | + | let db_project = match db::projects::get_project_by_git_repo(&state.db, db_user.id, repo_name).await { | |
| 127 | + | Ok(Some(p)) if p.is_public => p, | |
| 128 | + | _ => return (None, Vec::new()), | |
| 129 | + | }; | |
| 130 | + | ||
| 131 | + | let db_items = match db::items::get_public_items_by_project(&state.db, db_project.id).await { | |
| 132 | + | Ok(items) => items, | |
| 133 | + | Err(_) => return (Some(Project::from_db(&db_project, 0)), Vec::new()), | |
| 134 | + | }; | |
| 135 | + | ||
| 136 | + | let mut release_items = Vec::new(); | |
| 137 | + | for item in &db_items { | |
| 138 | + | let versions = match db::versions::get_versions_by_item(&state.db, item.id).await { | |
| 139 | + | Ok(v) if !v.is_empty() => v, | |
| 140 | + | _ => continue, | |
| 141 | + | }; | |
| 142 | + | let tags = db::tags::get_tags_for_item(&state.db, item.id).await.unwrap_or_default(); | |
| 143 | + | let view_item = Item::from_db_list(item, &tags, item.price_cents == 0, false); | |
| 144 | + | let view_versions: Vec<Version> = versions.iter().map(Version::from_db).collect(); | |
| 145 | + | release_items.push(ReleaseItem { | |
| 146 | + | item: view_item, | |
| 147 | + | versions: view_versions, | |
| 148 | + | }); | |
| 149 | + | } | |
| 150 | + | ||
| 151 | + | let project = Project::from_db(&db_project, db_items.len() as u32); | |
| 152 | + | (Some(project), release_items) | |
| 153 | + | } | |
| 154 | + | ||
| 108 | 155 | // ============================================================================ | |
| 109 | 156 | // Browsing handlers | |
| 110 | 157 | // ============================================================================ | |
| @@ -128,6 +175,9 @@ | |||
| 128 | 175 | ||
| 129 | 176 | let csrf_token = get_csrf_token(&session).await; | |
| 130 | 177 | ||
| 178 | + | // Look up linked project + releases | |
| 179 | + | let (linked_project, release_items) = fetch_linked_releases(&state, &owner, &repo_name).await; | |
| 180 | + | ||
| 131 | 181 | Ok(GitRepoTemplate { | |
| 132 | 182 | csrf_token, | |
| 133 | 183 | session_user: maybe_user, | |
| @@ -139,6 +189,8 @@ | |||
| 139 | 189 | tree_items, | |
| 140 | 190 | readme_html, | |
| 141 | 191 | host_url: state.config.host_url.clone(), | |
| 192 | + | linked_project, | |
| 193 | + | release_items, | |
| 142 | 194 | }) | |
| 143 | 195 | } | |
| 144 | 196 | ||
| @@ -159,6 +211,9 @@ | |||
| 159 | 211 | ||
| 160 | 212 | let csrf_token = get_csrf_token(&session).await; | |
| 161 | 213 | ||
| 214 | + | // Look up linked project + releases | |
| 215 | + | let (linked_project, release_items) = fetch_linked_releases(&state, &owner, &repo_name).await; | |
| 216 | + | ||
| 162 | 217 | Ok(GitRepoTemplate { | |
| 163 | 218 | csrf_token, | |
| 164 | 219 | session_user: maybe_user, | |
| @@ -170,6 +225,8 @@ | |||
| 170 | 225 | tree_items, | |
| 171 | 226 | readme_html, | |
| 172 | 227 | host_url: state.config.host_url.clone(), | |
| 228 | + | linked_project, | |
| 229 | + | release_items, | |
| 173 | 230 | }) | |
| 174 | 231 | } | |
| 175 | 232 |
| @@ -233,6 +233,10 @@ | |||
| 233 | 233 | pub project: Project, | |
| 234 | 234 | /// Current category name for pre-populating the form, or empty. | |
| 235 | 235 | pub category_name: String, | |
| 236 | + | /// Current linked git repo name, or empty. | |
| 237 | + | pub git_repo_name: String, | |
| 238 | + | /// Whether git hosting is configured on this server. | |
| 239 | + | pub git_enabled: bool, | |
| 236 | 240 | } | |
| 237 | 241 | ||
| 238 | 242 | /// Dashboard blog tab partial. |
| @@ -140,6 +140,8 @@ | |||
| 140 | 140 | pub has_subscription: bool, | |
| 141 | 141 | /// Base URL for OG meta tags. | |
| 142 | 142 | pub host_url: String, | |
| 143 | + | /// URL to linked git repository, if configured. | |
| 144 | + | pub git_repo_url: Option<String>, | |
| 143 | 145 | } | |
| 144 | 146 | ||
| 145 | 147 | /// Public item detail page. | |
| @@ -402,6 +404,16 @@ | |||
| 402 | 404 | pub link_text: String, | |
| 403 | 405 | } | |
| 404 | 406 | ||
| 407 | + | /// Confirmation page shown before account deletion (GET step). | |
| 408 | + | #[derive(Template)] | |
| 409 | + | #[template(path = "pages/confirm_delete.html")] | |
| 410 | + | pub struct ConfirmDeleteTemplate { | |
| 411 | + | pub csrf_token: CsrfTokenOption, | |
| 412 | + | pub user: String, | |
| 413 | + | pub expires: String, | |
| 414 | + | pub sig: String, | |
| 415 | + | } | |
| 416 | + | ||
| 405 | 417 | #[derive(Template)] | |
| 406 | 418 | #[template(path = "pages/account-deleted.html")] | |
| 407 | 419 | pub struct AccountDeletedTemplate { | |
| @@ -504,6 +516,12 @@ | |||
| 504 | 516 | // Git Source Browser | |
| 505 | 517 | // ============================================================================ | |
| 506 | 518 | ||
| 519 | + | /// An item paired with its versions, for release display on the git repo page. | |
| 520 | + | pub struct ReleaseItem { | |
| 521 | + | pub item: Item, | |
| 522 | + | pub versions: Vec<Version>, | |
| 523 | + | } | |
| 524 | + | ||
| 507 | 525 | /// Repository overview: file tree at HEAD + README. | |
| 508 | 526 | #[derive(Template)] | |
| 509 | 527 | #[template(path = "pages/git/repo.html")] | |
| @@ -518,6 +536,10 @@ | |||
| 518 | 536 | pub tree_items: Vec<git::TreeItem>, | |
| 519 | 537 | pub readme_html: Option<String>, | |
| 520 | 538 | pub host_url: String, | |
| 539 | + | /// Linked project, if this repo is associated with a public project. | |
| 540 | + | pub linked_project: Option<Project>, | |
| 541 | + | /// Public items with versions from the linked project (releases). | |
| 542 | + | pub release_items: Vec<ReleaseItem>, | |
| 521 | 543 | } | |
| 522 | 544 | ||
| 523 | 545 | /// Subdirectory listing with breadcrumb navigation. |