Skip to main content

max / makenotwork

Decompose AppState Phase 2: api ownership-helper foundation Narrow the three shared api ownership helpers (verify_project_ownership, verify_item_ownership, verify_blog_post_ownership) from &AppState to &PgPool, and update all 56 call sites across api/ to pass &state.db (handlers still hold State<AppState> for now). Migrate the two db-only api/mod.rs handlers (public_projects, email_signup) to State<PgPool>. This decouples the per-submodule handler migrations that follow: each can flip its own &state.db to &db independently. Crate stays green. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-14 00:17 UTC
Commit: 8a44e4079d9016edb9df0974284ae47dbd4c5031
Parent: cf7421b
18 files changed, +68 insertions, -66 deletions
@@ -120,7 +120,7 @@ pub(super) async fn get_blog_post(
120 120 AuthUser(user): AuthUser,
121 121 Path(blog_post_id): Path<BlogPostId>,
122 122 ) -> Result<impl IntoResponse> {
123 - let post = verify_blog_post_ownership(&state, blog_post_id, user.id).await?;
123 + let post = verify_blog_post_ownership(&state.db, blog_post_id, user.id).await?;
124 124 Ok(Json(blog_post_edit_response(&post)))
125 125 }
126 126
@@ -133,7 +133,7 @@ pub(super) async fn create_blog_post(
133 133 ValidatedJson(req): ValidatedJson<CreateBlogPostRequest>,
134 134 ) -> Result<impl IntoResponse> {
135 135 user.check_not_suspended()?;
136 - verify_project_ownership(&state, project_id, user.id).await?;
136 + verify_project_ownership(&state.db, project_id, user.id).await?;
137 137
138 138 // Validate title
139 139 validation::validate_blog_post_title(&req.title)?;
@@ -191,7 +191,7 @@ pub(super) async fn update_blog_post(
191 191 ValidatedJson(req): ValidatedJson<UpdateBlogPostRequest>,
192 192 ) -> Result<impl IntoResponse> {
193 193 user.check_not_suspended()?;
194 - let existing = verify_blog_post_ownership(&state, id, user.id).await?;
194 + let existing = verify_blog_post_ownership(&state.db, id, user.id).await?;
195 195
196 196 validation::validate_blog_post_title(&req.title)?;
197 197 // slug is validated by Slug's Deserialize impl
@@ -262,7 +262,7 @@ pub(super) async fn delete_blog_post(
262 262 Path(id): Path<BlogPostId>,
263 263 ) -> Result<impl IntoResponse> {
264 264 user.check_not_suspended()?;
265 - let post = verify_blog_post_ownership(&state, id, user.id).await?;
265 + let post = verify_blog_post_ownership(&state.db, id, user.id).await?;
266 266
267 267 db::blog_posts::delete_blog_post(&state.db, id, user.id).await?;
268 268 db::projects::bump_cache_generation(&state.db, post.project_id).await?;
@@ -353,7 +353,7 @@ pub(super) async fn list_placements(
353 353 AuthUser(user): AuthUser,
354 354 Path(item_id): Path<ItemId>,
355 355 ) -> Result<impl IntoResponse> {
356 - let (item, _project) = verify_item_ownership(&state, item_id, user.id).await?;
356 + let (item, _project) = verify_item_ownership(&state.db, item_id, user.id).await?;
357 357
358 358 let placements = db::content_insertions::list_placements_for_item(&state.db, item_id).await?;
359 359 // Only offer clips that can legally be placed on this item (a video clip on an
@@ -406,7 +406,7 @@ pub(super) async fn create_placement(
406 406 Json(req): Json<CreatePlacementRequest>,
407 407 ) -> Result<impl IntoResponse> {
408 408 user.check_not_suspended()?;
409 - let (item, _project) = verify_item_ownership(&state, item_id, user.id).await?;
409 + let (item, _project) = verify_item_ownership(&state.db, item_id, user.id).await?;
410 410
411 411 // Verify the insertion belongs to this user
412 412 let insertion = db::content_insertions::get_insertion(&state.db, req.insertion_id, user.id)
@@ -463,7 +463,7 @@ pub(super) async fn delete_placement(
463 463 .await?
464 464 .ok_or(AppError::NotFound)?;
465 465
466 - verify_item_ownership(&state, placement.item_id, user.id).await?;
466 + verify_item_ownership(&state.db, placement.item_id, user.id).await?;
467 467
468 468 db::content_insertions::delete_placement(&state.db, placement_id).await?;
469 469
@@ -43,7 +43,7 @@ pub(super) async fn start_import(
43 43 user.check_not_sandbox()?;
44 44
45 45 // Validate project ownership
46 - super::verify_project_ownership(&state, req.project_id, user.id).await?;
46 + super::verify_project_ownership(&state.db, req.project_id, user.id).await?;
47 47
48 48 // Only generic_csv is currently supported
49 49 if req.source != ImportSource::GenericCsv {
@@ -62,7 +62,7 @@ async fn verify_bulk_ownership(
62 62 }
63 63
64 64 // Verify the user owns that project
65 - verify_project_ownership(state, project_id, user_id).await?;
65 + verify_project_ownership(&state.db, project_id, user_id).await?;
66 66
67 67 Ok(project_id)
68 68 }
@@ -36,7 +36,7 @@ pub async fn bundle_add(
36 36 Json(req): Json<BundleAddRequest>,
37 37 ) -> Result<impl IntoResponse> {
38 38 user.check_not_suspended()?;
39 - let (item, _project) = verify_item_ownership(&state, bundle_id, user.id).await?;
39 + let (item, _project) = verify_item_ownership(&state.db, bundle_id, user.id).await?;
40 40 if item.item_type != ItemType::Bundle {
41 41 return Err(AppError::BadRequest("Item is not a bundle".to_string()));
42 42 }
@@ -62,7 +62,7 @@ pub async fn bundle_remove(
62 62 Path((bundle_id, child_id)): Path<(ItemId, ItemId)>,
63 63 ) -> Result<impl IntoResponse> {
64 64 user.check_not_suspended()?;
65 - let (item, _project) = verify_item_ownership(&state, bundle_id, user.id).await?;
65 + let (item, _project) = verify_item_ownership(&state.db, bundle_id, user.id).await?;
66 66 if item.item_type != ItemType::Bundle {
67 67 return Err(AppError::BadRequest("Item is not a bundle".to_string()));
68 68 }
@@ -79,7 +79,7 @@ pub async fn bundle_toggle_listed(
79 79 Json(req): Json<BundleListedRequest>,
80 80 ) -> Result<impl IntoResponse> {
81 81 user.check_not_suspended()?;
82 - let (item, _project) = verify_item_ownership(&state, bundle_id, user.id).await?;
82 + let (item, _project) = verify_item_ownership(&state.db, bundle_id, user.id).await?;
83 83 if item.item_type != ItemType::Bundle {
84 84 return Err(AppError::BadRequest("Item is not a bundle".to_string()));
85 85 }
@@ -112,7 +112,7 @@ pub async fn bundle_create_child(
112 112 Json(req): Json<BundleCreateChildRequest>,
113 113 ) -> Result<impl IntoResponse> {
114 114 user.check_not_suspended()?;
115 - let (bundle, _project) = verify_item_ownership(&state, bundle_id, user.id).await?;
115 + let (bundle, _project) = verify_item_ownership(&state.db, bundle_id, user.id).await?;
116 116 if bundle.item_type != ItemType::Bundle {
117 117 return Err(AppError::BadRequest("Item is not a bundle".to_string()));
118 118 }
@@ -57,7 +57,7 @@ pub(in crate::routes::api) async fn create_chapter(
57 57 ) -> Result<impl IntoResponse> {
58 58 user.check_not_suspended()?;
59 59 validation::validate_chapter_title(&req.title)?;
60 - let (item, _) = verify_item_ownership(&state, item_id, user.id).await?;
60 + let (item, _) = verify_item_ownership(&state.db, item_id, user.id).await?;
61 61
62 62 let chapter = db::chapters::create_chapter(
63 63 &state.db,
@@ -117,7 +117,7 @@ pub(in crate::routes::api) async fn update_chapter(
117 117 .await?
118 118 .ok_or(AppError::NotFound)?;
119 119
120 - let (item, _) = verify_item_ownership(&state, chapter.item_id, user.id).await?;
120 + let (item, _) = verify_item_ownership(&state.db, chapter.item_id, user.id).await?;
121 121
122 122 let updated = db::chapters::update_chapter(
123 123 &state.db,
@@ -153,7 +153,7 @@ pub(in crate::routes::api) async fn delete_chapter(
153 153 .await?
154 154 .ok_or(AppError::NotFound)?;
155 155
156 - let (item, _) = verify_item_ownership(&state, chapter.item_id, user.id).await?;
156 + let (item, _) = verify_item_ownership(&state.db, chapter.item_id, user.id).await?;
157 157
158 158 db::chapters::delete_chapter(&state.db, chapter_id).await?;
159 159 db::projects::bump_cache_generation(&state.db, item.project_id).await?;
@@ -71,7 +71,7 @@ pub(in crate::routes::api) async fn create_item(
71 71 validation::validate_item_description(desc)?;
72 72 }
73 73
74 - verify_project_ownership(&state, project_id, user.id).await?;
74 + verify_project_ownership(&state.db, project_id, user.id).await?;
75 75
76 76 // Validate item type against project features
77 77 let project = db::projects::get_project_by_id(&state.db, project_id)
@@ -172,7 +172,7 @@ pub(in crate::routes::api) async fn update_item(
172 172 ) -> Result<Response> {
173 173 tracing::Span::current().record("item_id", tracing::field::display(&id));
174 174 user.check_not_suspended()?;
175 - verify_item_ownership(&state, id, user.id).await?;
175 + verify_item_ownership(&state.db, id, user.id).await?;
176 176
177 177 // Validate input (same rules as create_item, but all fields are optional)
178 178 if let Some(ref title) = req.title {
@@ -284,7 +284,7 @@ pub(in crate::routes::api) async fn delete_item(
284 284 ) -> Result<Response> {
285 285 tracing::Span::current().record("item_id", tracing::field::display(&id));
286 286 user.check_not_suspended()?;
287 - let (item, _project) = verify_item_ownership(&state, id, user.id).await?;
287 + let (item, _project) = verify_item_ownership(&state.db, id, user.id).await?;
288 288
289 289 db::items::delete_item(&state.db, id, user.id).await?;
290 290 db::projects::bump_cache_generation(&state.db, item.project_id).await?;
@@ -302,7 +302,7 @@ pub(in crate::routes::api) async fn restore_item(
302 302 Path(id): Path<ItemId>,
303 303 ) -> Result<impl IntoResponse> {
304 304 user.check_not_suspended()?;
305 - verify_item_ownership(&state, id, user.id).await?;
305 + verify_item_ownership(&state.db, id, user.id).await?;
306 306
307 307 let restored = db::items::restore_item(&state.db, id, user.id).await?;
308 308 if !restored {
@@ -322,7 +322,7 @@ pub(in crate::routes::api) async fn duplicate_item(
322 322 ) -> Result<Response> {
323 323 tracing::Span::current().record("item_id", tracing::field::display(&id));
324 324 user.check_not_suspended()?;
325 - verify_item_ownership(&state, id, user.id).await?;
325 + verify_item_ownership(&state.db, id, user.id).await?;
326 326
327 327 let new_item = db::items::duplicate_item(&state.db, id, user.id).await?;
328 328
@@ -370,7 +370,7 @@ pub(in crate::routes::api) async fn move_item(
370 370 ) -> Result<impl IntoResponse> {
371 371 tracing::Span::current().record("item_id", tracing::field::display(&id));
372 372 user.check_not_suspended()?;
373 - let (item, _project) = verify_item_ownership(&state, id, user.id).await?;
373 + let (item, _project) = verify_item_ownership(&state.db, id, user.id).await?;
374 374
375 375 db::items::move_item(&state.db, item.project_id, user.id, id, &req.direction).await?;
376 376 db::projects::bump_cache_generation(&state.db, item.project_id).await?;
@@ -409,7 +409,7 @@ pub(in crate::routes::api) async fn update_item_text(
409 409 tracing::Span::current().record("item_id", tracing::field::display(&id));
410 410 user.check_not_suspended()?;
411 411 validation::validate_item_text_body(&req.body)?;
412 - verify_item_ownership(&state, id, user.id).await?;
412 + verify_item_ownership(&state.db, id, user.id).await?;
413 413
414 414 let item = db::items::update_item_text(&state.db, id, user.id, &req.body).await?;
415 415 db::projects::bump_cache_generation(&state.db, item.project_id).await?;
@@ -34,7 +34,7 @@ pub(in crate::routes::api) async fn refund_transaction(
34 34 Json(req): Json<RefundRequest>,
35 35 ) -> Result<impl IntoResponse> {
36 36 user.check_not_suspended()?;
37 - verify_item_ownership(&state, id, user.id).await?;
37 + verify_item_ownership(&state.db, id, user.id).await?;
38 38
39 39 // Fetch the transaction and validate it belongs to this item
40 40 let tx = db::transactions::get_transaction_by_id(&state.db, req.transaction_id)
@@ -82,7 +82,7 @@ pub(in crate::routes::api) async fn create_section(
82 82 validation::validate_section_title(&title)?;
83 83 validation::validate_section_body(&req.body)?;
84 84
85 - let (item, _) = verify_item_ownership(&state, item_id, user.id).await?;
85 + let (item, _) = verify_item_ownership(&state.db, item_id, user.id).await?;
86 86
87 87 // Enforce max sections limit
88 88 let count = db::item_sections::count_by_item(&state.db, item_id).await?;
@@ -147,7 +147,7 @@ pub(in crate::routes::api) async fn update_section(
147 147 .await?
148 148 .ok_or(AppError::NotFound)?;
149 149
150 - let (item, _) = verify_item_ownership(&state, section.item_id, user.id).await?;
150 + let (item, _) = verify_item_ownership(&state.db, section.item_id, user.id).await?;
151 151
152 152 // Same dedup as create. Re-saving the section under its own slug is not a
153 153 // conflict (same row), so the bare base succeeds; only a collision with a
@@ -179,7 +179,7 @@ pub(in crate::routes::api) async fn delete_section(
179 179 .await?
180 180 .ok_or(AppError::NotFound)?;
181 181
182 - let (item, _) = verify_item_ownership(&state, section.item_id, user.id).await?;
182 + let (item, _) = verify_item_ownership(&state.db, section.item_id, user.id).await?;
183 183
184 184 db::item_sections::delete(&state.db, section_id).await?;
185 185 db::projects::bump_cache_generation(&state.db, item.project_id).await?;
@@ -200,7 +200,7 @@ pub(in crate::routes::api) async fn reorder_sections(
200 200 Json(req): Json<ReorderSectionsRequest>,
201 201 ) -> Result<impl IntoResponse> {
202 202 user.check_not_suspended()?;
203 - let (item, _) = verify_item_ownership(&state, item_id, user.id).await?;
203 + let (item, _) = verify_item_ownership(&state.db, item_id, user.id).await?;
204 204
205 205 db::item_sections::reorder(&state.db, item_id, &req.section_ids).await?;
206 206 db::projects::bump_cache_generation(&state.db, item.project_id).await?;
@@ -39,7 +39,7 @@ pub(in crate::routes::api) async fn add_tag(
39 39 Form(form): Form<AddTagForm>,
40 40 ) -> Result<impl IntoResponse> {
41 41 user.check_not_suspended()?;
42 - let (item, _) = verify_item_ownership(&state, id, user.id).await?;
42 + let (item, _) = verify_item_ownership(&state.db, id, user.id).await?;
43 43
44 44 let tag = db::tags::get_tag_by_id(&state.db, form.tag_id)
45 45 .await?
@@ -79,7 +79,7 @@ pub(in crate::routes::api) async fn remove_tag(
79 79 Path((id, tag_id)): Path<(ItemId, TagId)>,
80 80 ) -> Result<impl IntoResponse> {
81 81 user.check_not_suspended()?;
82 - let (item, _) = verify_item_ownership(&state, id, user.id).await?;
82 + let (item, _) = verify_item_ownership(&state.db, id, user.id).await?;
83 83
84 84 db::tags::remove_tag_from_item(&state.db, id, tag_id).await?;
85 85 db::projects::bump_cache_generation(&state.db, item.project_id).await?;
@@ -101,7 +101,7 @@ pub(in crate::routes::api) async fn set_primary_tag(
101 101 Form(form): Form<SetPrimaryTagForm>,
102 102 ) -> Result<impl IntoResponse> {
103 103 user.check_not_suspended()?;
104 - let (item, _) = verify_item_ownership(&state, id, user.id).await?;
104 + let (item, _) = verify_item_ownership(&state.db, id, user.id).await?;
105 105
106 106 db::tags::set_primary_tag(&state.db, id, form.tag_id).await?;
107 107 db::projects::bump_cache_generation(&state.db, item.project_id).await?;
@@ -56,7 +56,7 @@ pub(in crate::routes::api) async fn create_version(
56 56 validation::validate_changelog(changelog)?;
57 57 }
58 58
59 - let (item, _) = verify_item_ownership(&state, item_id, user.id).await?;
59 + let (item, _) = verify_item_ownership(&state.db, item_id, user.id).await?;
60 60
61 61 let version = db::versions::create_version(
62 62 &state.db,
@@ -91,7 +91,7 @@ pub(in crate::routes::api) async fn delete_version(
91 91 Path((item_id, version_id)): Path<(ItemId, VersionId)>,
92 92 ) -> Result<impl IntoResponse> {
93 93 user.check_not_suspended()?;
94 - verify_item_ownership(&state, item_id, user.id).await?;
94 + verify_item_ownership(&state.db, item_id, user.id).await?;
95 95
96 96 let version = db::versions::get_version_by_id(&state.db, version_id)
97 97 .await?
@@ -355,7 +355,7 @@ pub(super) async fn update_license_settings(
355 355 ) -> Result<Response> {
356 356 user.check_not_suspended()?;
357 357
358 - verify_item_ownership(&state, id, user.id).await?;
358 + verify_item_ownership(&state.db, id, user.id).await?;
359 359
360 360 let enable = req.enable_license_keys.is_some();
361 361 db::items::update_item_license_settings(&state.db, id, user.id, enable, req.default_max_activations).await?;
@@ -412,7 +412,7 @@ pub(super) async fn generate_key(
412 412 ) -> Result<Response> {
413 413 user.check_not_suspended()?;
414 414
415 - let (item, _project) = verify_item_ownership(&state, item_id, user.id).await?;
415 + let (item, _project) = verify_item_ownership(&state.db, item_id, user.id).await?;
416 416
417 417 if !item.enable_license_keys {
418 418 return Err(AppError::BadRequest(
@@ -468,7 +468,7 @@ pub(super) async fn list_keys(
468 468 AuthUser(user): AuthUser,
469 469 Path(item_id): Path<ItemId>,
470 470 ) -> Result<Response> {
471 - verify_item_ownership(&state, item_id, user.id).await?;
471 + verify_item_ownership(&state.db, item_id, user.id).await?;
472 472
473 473 let keys = db::license_keys::get_license_keys_by_item(&state.db, item_id).await?;
474 474
@@ -503,7 +503,7 @@ pub(super) async fn revoke_key(
503 503 .await?
504 504 .ok_or(AppError::NotFound)?;
505 505
506 - verify_item_ownership(&state, key.item_id, user.id).await?;
506 + verify_item_ownership(&state.db, key.item_id, user.id).await?;
507 507
508 508 db::license_keys::revoke_license_key(&state.db, key_id).await?;
509 509
@@ -55,6 +55,8 @@ use serde::{Deserialize, Serialize};
55 55 use serde_json::json;
56 56 use tower_governor::GovernorLayer;
57 57
58 + use sqlx::PgPool;
59 +
58 60 use crate::{
59 61 constants,
60 62 csrf::{delete_csrf, post_csrf, post_csrf_skip, put_csrf, CsrfRouter},
@@ -69,11 +71,11 @@ const GUEST_CHECKOUT_SKIP: &str = "guest checkout: pre-auth, no session";
69 71 /// Fetch a project and verify the user owns it. Shared by all ownership checks
70 72 /// that go through a project (items, blog posts, direct project access).
71 73 pub(super) async fn verify_project_ownership(
72 - state: &AppState,
74 + db: &sqlx::PgPool,
73 75 project_id: ProjectId,
74 76 user_id: UserId,
75 77 ) -> Result<db::DbProject> {
76 - let project = db::projects::get_project_by_id(&state.db, project_id)
78 + let project = db::projects::get_project_by_id(db, project_id)
77 79 .await?
78 80 .ok_or(AppError::NotFound)?;
79 81 if project.user_id != user_id {
@@ -83,26 +85,26 @@ pub(super) async fn verify_project_ownership(
83 85 }
84 86
85 87 pub(super) async fn verify_item_ownership(
86 - state: &AppState,
88 + db: &sqlx::PgPool,
87 89 item_id: ItemId,
88 90 user_id: UserId,
89 91 ) -> Result<(db::DbItem, db::DbProject)> {
90 - let item = db::items::get_item_by_id(&state.db, item_id)
92 + let item = db::items::get_item_by_id(db, item_id)
91 93 .await?
92 94 .ok_or(AppError::NotFound)?;
93 - let project = verify_project_ownership(state, item.project_id, user_id).await?;
95 + let project = verify_project_ownership(db, item.project_id, user_id).await?;
94 96 Ok((item, project))
95 97 }
96 98
97 99 pub(super) async fn verify_blog_post_ownership(
98 - state: &AppState,
100 + db: &sqlx::PgPool,
99 101 blog_post_id: BlogPostId,
100 102 user_id: UserId,
101 103 ) -> Result<db::DbBlogPost> {
102 - let post = db::blog_posts::get_blog_post_by_id(&state.db, blog_post_id)
104 + let post = db::blog_posts::get_blog_post_by_id(db, blog_post_id)
103 105 .await?
104 106 .ok_or(AppError::NotFound)?;
105 - verify_project_ownership(state, post.project_id, user_id).await?;
107 + verify_project_ownership(db, post.project_id, user_id).await?;
106 108 Ok(post)
107 109 }
108 110
@@ -136,9 +138,9 @@ struct PublicProject {
136 138
137 139 #[tracing::instrument(skip_all, name = "api::public_projects")]
138 140 async fn public_projects(
139 - State(state): State<AppState>,
141 + State(db): State<PgPool>,
140 142 ) -> Result<impl IntoResponse> {
141 - let rows = db::discover::discover_projects(&state.db, None, None, None, false, 50, 0).await?;
143 + let rows = db::discover::discover_projects(&db, None, None, None, false, 50, 0).await?;
142 144 let data: Vec<PublicProject> = rows
143 145 .into_iter()
144 146 .map(|r| PublicProject {
@@ -162,11 +164,11 @@ struct EmailSignupForm {
162 164
163 165 #[tracing::instrument(skip_all, name = "api::email_signup")]
164 166 async fn email_signup(
165 - State(state): State<AppState>,
167 + State(db): State<PgPool>,
166 168 Json(form): Json<EmailSignupForm>,
167 169 ) -> Result<impl IntoResponse> {
168 170 let email = db::Email::new(&form.email)?;
169 - db::email_signups::insert_email_signup(&state.db, email.as_str(), "landing").await?;
171 + db::email_signups::insert_email_signup(&db, email.as_str(), "landing").await?;
170 172 Ok(Json(json!({"success": true})))
171 173 }
172 174
@@ -78,7 +78,7 @@ pub(super) async fn create_section(
78 78 validation::validate_section_title(&title)?;
79 79 validation::validate_section_body(&req.body)?;
80 80
81 - verify_project_ownership(&state, project_id, user.id).await?;
81 + verify_project_ownership(&state.db, project_id, user.id).await?;
82 82
83 83 let count = db::project_sections::count_by_project(&state.db, project_id).await?;
84 84 if count >= MAX_SECTIONS_PER_PROJECT {
@@ -139,7 +139,7 @@ pub(super) async fn update_section(
139 139 .await?
140 140 .ok_or(AppError::NotFound)?;
141 141
142 - verify_project_ownership(&state, section.project_id, user.id).await?;
142 + verify_project_ownership(&state.db, section.project_id, user.id).await?;
143 143
144 144 // Same dedup as create. Re-saving under the section's own slug is not a
145 145 // conflict (same row); only a collision with a different section triggers
@@ -170,7 +170,7 @@ pub(super) async fn delete_section(
170 170 .await?
171 171 .ok_or(AppError::NotFound)?;
172 172
173 - verify_project_ownership(&state, section.project_id, user.id).await?;
173 + verify_project_ownership(&state.db, section.project_id, user.id).await?;
174 174
175 175 db::project_sections::delete(&state.db, section_id).await?;
176 176 db::projects::bump_cache_generation(&state.db, section.project_id).await?;
@@ -190,7 +190,7 @@ pub(super) async fn reorder_sections(
190 190 Json(req): Json<ReorderSectionsRequest>,
191 191 ) -> Result<impl IntoResponse> {
192 192 user.check_not_suspended()?;
193 - verify_project_ownership(&state, project_id, user.id).await?;
193 + verify_project_ownership(&state.db, project_id, user.id).await?;
194 194
195 195 db::project_sections::reorder(&state.db, project_id, &req.section_ids).await?;
196 196 db::projects::bump_cache_generation(&state.db, project_id).await?;
@@ -219,7 +219,7 @@ pub(super) async fn update_project(
219 219 ) -> Result<impl IntoResponse> {
220 220 tracing::Span::current().record("project_id", tracing::field::display(&id));
221 221 user.check_not_suspended()?;
222 - verify_project_ownership(&state, id, user.id).await?;
222 + verify_project_ownership(&state.db, id, user.id).await?;
223 223
224 224 // Validate input (same rules as create_project, but all fields are optional)
225 225 if let Some(ref title) = req.title {
@@ -331,7 +331,7 @@ pub(super) async fn update_project_theme(
331 331 ) -> Result<impl IntoResponse> {
332 332 tracing::Span::current().record("project_id", tracing::field::display(&id));
333 333 user.check_not_suspended()?;
334 - verify_project_ownership(&state, id, user.id).await?;
334 + verify_project_ownership(&state.db, id, user.id).await?;
335 335
336 336 let theme_id = crate::theming::normalize_theme_id(req.theme_id.as_deref())
337 337 .map_err(|t| AppError::validation(format!("Unknown theme: {t}")))?;
@@ -353,7 +353,7 @@ pub(super) async fn delete_project(
353 353 ) -> Result<impl IntoResponse> {
354 354 tracing::Span::current().record("project_id", tracing::field::display(&id));
355 355 user.check_not_suspended()?;
356 - let project = verify_project_ownership(&state, id, user.id).await?;
356 + let project = verify_project_ownership(&state.db, id, user.id).await?;
357 357
358 358 // Collect all S3 keys from items + versions + galleries before CASCADE
359 359 // delete destroys them. Gallery rows (item_images / project_images) cascade
@@ -431,7 +431,7 @@ pub(super) async fn link_repo(
431 431 Json(req): Json<LinkRepoRequest>,
432 432 ) -> Result<impl IntoResponse> {
433 433 user.check_not_suspended()?;
434 - verify_project_ownership(&state, id, user.id).await?;
434 + verify_project_ownership(&state.db, id, user.id).await?;
435 435
436 436 let repo = db::git_repos::get_repo_by_user_and_name(&state.db, user.id, &req.name)
437 437 .await?
@@ -451,7 +451,7 @@ pub(super) async fn unlink_repo(
451 451 Path((id, repo_name)): Path<(ProjectId, String)>,
452 452 ) -> Result<impl IntoResponse> {
453 453 user.check_not_suspended()?;
454 - verify_project_ownership(&state, id, user.id).await?;
454 + verify_project_ownership(&state.db, id, user.id).await?;
455 455
456 456 let repo = db::git_repos::get_repo_by_user_and_name(&state.db, user.id, &repo_name)
457 457 .await?
@@ -616,7 +616,7 @@ pub async fn add_project_member(
616 616 Path(project_id): Path<ProjectId>,
617 617 Form(form): Form<AddMemberForm>,
618 618 ) -> Result<Response> {
619 - let _project = verify_project_ownership(&state, project_id, session_user.id).await?;
619 + let _project = verify_project_ownership(&state.db, project_id, session_user.id).await?;
620 620
621 621 // Validate split percent
622 622 if form.split_percent < 1 || form.split_percent > 99 {
@@ -661,7 +661,7 @@ pub async fn remove_project_member(
661 661 AuthUser(session_user): AuthUser,
662 662 Path((project_id, user_id)): Path<(ProjectId, db::UserId)>,
663 663 ) -> Result<Response> {
664 - verify_project_ownership(&state, project_id, session_user.id).await?;
664 + verify_project_ownership(&state.db, project_id, session_user.id).await?;
665 665
666 666 let removed = db::project_members::remove_project_member(&state.db, project_id, user_id).await?;
667 667
@@ -159,7 +159,7 @@ pub(super) async fn create_promo_code(
159 159 } else {
160 160 let item_id: ItemId = id_str.parse()
161 161 .map_err(|_| AppError::BadRequest("Invalid item ID".to_string()))?;
162 - verify_item_ownership(&state, item_id, user.id).await?;
162 + verify_item_ownership(&state.db, item_id, user.id).await?;
163 163 Some(item_id)
164 164 }
165 165 } else {
@@ -59,7 +59,7 @@ pub(super) async fn create_tier(
59 59 ) -> Result<impl IntoResponse> {
60 60 user.check_not_suspended()?;
61 61 // Verify ownership
62 - let project = verify_project_ownership(&state, project_id, user.id).await?;
62 + let project = verify_project_ownership(&state.db, project_id, user.id).await?;
63 63
64 64 // Validate input
65 65 validation::validate_tier_name(&req.name)?;
@@ -131,7 +131,7 @@ pub(super) async fn list_tiers(
131 131 Path(project_id): Path<ProjectId>,
132 132 ) -> Result<impl IntoResponse> {
133 133 // Verify ownership (only creator can see all tiers including inactive)
134 - verify_project_ownership(&state, project_id, user.id).await?;
134 + verify_project_ownership(&state.db, project_id, user.id).await?;
135 135
136 136 let tiers = db::subscriptions::get_all_tiers_by_project(&state.db, project_id).await?;
137 137
@@ -161,7 +161,7 @@ pub(super) async fn update_tier(
161 161 .ok_or(AppError::NotFound)?;
162 162
163 163 let tier_project_id = tier.project_id.ok_or(AppError::NotFound)?;
164 - verify_project_ownership(&state, tier_project_id, user.id).await?;
164 + verify_project_ownership(&state.db, tier_project_id, user.id).await?;
165 165
166 166 // Validate input
167 167 validation::validate_tier_name(&req.name)?;
@@ -203,7 +203,7 @@ pub(super) async fn delete_tier(
203 203 .ok_or(AppError::NotFound)?;
204 204
205 205 let tier_project_id = tier.project_id.ok_or(AppError::NotFound)?;
206 - verify_project_ownership(&state, tier_project_id, user.id).await?;
206 + verify_project_ownership(&state.db, tier_project_id, user.id).await?;
207 207
208 208 db::subscriptions::delete_subscription_tier(&state.db, tier_id).await?;
209 209
@@ -62,7 +62,7 @@ pub(super) async fn suggest_tags(
62 62 AuthUser(user): AuthUser,
63 63 Path(item_id): Path<ItemId>,
64 64 ) -> Result<Response> {
65 - let (item, _project) = super::verify_item_ownership(&state, item_id, user.id).await?;
65 + let (item, _project) = super::verify_item_ownership(&state.db, item_id, user.id).await?;
66 66
67 67 let tags = db::tags::suggest_tags_for_item(
68 68 &state.db,