max / makenotwork
7 files changed,
+185 insertions,
-173 deletions
| @@ -6,12 +6,13 @@ use axum::{ | |||
| 6 | 6 | Json, | |
| 7 | 7 | }; | |
| 8 | 8 | ||
| 9 | + | use sqlx::PgPool; | |
| 10 | + | ||
| 9 | 11 | use crate::{ | |
| 10 | 12 | auth::AuthUser, | |
| 11 | 13 | db::{self, SyncAppId}, | |
| 12 | 14 | error::{AppError, Result}, | |
| 13 | 15 | validation, | |
| 14 | - | AppState, | |
| 15 | 16 | }; | |
| 16 | 17 | ||
| 17 | 18 | use super::UpdateAppSlugRequest; | |
| @@ -24,19 +25,19 @@ use super::{CreateAppRequest, UpdateAppLinkRequest}; | |||
| 24 | 25 | /// Returns the app data plus the plaintext API key (shown only once). | |
| 25 | 26 | #[tracing::instrument(skip_all, name = "synckit::create_app")] | |
| 26 | 27 | pub(super) async fn create_app( | |
| 27 | - | State(state): State<AppState>, | |
| 28 | + | State(db): State<PgPool>, | |
| 28 | 29 | AuthUser(user): AuthUser, | |
| 29 | 30 | Json(req): Json<CreateAppRequest>, | |
| 30 | 31 | ) -> Result<impl IntoResponse> { | |
| 31 | 32 | user.check_not_sandbox()?; | |
| 32 | 33 | validation::validate_sync_app_name(&req.name)?; | |
| 33 | 34 | ||
| 34 | - | let project_id = parse_and_verify_project(&state, user.id, req.project_id.as_deref()).await?; | |
| 35 | - | let item_id = parse_and_verify_item(&state, user.id, req.item_id.as_deref()).await?; | |
| 35 | + | let project_id = parse_and_verify_project(&db, user.id, req.project_id.as_deref()).await?; | |
| 36 | + | let item_id = parse_and_verify_item(&db, user.id, req.item_id.as_deref()).await?; | |
| 36 | 37 | ||
| 37 | 38 | let api_key = super::generate_api_key(); | |
| 38 | 39 | let app = db::synckit::create_sync_app( | |
| 39 | - | &state.db, user.id, &req.name, &api_key, project_id, item_id, | |
| 40 | + | &db, user.id, &req.name, &api_key, project_id, item_id, | |
| 40 | 41 | ).await?; | |
| 41 | 42 | ||
| 42 | 43 | Ok((axum::http::StatusCode::CREATED, Json(super::AppWithKey { app, api_key }))) | |
| @@ -47,10 +48,10 @@ pub(super) async fn create_app( | |||
| 47 | 48 | /// `GET /api/sync/apps`: Session auth required. | |
| 48 | 49 | #[tracing::instrument(skip_all, name = "synckit::list_apps")] | |
| 49 | 50 | pub(super) async fn list_apps( | |
| 50 | - | State(state): State<AppState>, | |
| 51 | + | State(db): State<PgPool>, | |
| 51 | 52 | AuthUser(user): AuthUser, | |
| 52 | 53 | ) -> Result<impl IntoResponse> { | |
| 53 | - | let apps = db::synckit::get_sync_apps_by_creator(&state.db, user.id).await?; | |
| 54 | + | let apps = db::synckit::get_sync_apps_by_creator(&db, user.id).await?; | |
| 54 | 55 | ||
| 55 | 56 | Ok(Json(apps)) | |
| 56 | 57 | } | |
| @@ -60,11 +61,11 @@ pub(super) async fn list_apps( | |||
| 60 | 61 | /// `POST /api/sync/apps/{id}/regenerate-key`: Session auth required. | |
| 61 | 62 | #[tracing::instrument(skip_all, name = "synckit::regenerate_app_key")] | |
| 62 | 63 | pub(super) async fn regenerate_app_key( | |
| 63 | - | State(state): State<AppState>, | |
| 64 | + | State(db): State<PgPool>, | |
| 64 | 65 | AuthUser(user): AuthUser, | |
| 65 | 66 | Path(app_id): Path<SyncAppId>, | |
| 66 | 67 | ) -> Result<impl IntoResponse> { | |
| 67 | - | let app = db::synckit::get_sync_app_by_id(&state.db, app_id) | |
| 68 | + | let app = db::synckit::get_sync_app_by_id(&db, app_id) | |
| 68 | 69 | .await? | |
| 69 | 70 | .ok_or(AppError::NotFound)?; | |
| 70 | 71 | ||
| @@ -73,7 +74,7 @@ pub(super) async fn regenerate_app_key( | |||
| 73 | 74 | } | |
| 74 | 75 | ||
| 75 | 76 | let new_key = super::generate_api_key(); | |
| 76 | - | let updated = db::synckit::regenerate_sync_app_key(&state.db, app_id, &new_key).await?; | |
| 77 | + | let updated = db::synckit::regenerate_sync_app_key(&db, app_id, &new_key).await?; | |
| 77 | 78 | ||
| 78 | 79 | Ok(Json(super::AppWithKey { app: updated, api_key: new_key })) | |
| 79 | 80 | } | |
| @@ -83,11 +84,11 @@ pub(super) async fn regenerate_app_key( | |||
| 83 | 84 | /// `DELETE /api/sync/apps/{id}`: Session auth required. | |
| 84 | 85 | #[tracing::instrument(skip_all, name = "synckit::delete_app")] | |
| 85 | 86 | pub(super) async fn delete_app( | |
| 86 | - | State(state): State<AppState>, | |
| 87 | + | State(db): State<PgPool>, | |
| 87 | 88 | AuthUser(user): AuthUser, | |
| 88 | 89 | Path(app_id): Path<SyncAppId>, | |
| 89 | 90 | ) -> Result<impl IntoResponse> { | |
| 90 | - | let app = db::synckit::get_sync_app_by_id(&state.db, app_id) | |
| 91 | + | let app = db::synckit::get_sync_app_by_id(&db, app_id) | |
| 91 | 92 | .await? | |
| 92 | 93 | .ok_or(AppError::NotFound)?; | |
| 93 | 94 | ||
| @@ -95,7 +96,7 @@ pub(super) async fn delete_app( | |||
| 95 | 96 | return Err(AppError::Forbidden); | |
| 96 | 97 | } | |
| 97 | 98 | ||
| 98 | - | db::synckit::delete_sync_app(&state.db, app_id).await?; | |
| 99 | + | db::synckit::delete_sync_app(&db, app_id).await?; | |
| 99 | 100 | ||
| 100 | 101 | Ok(axum::http::StatusCode::NO_CONTENT) | |
| 101 | 102 | } | |
| @@ -105,12 +106,12 @@ pub(super) async fn delete_app( | |||
| 105 | 106 | /// `PUT /api/sync/apps/{id}/link`: Session auth required. | |
| 106 | 107 | #[tracing::instrument(skip_all, name = "synckit::update_app_link")] | |
| 107 | 108 | pub(super) async fn update_app_link( | |
| 108 | - | State(state): State<AppState>, | |
| 109 | + | State(db): State<PgPool>, | |
| 109 | 110 | AuthUser(user): AuthUser, | |
| 110 | 111 | Path(app_id): Path<SyncAppId>, | |
| 111 | 112 | Json(req): Json<UpdateAppLinkRequest>, | |
| 112 | 113 | ) -> Result<impl IntoResponse> { | |
| 113 | - | let app = db::synckit::get_sync_app_by_id(&state.db, app_id) | |
| 114 | + | let app = db::synckit::get_sync_app_by_id(&db, app_id) | |
| 114 | 115 | .await? | |
| 115 | 116 | .ok_or(AppError::NotFound)?; | |
| 116 | 117 | ||
| @@ -118,11 +119,11 @@ pub(super) async fn update_app_link( | |||
| 118 | 119 | return Err(AppError::Forbidden); | |
| 119 | 120 | } | |
| 120 | 121 | ||
| 121 | - | let project_id = parse_and_verify_project(&state, user.id, req.project_id.as_deref()).await?; | |
| 122 | - | let item_id = parse_and_verify_item(&state, user.id, req.item_id.as_deref()).await?; | |
| 122 | + | let project_id = parse_and_verify_project(&db, user.id, req.project_id.as_deref()).await?; | |
| 123 | + | let item_id = parse_and_verify_item(&db, user.id, req.item_id.as_deref()).await?; | |
| 123 | 124 | ||
| 124 | 125 | let updated = | |
| 125 | - | db::synckit::update_sync_app_link(&state.db, app_id, project_id, item_id).await?; | |
| 126 | + | db::synckit::update_sync_app_link(&db, app_id, project_id, item_id).await?; | |
| 126 | 127 | ||
| 127 | 128 | Ok(Json(updated)) | |
| 128 | 129 | } | |
| @@ -132,12 +133,12 @@ pub(super) async fn update_app_link( | |||
| 132 | 133 | /// `PUT /api/sync/apps/{id}/slug`: Session auth required. | |
| 133 | 134 | #[tracing::instrument(skip_all, name = "synckit::update_app_slug")] | |
| 134 | 135 | pub(super) async fn update_app_slug( | |
| 135 | - | State(state): State<AppState>, | |
| 136 | + | State(db): State<PgPool>, | |
| 136 | 137 | AuthUser(user): AuthUser, | |
| 137 | 138 | Path(app_id): Path<SyncAppId>, | |
| 138 | 139 | Json(req): Json<UpdateAppSlugRequest>, | |
| 139 | 140 | ) -> Result<impl IntoResponse> { | |
| 140 | - | let app = db::synckit::get_sync_app_by_id(&state.db, app_id) | |
| 141 | + | let app = db::synckit::get_sync_app_by_id(&db, app_id) | |
| 141 | 142 | .await? | |
| 142 | 143 | .ok_or(AppError::NotFound)?; | |
| 143 | 144 | ||
| @@ -148,7 +149,7 @@ pub(super) async fn update_app_slug( | |||
| 148 | 149 | // Reuse the OTA slug validation | |
| 149 | 150 | crate::routes::ota::validate_slug_public(&req.slug)?; | |
| 150 | 151 | ||
| 151 | - | db::ota::set_app_slug(&state.db, app_id, &req.slug).await?; | |
| 152 | + | db::ota::set_app_slug(&db, app_id, &req.slug).await?; | |
| 152 | 153 | ||
| 153 | 154 | Ok(axum::http::StatusCode::NO_CONTENT) | |
| 154 | 155 | } | |
| @@ -157,7 +158,7 @@ pub(super) async fn update_app_slug( | |||
| 157 | 158 | ||
| 158 | 159 | /// Parse an optional UUID string and verify the project belongs to the user. | |
| 159 | 160 | async fn parse_and_verify_project( | |
| 160 | - | state: &AppState, | |
| 161 | + | db: &PgPool, | |
| 161 | 162 | user_id: db::UserId, | |
| 162 | 163 | raw: Option<&str>, | |
| 163 | 164 | ) -> Result<Option<db::ProjectId>> { | |
| @@ -167,7 +168,7 @@ async fn parse_and_verify_project( | |||
| 167 | 168 | let pid: db::ProjectId = s | |
| 168 | 169 | .parse() | |
| 169 | 170 | .map_err(|_| AppError::BadRequest("Invalid project_id".to_string()))?; | |
| 170 | - | let project = db::projects::get_project_by_id(&state.db, pid) | |
| 171 | + | let project = db::projects::get_project_by_id(db, pid) | |
| 171 | 172 | .await? | |
| 172 | 173 | .ok_or(AppError::BadRequest("Project not found".to_string()))?; | |
| 173 | 174 | if project.user_id != user_id { | |
| @@ -179,7 +180,7 @@ async fn parse_and_verify_project( | |||
| 179 | 180 | /// Parse an optional UUID string and verify the item belongs to the user | |
| 180 | 181 | /// (via its parent project). | |
| 181 | 182 | async fn parse_and_verify_item( | |
| 182 | - | state: &AppState, | |
| 183 | + | db: &PgPool, | |
| 183 | 184 | user_id: db::UserId, | |
| 184 | 185 | raw: Option<&str>, | |
| 185 | 186 | ) -> Result<Option<db::ItemId>> { | |
| @@ -189,10 +190,10 @@ async fn parse_and_verify_item( | |||
| 189 | 190 | let iid: db::ItemId = s | |
| 190 | 191 | .parse() | |
| 191 | 192 | .map_err(|_| AppError::BadRequest("Invalid item_id".to_string()))?; | |
| 192 | - | let item = db::items::get_item_by_id(&state.db, iid) | |
| 193 | + | let item = db::items::get_item_by_id(db, iid) | |
| 193 | 194 | .await? | |
| 194 | 195 | .ok_or(AppError::BadRequest("Item not found".to_string()))?; | |
| 195 | - | let project = db::projects::get_project_by_id(&state.db, item.project_id) | |
| 196 | + | let project = db::projects::get_project_by_id(db, item.project_id) | |
| 196 | 197 | .await? | |
| 197 | 198 | .ok_or(AppError::BadRequest("Item's project not found".to_string()))?; | |
| 198 | 199 | if project.user_id != user_id { |
| @@ -6,13 +6,15 @@ use axum::{ | |||
| 6 | 6 | Json, | |
| 7 | 7 | }; | |
| 8 | 8 | ||
| 9 | + | use sqlx::PgPool; | |
| 10 | + | ||
| 9 | 11 | use crate::{ | |
| 10 | 12 | auth::verify_password_async, | |
| 13 | + | config::Config, | |
| 11 | 14 | db, | |
| 12 | 15 | error::{AppError, Result}, | |
| 13 | 16 | synckit_auth, | |
| 14 | 17 | validation, | |
| 15 | - | AppState, | |
| 16 | 18 | }; | |
| 17 | 19 | ||
| 18 | 20 | /// Pre-computed dummy Argon2 hash used to equalize timing when a user is not found, | |
| @@ -40,12 +42,12 @@ use super::{SyncAuthRequest, SyncAuthResponse, ValidateAppQuery, ValidateAppResp | |||
| 40 | 42 | )] | |
| 41 | 43 | #[tracing::instrument(skip_all, name = "synckit::sync_auth")] | |
| 42 | 44 | pub(super) async fn sync_auth( | |
| 43 | - | State(state): State<AppState>, | |
| 45 | + | State(db): State<PgPool>, | |
| 46 | + | State(config): State<Config>, | |
| 44 | 47 | headers: axum::http::HeaderMap, | |
| 45 | 48 | Json(req): Json<SyncAuthRequest>, | |
| 46 | 49 | ) -> Result<impl IntoResponse> { | |
| 47 | - | let secret = state | |
| 48 | - | .config | |
| 50 | + | let secret = config | |
| 49 | 51 | .synckit_jwt_secret | |
| 50 | 52 | .as_deref() | |
| 51 | 53 | .ok_or_else(|| AppError::ServiceUnavailable("SyncKit is not configured".to_string()))?; | |
| @@ -53,7 +55,7 @@ pub(super) async fn sync_auth( | |||
| 53 | 55 | validation::validate_synckit_key(&req.key)?; | |
| 54 | 56 | ||
| 55 | 57 | // Verify app exists and is active | |
| 56 | - | let app = db::synckit::get_sync_app_by_api_key(&state.db, &req.api_key) | |
| 58 | + | let app = db::synckit::get_sync_app_by_api_key(&db, &req.api_key) | |
| 57 | 59 | .await? | |
| 58 | 60 | .ok_or(AppError::Unauthorized)?; | |
| 59 | 61 | ||
| @@ -75,7 +77,7 @@ pub(super) async fn sync_auth( | |||
| 75 | 77 | return Err(AppError::Unauthorized); | |
| 76 | 78 | } | |
| 77 | 79 | }; | |
| 78 | - | let user = match db::users::get_user_by_email(&state.db, &email).await? { | |
| 80 | + | let user = match db::users::get_user_by_email(&db, &email).await? { | |
| 79 | 81 | Some(u) => u, | |
| 80 | 82 | None => { | |
| 81 | 83 | // Equalize timing to prevent email enumeration | |
| @@ -94,7 +96,7 @@ pub(super) async fn sync_auth( | |||
| 94 | 96 | // also avoids leaking 2FA status. (ultra-fuzz Run 3 SECURITY #4.) The folding | |
| 95 | 97 | // and counter ordering live in the shared relying-party gate so this flow and | |
| 96 | 98 | // OAuth can't drift apart (Run 11 Sec M1). | |
| 97 | - | match crate::auth::relying_party_login_gate(&state.db, &user, &req.password).await? { | |
| 99 | + | match crate::auth::relying_party_login_gate(&db, &user, &req.password).await? { | |
| 98 | 100 | crate::auth::LoginGate::Deny { .. } => { | |
| 99 | 101 | // Audit the failed sync auth (best-effort). Recorded only for a real | |
| 100 | 102 | // account; the email-enumeration equalization paths above (unknown | |
| @@ -102,7 +104,7 @@ pub(super) async fn sync_auth( | |||
| 102 | 104 | // and because they carry no user_id. | |
| 103 | 105 | let ip = crate::helpers::extract_client_ip(&headers); | |
| 104 | 106 | if let Err(e) = db::synckit::record_security_event( | |
| 105 | - | &state.db, | |
| 107 | + | &db, | |
| 106 | 108 | app.id, | |
| 107 | 109 | Some(user.id), | |
| 108 | 110 | db::synckit::sync_security_event::AUTH_FAILURE, | |
| @@ -127,12 +129,12 @@ pub(super) async fn sync_auth( | |||
| 127 | 129 | // internal and `bulk`/`app_wide` apps are uncapped and skip the claim. | |
| 128 | 130 | // `claim_key` is idempotent for an already-claimed key and enforces the cap | |
| 129 | 131 | // atomically under the usage-row lock. | |
| 130 | - | let billing = db::synckit_billing::get_app_with_billing(&state.db, app.id) | |
| 132 | + | let billing = db::synckit_billing::get_app_with_billing(&db, app.id) | |
| 131 | 133 | .await? | |
| 132 | 134 | .ok_or(AppError::Unauthorized)?; | |
| 133 | 135 | if !billing.is_internal && billing.enforcement_mode == "per_key" { | |
| 134 | 136 | let key_cap = billing.key_cap.unwrap_or(0); | |
| 135 | - | let claim = db::synckit_billing::claim_key(&state.db, app.id, &req.key, Some(key_cap)).await?; | |
| 137 | + | let claim = db::synckit_billing::claim_key(&db, app.id, &req.key, Some(key_cap)).await?; | |
| 136 | 138 | if claim.cap_reached { | |
| 137 | 139 | return Err(AppError::PaymentRequired(format!( | |
| 138 | 140 | "key limit reached ({} of {key_cap} keys claimed); release an unused key or raise the cap", | |
| @@ -165,10 +167,10 @@ pub(super) async fn sync_auth( | |||
| 165 | 167 | )] | |
| 166 | 168 | #[tracing::instrument(skip_all, name = "synckit::validate_app")] | |
| 167 | 169 | pub(super) async fn validate_app( | |
| 168 | - | State(state): State<AppState>, | |
| 170 | + | State(db): State<PgPool>, | |
| 169 | 171 | Json(params): Json<ValidateAppQuery>, | |
| 170 | 172 | ) -> Result<impl IntoResponse> { | |
| 171 | - | let app = db::synckit::get_sync_app_by_api_key(&state.db, ¶ms.api_key) | |
| 173 | + | let app = db::synckit::get_sync_app_by_api_key(&db, ¶ms.api_key) | |
| 172 | 174 | .await? | |
| 173 | 175 | .ok_or(AppError::Unauthorized)?; | |
| 174 | 176 | Ok(Json(ValidateAppResponse { |
| @@ -16,12 +16,14 @@ use axum::{ | |||
| 16 | 16 | Json, | |
| 17 | 17 | }; | |
| 18 | 18 | ||
| 19 | + | use sqlx::PgPool; | |
| 20 | + | ||
| 19 | 21 | use crate::{ | |
| 20 | 22 | auth::AuthUser, | |
| 23 | + | config::Config, | |
| 21 | 24 | db::{self, SyncAppId}, | |
| 22 | 25 | error::{AppError, Result}, | |
| 23 | 26 | synckit_billing::monthly_price_cents, | |
| 24 | - | AppState, | |
| 25 | 27 | }; | |
| 26 | 28 | ||
| 27 | 29 | use super::{ | |
| @@ -35,14 +37,16 @@ use super::{ | |||
| 35 | 37 | /// `POST /api/sync/apps/{id}/billing/setup` | |
| 36 | 38 | #[tracing::instrument(skip_all, name = "synckit::billing::setup")] | |
| 37 | 39 | pub(super) async fn setup( | |
| 38 | - | State(state): State<AppState>, | |
| 40 | + | State(db): State<PgPool>, | |
| 41 | + | State(payments): State<crate::Billing>, | |
| 42 | + | State(config): State<Config>, | |
| 39 | 43 | AuthUser(user): AuthUser, | |
| 40 | 44 | Path(app_id): Path<SyncAppId>, | |
| 41 | 45 | ) -> Result<impl IntoResponse> { | |
| 42 | 46 | user.check_not_sandbox()?; | |
| 43 | 47 | user.check_not_suspended()?; | |
| 44 | 48 | ||
| 45 | - | let app = db::synckit_billing::get_app_with_billing(&state.db, app_id) | |
| 49 | + | let app = db::synckit_billing::get_app_with_billing(&db, app_id) | |
| 46 | 50 | .await? | |
| 47 | 51 | .ok_or(AppError::NotFound)?; | |
| 48 | 52 | if app.creator_id != user.id { | |
| @@ -55,8 +59,7 @@ pub(super) async fn setup( | |||
| 55 | 59 | ))); | |
| 56 | 60 | } | |
| 57 | 61 | ||
| 58 | - | let stripe = state | |
| 59 | - | .stripe | |
| 62 | + | let stripe = payments.stripe | |
| 60 | 63 | .as_ref() | |
| 61 | 64 | .ok_or_else(|| AppError::ServiceUnavailable("Stripe is not configured".to_string()))?; | |
| 62 | 65 | ||
| @@ -64,18 +67,18 @@ pub(super) async fn setup( | |||
| 64 | 67 | let customer_id = match app.stripe_customer_id.as_deref() { | |
| 65 | 68 | Some(id) => id.to_string(), | |
| 66 | 69 | None => { | |
| 67 | - | let developer = db::users::get_user_by_id(&state.db, user.id) | |
| 70 | + | let developer = db::users::get_user_by_id(&db, user.id) | |
| 68 | 71 | .await? | |
| 69 | 72 | .ok_or(AppError::Unauthorized)?; | |
| 70 | 73 | let id = stripe | |
| 71 | 74 | .create_synckit_customer(user.id, app_id, developer.email.as_str(), &app.name) | |
| 72 | 75 | .await?; | |
| 73 | - | db::synckit_billing::set_stripe_customer(&state.db, app_id, &id).await?; | |
| 76 | + | db::synckit_billing::set_stripe_customer(&db, app_id, &id).await?; | |
| 74 | 77 | id | |
| 75 | 78 | } | |
| 76 | 79 | }; | |
| 77 | 80 | ||
| 78 | - | let return_url = synckit_return_url(&state, &app); | |
| 81 | + | let return_url = synckit_return_url(&config, &app); | |
| 79 | 82 | let portal_url = stripe | |
| 80 | 83 | .create_synckit_billing_portal(&customer_id, &return_url) | |
| 81 | 84 | .await?; | |
| @@ -92,7 +95,8 @@ pub(super) async fn setup( | |||
| 92 | 95 | /// `POST /api/sync/apps/{id}/billing/activate` | |
| 93 | 96 | #[tracing::instrument(skip_all, name = "synckit::billing::activate")] | |
| 94 | 97 | pub(super) async fn activate( | |
| 95 | - | State(state): State<AppState>, | |
| 98 | + | State(db): State<PgPool>, | |
| 99 | + | State(payments): State<crate::Billing>, | |
| 96 | 100 | AuthUser(user): AuthUser, | |
| 97 | 101 | Path(app_id): Path<SyncAppId>, | |
| 98 | 102 | Json(req): Json<BillingActivateRequest>, | |
| @@ -101,7 +105,7 @@ pub(super) async fn activate( | |||
| 101 | 105 | user.check_not_suspended()?; | |
| 102 | 106 | let mode = validate_knobs(&req.enforcement_mode, req.storage_gb_cap, req.key_cap, req.gb_per_key)?; | |
| 103 | 107 | ||
| 104 | - | let app = db::synckit_billing::get_app_with_billing(&state.db, app_id) | |
| 108 | + | let app = db::synckit_billing::get_app_with_billing(&db, app_id) | |
| 105 | 109 | .await? | |
| 106 | 110 | .ok_or(AppError::NotFound)?; | |
| 107 | 111 | if app.creator_id != user.id { | |
| @@ -126,8 +130,7 @@ pub(super) async fn activate( | |||
| 126 | 130 | req.gb_per_key, | |
| 127 | 131 | ); | |
| 128 | 132 | ||
| 129 | - | let stripe = state | |
| 130 | - | .stripe | |
| 133 | + | let stripe = payments.stripe | |
| 131 | 134 | .as_ref() | |
| 132 | 135 | .ok_or_else(|| AppError::ServiceUnavailable("Stripe is not configured".to_string()))?; | |
| 133 | 136 | let sub = stripe | |
| @@ -140,7 +143,7 @@ pub(super) async fn activate( | |||
| 140 | 143 | .ok_or_else(|| AppError::Internal(anyhow::anyhow!("Invalid period_end from Stripe")))?; | |
| 141 | 144 | ||
| 142 | 145 | db::synckit_billing::activate_billing( | |
| 143 | - | &state.db, | |
| 146 | + | &db, | |
| 144 | 147 | app_id, | |
| 145 | 148 | mode, | |
| 146 | 149 | req.storage_gb_cap.map(|v| v as i32), | |
| @@ -164,7 +167,8 @@ pub(super) async fn activate( | |||
| 164 | 167 | /// `PATCH /api/sync/apps/{id}/billing` | |
| 165 | 168 | #[tracing::instrument(skip_all, name = "synckit::billing::patch")] | |
| 166 | 169 | pub(super) async fn patch( | |
| 167 | - | State(state): State<AppState>, | |
| 170 | + | State(db): State<PgPool>, | |
| 171 | + | State(payments): State<crate::Billing>, | |
| 168 | 172 | AuthUser(user): AuthUser, | |
| 169 | 173 | Path(app_id): Path<SyncAppId>, | |
| 170 | 174 | Json(req): Json<BillingPatchRequest>, | |
| @@ -173,7 +177,7 @@ pub(super) async fn patch( | |||
| 173 | 177 | user.check_not_suspended()?; | |
| 174 | 178 | let mode = validate_knobs(&req.enforcement_mode, req.storage_gb_cap, req.key_cap, req.gb_per_key)?; | |
| 175 | 179 | ||
| 176 | - | let app = db::synckit_billing::get_app_with_billing(&state.db, app_id) | |
| 180 | + | let app = db::synckit_billing::get_app_with_billing(&db, app_id) | |
| 177 | 181 | .await? | |
| 178 | 182 | .ok_or(AppError::NotFound)?; | |
| 179 | 183 | if app.creator_id != user.id { | |
| @@ -198,8 +202,7 @@ pub(super) async fn patch( | |||
| 198 | 202 | req.gb_per_key, | |
| 199 | 203 | ); | |
| 200 | 204 | ||
| 201 | - | let stripe = state | |
| 202 | - | .stripe | |
| 205 | + | let stripe = payments.stripe | |
| 203 | 206 | .as_ref() | |
| 204 | 207 | .ok_or_else(|| AppError::ServiceUnavailable("Stripe is not configured".to_string()))?; | |
| 205 | 208 | stripe | |
| @@ -207,7 +210,7 @@ pub(super) async fn patch( | |||
| 207 | 210 | .await?; | |
| 208 | 211 | ||
| 209 | 212 | db::synckit_billing::update_knobs( | |
| 210 | - | &state.db, | |
| 213 | + | &db, | |
| 211 | 214 | app_id, | |
| 212 | 215 | mode, | |
| 213 | 216 | req.storage_gb_cap.map(|v| v as i32), | |
| @@ -228,13 +231,14 @@ pub(super) async fn patch( | |||
| 228 | 231 | /// `DELETE /api/sync/apps/{id}/billing` | |
| 229 | 232 | #[tracing::instrument(skip_all, name = "synckit::billing::cancel")] | |
| 230 | 233 | pub(super) async fn cancel( | |
| 231 | - | State(state): State<AppState>, | |
| 234 | + | State(db): State<PgPool>, | |
| 235 | + | State(payments): State<crate::Billing>, | |
| 232 | 236 | AuthUser(user): AuthUser, | |
| 233 | 237 | Path(app_id): Path<SyncAppId>, | |
| 234 | 238 | ) -> Result<impl IntoResponse> { | |
| 235 | 239 | user.check_not_sandbox()?; | |
| 236 | 240 | ||
| 237 | - | let app = db::synckit_billing::get_app_with_billing(&state.db, app_id) | |
| 241 | + | let app = db::synckit_billing::get_app_with_billing(&db, app_id) | |
| 238 | 242 | .await? | |
| 239 | 243 | .ok_or(AppError::NotFound)?; | |
| 240 | 244 | if app.creator_id != user.id { | |
| @@ -245,14 +249,13 @@ pub(super) async fn cancel( | |||
| 245 | 249 | } | |
| 246 | 250 | ||
| 247 | 251 | if let Some(sub_id) = app.stripe_subscription_id.as_deref() { | |
| 248 | - | let stripe = state | |
| 249 | - | .stripe | |
| 252 | + | let stripe = payments.stripe | |
| 250 | 253 | .as_ref() | |
| 251 | 254 | .ok_or_else(|| AppError::ServiceUnavailable("Stripe is not configured".to_string()))?; | |
| 252 | 255 | stripe.cancel_synckit_subscription(sub_id).await?; | |
| 253 | 256 | } | |
| 254 | 257 | ||
| 255 | - | db::synckit_billing::apply_billing_update(&state.db, app_id, Some("canceled"), None).await?; | |
| 258 | + | db::synckit_billing::apply_billing_update(&db, app_id, Some("canceled"), None).await?; | |
| 256 | 259 | ||
| 257 | 260 | Ok(axum::http::StatusCode::NO_CONTENT) | |
| 258 | 261 | } | |
| @@ -262,11 +265,11 @@ pub(super) async fn cancel( | |||
| 262 | 265 | /// `GET /api/sync/apps/{id}/billing` | |
| 263 | 266 | #[tracing::instrument(skip_all, name = "synckit::billing::get")] | |
| 264 | 267 | pub(super) async fn get( | |
| 265 | - | State(state): State<AppState>, | |
| 268 | + | State(db): State<PgPool>, | |
| 266 | 269 | AuthUser(user): AuthUser, | |
| 267 | 270 | Path(app_id): Path<SyncAppId>, | |
| 268 | 271 | ) -> Result<impl IntoResponse> { | |
| 269 | - | let app = db::synckit_billing::get_app_with_billing(&state.db, app_id) | |
| 272 | + | let app = db::synckit_billing::get_app_with_billing(&db, app_id) | |
| 270 | 273 | .await? | |
| 271 | 274 | .ok_or(AppError::NotFound)?; | |
| 272 | 275 | if app.creator_id != user.id { | |
| @@ -311,13 +314,15 @@ pub(super) async fn get( | |||
| 311 | 314 | /// `GET /api/sync/apps/{id}/billing/portal` | |
| 312 | 315 | #[tracing::instrument(skip_all, name = "synckit::billing::portal")] | |
| 313 | 316 | pub(super) async fn portal( | |
| 314 | - | State(state): State<AppState>, | |
| 317 | + | State(db): State<PgPool>, | |
| 318 | + | State(payments): State<crate::Billing>, | |
| 319 | + | State(config): State<Config>, | |
| 315 | 320 | AuthUser(user): AuthUser, | |
| 316 | 321 | Path(app_id): Path<SyncAppId>, | |
| 317 | 322 | ) -> Result<impl IntoResponse> { | |
| 318 | 323 | user.check_not_sandbox()?; | |
| 319 | 324 | ||
| 320 | - | let app = db::synckit_billing::get_app_with_billing(&state.db, app_id) | |
| 325 | + | let app = db::synckit_billing::get_app_with_billing(&db, app_id) | |
| 321 | 326 | .await? | |
| 322 | 327 | .ok_or(AppError::NotFound)?; | |
| 323 | 328 | if app.creator_id != user.id { | |
| @@ -330,12 +335,11 @@ pub(super) async fn portal( | |||
| 330 | 335 | ) | |
| 331 | 336 | })?; | |
| 332 | 337 | ||
| 333 | - | let stripe = state | |
| 334 | - | .stripe | |
| 338 | + | let stripe = payments.stripe | |
| 335 | 339 | .as_ref() | |
| 336 | 340 | .ok_or_else(|| AppError::ServiceUnavailable("Stripe is not configured".to_string()))?; | |
| 337 | 341 | ||
| 338 | - | let return_url = synckit_return_url(&state, &app); | |
| 342 | + | let return_url = synckit_return_url(&config, &app); | |
| 339 | 343 | let portal_url = stripe | |
| 340 | 344 | .create_synckit_billing_portal(customer_id, &return_url) | |
| 341 | 345 | .await?; | |
| @@ -349,15 +353,15 @@ pub(super) async fn portal( | |||
| 349 | 353 | /// developer back to the SyncKit tab on the project dashboard when the app is | |
| 350 | 354 | /// linked to a project, or the user-dashboard SyncKit tab otherwise. | |
| 351 | 355 | fn synckit_return_url( | |
| 352 | - | state: &AppState, | |
| 356 | + | config: &Config, | |
| 353 | 357 | app: &crate::db::DbSyncAppBilling, | |
| 354 | 358 | ) -> String { | |
| 355 | 359 | match app.project_slug.as_deref() { | |
| 356 | 360 | Some(slug) => format!( | |
| 357 | 361 | "{}/dashboard/project/{}#tab-synckit", | |
| 358 | - | state.config.host_url, slug | |
| 362 | + | config.host_url, slug | |
| 359 | 363 | ), | |
| 360 | - | None => format!("{}/dashboard#tab-synckit", state.config.host_url), | |
| 364 | + | None => format!("{}/dashboard#tab-synckit", config.host_url), | |
| 361 | 365 | } | |
| 362 | 366 | } | |
| 363 | 367 |
| @@ -8,13 +8,14 @@ use axum::{ | |||
| 8 | 8 | }; | |
| 9 | 9 | use serde_json::json; | |
| 10 | 10 | ||
| 11 | + | use sqlx::PgPool; | |
| 12 | + | ||
| 11 | 13 | use crate::{ | |
| 12 | 14 | constants, | |
| 13 | 15 | db::{self, synckit_billing}, | |
| 14 | 16 | error::{AppError, Result, ResultExt}, | |
| 15 | 17 | synckit_auth::SyncUser, | |
| 16 | 18 | validation, | |
| 17 | - | AppState, | |
| 18 | 19 | }; | |
| 19 | 20 | ||
| 20 | 21 | use super::{ | |
| @@ -34,12 +35,12 @@ use super::{ | |||
| 34 | 35 | )] | |
| 35 | 36 | #[tracing::instrument(skip_all, name = "synckit::blob_upload_url")] | |
| 36 | 37 | pub(super) async fn blob_upload_url( | |
| 37 | - | State(state): State<AppState>, | |
| 38 | + | State(db): State<PgPool>, | |
| 39 | + | State(storage): State<crate::AppStorage>, | |
| 38 | 40 | sync_user: SyncUser, | |
| 39 | 41 | Json(req): Json<BlobUploadUrlRequest>, | |
| 40 | 42 | ) -> Result<Response> { | |
| 41 | - | let synckit_s3 = state | |
| 42 | - | .storage.synckit_s3 | |
| 43 | + | let synckit_s3 = storage.synckit_s3 | |
| 43 | 44 | .as_ref() | |
| 44 | 45 | .ok_or_else(|| AppError::ServiceUnavailable("SyncKit blob storage is not configured".to_string()))?; | |
| 45 | 46 | ||
| @@ -55,7 +56,7 @@ pub(super) async fn blob_upload_url( | |||
| 55 | 56 | // an unsubscribed user, so they can't stage orphan S3 objects. Non-internal | |
| 56 | 57 | // (developer-billed) apps are always allowed here. Storage-cap enforcement | |
| 57 | 58 | // still happens atomically at confirm time. | |
| 58 | - | if !db::synckit::internal_write_allowed(&state.db, sync_user.app_id, sync_user.user_id).await? { | |
| 59 | + | if !db::synckit::internal_write_allowed(&db, sync_user.app_id, sync_user.user_id).await? { | |
| 59 | 60 | return Ok(( | |
| 60 | 61 | StatusCode::PAYMENT_REQUIRED, | |
| 61 | 62 | Json(json!({ "reason": "no_subscription" })), | |
| @@ -65,7 +66,7 @@ pub(super) async fn blob_upload_url( | |||
| 65 | 66 | ||
| 66 | 67 | // Check dedup — if this hash already exists, skip upload | |
| 67 | 68 | if let Some(_existing) = db::synckit::get_sync_blob_by_hash( | |
| 68 | - | &state.db, | |
| 69 | + | &db, | |
| 69 | 70 | sync_user.app_id, | |
| 70 | 71 | sync_user.user_id, | |
| 71 | 72 | &req.hash, | |
| @@ -84,7 +85,7 @@ pub(super) async fn blob_upload_url( | |||
| 84 | 85 | ); | |
| 85 | 86 | ||
| 86 | 87 | // Track the pending upload so the reaper can clean it up if never confirmed | |
| 87 | - | db::pending_uploads::record_pending_upload(&state.db, sync_user.user_id, &s3_key, "synckit").await?; | |
| 88 | + | db::pending_uploads::record_pending_upload(&db, sync_user.user_id, &s3_key, "synckit").await?; | |
| 88 | 89 | ||
| 89 | 90 | let upload_url = synckit_s3 | |
| 90 | 91 | .presign_upload( | |
| @@ -131,18 +132,18 @@ pub(super) async fn blob_upload_url( | |||
| 131 | 132 | )] | |
| 132 | 133 | #[tracing::instrument(skip_all, name = "synckit::blob_confirm_upload")] | |
| 133 | 134 | pub(super) async fn blob_confirm_upload( | |
| 134 | - | State(state): State<AppState>, | |
| 135 | + | State(db): State<PgPool>, | |
| 136 | + | State(storage): State<crate::AppStorage>, | |
| 135 | 137 | sync_user: SyncUser, | |
| 136 | 138 | Json(req): Json<BlobConfirmRequest>, | |
| 137 | 139 | ) -> Result<Response> { | |
| 138 | - | let synckit_s3 = state | |
| 139 | - | .storage.synckit_s3 | |
| 140 | + | let synckit_s3 = storage.synckit_s3 | |
| 140 | 141 | .as_ref() | |
| 141 | 142 | .ok_or_else(|| AppError::ServiceUnavailable("SyncKit blob storage is not configured".to_string()))?; | |
| 142 | 143 | ||
| 143 | 144 | validation::validate_sync_blob_hash(&req.hash)?; | |
| 144 | 145 | ||
| 145 | - | let billing = synckit_billing::get_app_with_billing(&state.db, sync_user.app_id) | |
| 146 | + | let billing = synckit_billing::get_app_with_billing(&db, sync_user.app_id) | |
| 146 | 147 | .await? | |
| 147 | 148 | .ok_or(AppError::NotFound)?; | |
| 148 | 149 | ||
| @@ -165,14 +166,14 @@ pub(super) async fn blob_confirm_upload( | |||
| 165 | 166 | } | |
| 166 | 167 | ||
| 167 | 168 | // Clear the pending upload record now that the upload is confirmed. | |
| 168 | - | db::pending_uploads::remove_pending_upload(&state.db, sync_user.user_id, &s3_key, "synckit").await?; | |
| 169 | + | db::pending_uploads::remove_pending_upload(&db, sync_user.user_id, &s3_key, "synckit").await?; | |
| 169 | 170 | ||
| 170 | 171 | // Record the blob and enforce the cap atomically. Internal apps gate on the | |
| 171 | 172 | // user's paid subscription + per-user cap; developer apps on the app/per-key | |
| 172 | 173 | // counters. Both are single-transaction (lock, check, insert, count). | |
| 173 | 174 | let outcome = if billing.is_internal { | |
| 174 | 175 | db::synckit::confirm_internal_blob( | |
| 175 | - | &state.db, sync_user.app_id, sync_user.user_id, | |
| 176 | + | &db, sync_user.app_id, sync_user.user_id, | |
| 176 | 177 | &req.hash, actual_size, &s3_key, &sync_user.key, | |
| 177 | 178 | ) | |
| 178 | 179 | .await? | |
| @@ -185,7 +186,7 @@ pub(super) async fn blob_confirm_upload( | |||
| 185 | 186 | .into_response()); | |
| 186 | 187 | } | |
| 187 | 188 | db::synckit::confirm_developer_blob( | |
| 188 | - | &state.db, sync_user.app_id, sync_user.user_id, | |
| 189 | + | &db, sync_user.app_id, sync_user.user_id, | |
| 189 | 190 | &req.hash, actual_size, &s3_key, &sync_user.key, | |
| 190 | 191 | billing.enforcement_mode, billing.storage_gb_cap, billing.key_cap, billing.gb_per_key, | |
| 191 | 192 | ) | |
| @@ -224,19 +225,20 @@ pub(super) async fn blob_confirm_upload( | |||
| 224 | 225 | )] | |
| 225 | 226 | #[tracing::instrument(skip_all, name = "synckit::blob_download_url")] | |
| 226 | 227 | pub(super) async fn blob_download_url( | |
| 227 | - | State(state): State<AppState>, | |
| 228 | + | State(db): State<PgPool>, | |
| 229 | + | State(storage): State<crate::AppStorage>, | |
| 230 | + | State(bg): State<crate::background::BackgroundTx>, | |
| 228 | 231 | sync_user: SyncUser, | |
| 229 | 232 | Json(req): Json<BlobDownloadUrlRequest>, | |
| 230 | 233 | ) -> Result<Response> { | |
| 231 | - | let synckit_s3 = state | |
| 232 | - | .storage.synckit_s3 | |
| 234 | + | let synckit_s3 = storage.synckit_s3 | |
| 233 | 235 | .as_ref() | |
| 234 | 236 | .ok_or_else(|| AppError::ServiceUnavailable("SyncKit blob storage is not configured".to_string()))?; | |
| 235 | 237 | ||
| 236 | 238 | validation::validate_sync_blob_hash(&req.hash)?; | |
| 237 | 239 | ||
| 238 | 240 | let blob = db::synckit::get_sync_blob_by_hash( | |
| 239 | - | &state.db, | |
| 241 | + | &db, | |
| 240 | 242 | sync_user.app_id, | |
| 241 | 243 | sync_user.user_id, | |
| 242 | 244 | &req.hash, | |
| @@ -247,7 +249,7 @@ pub(super) async fn blob_download_url( | |||
| 247 | 249 | // Billing check (internal apps bypass). Egress is NOT enforced — it's a | |
| 248 | 250 | // free metric for the developer's dashboard, absorbed in the storage rate | |
| 249 | 251 | // margin. We still count it at presign time so devs see the stat. | |
| 250 | - | let billing = synckit_billing::get_app_with_billing(&state.db, sync_user.app_id) | |
| 252 | + | let billing = synckit_billing::get_app_with_billing(&db, sync_user.app_id) | |
| 251 | 253 | .await? | |
| 252 | 254 | .ok_or(AppError::NotFound)?; | |
| 253 | 255 | if !billing.is_internal { | |
| @@ -264,10 +266,10 @@ pub(super) async fn blob_download_url( | |||
| 264 | 266 | // dashboard metric. Deferred onto the bounded background pool: it's a | |
| 265 | 267 | // single hot-row UPDATE that the download path shouldn't wait on or | |
| 266 | 268 | // contend its lock against (Perf P4). | |
| 267 | - | let db = state.db.clone(); | |
| 269 | + | let db = db.clone(); | |
| 268 | 270 | let app_id = sync_user.app_id; | |
| 269 | 271 | let egress_bytes = blob.size_bytes; | |
| 270 | - | state.bg.spawn("synckit-egress-bump", async move { | |
| 272 | + | bg.spawn("synckit-egress-bump", async move { | |
| 271 | 273 | if let Err(e) = synckit_billing::add_bytes_egress(&db, app_id, egress_bytes).await { | |
| 272 | 274 | tracing::error!(error = ?e, app_id = %app_id, "failed to bump bytes_egress_period"); | |
| 273 | 275 | } | |
| @@ -300,11 +302,11 @@ pub(super) async fn blob_download_url( | |||
| 300 | 302 | )] | |
| 301 | 303 | #[tracing::instrument(skip_all, name = "synckit::blob_delete")] | |
| 302 | 304 | pub(super) async fn blob_delete( | |
| 303 | - | State(state): State<AppState>, | |
| 305 | + | State(db): State<PgPool>, | |
| 304 | 306 | sync_user: SyncUser, | |
| 305 | 307 | axum::extract::Path(hash): axum::extract::Path<String>, | |
| 306 | 308 | ) -> Result<Response> { | |
| 307 | 309 | validation::validate_sync_blob_hash(&hash)?; | |
| 308 | - | db::synckit::delete_sync_blob(&state.db, sync_user.app_id, sync_user.user_id, &hash).await?; | |
| 310 | + | db::synckit::delete_sync_blob(&db, sync_user.app_id, sync_user.user_id, &hash).await?; | |
| 309 | 311 | Ok(StatusCode::NO_CONTENT.into_response()) | |
| 310 | 312 | } |
| @@ -18,10 +18,11 @@ use axum::{ | |||
| 18 | 18 | }; | |
| 19 | 19 | use serde_json::json; | |
| 20 | 20 | ||
| 21 | + | use sqlx::PgPool; | |
| 22 | + | ||
| 21 | 23 | use crate::{ | |
| 22 | 24 | db::{self, synckit_billing}, | |
| 23 | 25 | error::{AppError, Result}, | |
| 24 | - | AppState, | |
| 25 | 26 | }; | |
| 26 | 27 | ||
| 27 | 28 | use super::{ | |
| @@ -40,14 +41,14 @@ use super::{ | |||
| 40 | 41 | /// always idempotent OK). | |
| 41 | 42 | #[tracing::instrument(skip_all, name = "synckit::keys::claim")] | |
| 42 | 43 | pub(super) async fn claim( | |
| 43 | - | State(state): State<AppState>, | |
| 44 | + | State(db): State<PgPool>, | |
| 44 | 45 | Json(req): Json<ClaimKeyRequest>, | |
| 45 | 46 | ) -> Result<axum::response::Response> { | |
| 46 | - | let app = db::synckit::get_sync_app_by_api_key(&state.db, &req.api_key) | |
| 47 | + | let app = db::synckit::get_sync_app_by_api_key(&db, &req.api_key) | |
| 47 | 48 | .await? | |
| 48 | 49 | .ok_or(AppError::Unauthorized)?; | |
| 49 | 50 | ||
| 50 | - | let billing = synckit_billing::get_app_with_billing(&state.db, app.id) | |
| 51 | + | let billing = synckit_billing::get_app_with_billing(&db, app.id) | |
| 51 | 52 | .await? | |
| 52 | 53 | .ok_or(AppError::NotFound)?; | |
| 53 | 54 | ||
| @@ -71,7 +72,7 @@ pub(super) async fn claim( | |||
| 71 | 72 | None | |
| 72 | 73 | }; | |
| 73 | 74 | ||
| 74 | - | let result = synckit_billing::claim_key(&state.db, app.id, &req.key, key_cap).await?; | |
| 75 | + | let result = synckit_billing::claim_key(&db, app.id, &req.key, key_cap).await?; | |
| 75 | 76 | if result.cap_reached { | |
| 76 | 77 | return Ok(( | |
| 77 | 78 | StatusCode::PAYMENT_REQUIRED, | |
| @@ -96,14 +97,14 @@ pub(super) async fn claim( | |||
| 96 | 97 | /// cleanup paths can drain stale claims. | |
| 97 | 98 | #[tracing::instrument(skip_all, name = "synckit::keys::release")] | |
| 98 | 99 | pub(super) async fn release( | |
| 99 | - | State(state): State<AppState>, | |
| 100 | + | State(db): State<PgPool>, | |
| 100 | 101 | Json(req): Json<ReleaseKeyRequest>, | |
| 101 | 102 | ) -> Result<impl IntoResponse> { | |
| 102 | - | let app = db::synckit::get_sync_app_by_api_key(&state.db, &req.api_key) | |
| 103 | + | let app = db::synckit::get_sync_app_by_api_key(&db, &req.api_key) | |
| 103 | 104 | .await? | |
| 104 | 105 | .ok_or(AppError::Unauthorized)?; | |
| 105 | 106 | ||
| 106 | - | let result = synckit_billing::release_key(&state.db, app.id, &req.key).await?; | |
| 107 | + | let result = synckit_billing::release_key(&db, app.id, &req.key).await?; | |
| 107 | 108 | Ok(Json(ReleaseKeyResponse { | |
| 108 | 109 | newly_released: result.newly_released, | |
| 109 | 110 | total_claimed: result.total_claimed, | |
| @@ -116,10 +117,10 @@ pub(super) async fn release( | |||
| 116 | 117 | /// keeping the api_key out of access logs. | |
| 117 | 118 | #[tracing::instrument(skip_all, name = "synckit::keys::list")] | |
| 118 | 119 | pub(super) async fn list( | |
| 119 | - | State(state): State<AppState>, | |
| 120 | + | State(db): State<PgPool>, | |
| 120 | 121 | Json(req): Json<ListKeysRequest>, | |
| 121 | 122 | ) -> Result<impl IntoResponse> { | |
| 122 | - | let app = db::synckit::get_sync_app_by_api_key(&state.db, &req.api_key) | |
| 123 | + | let app = db::synckit::get_sync_app_by_api_key(&db, &req.api_key) | |
| 123 | 124 | .await? | |
| 124 | 125 | .ok_or(AppError::Unauthorized)?; | |
| 125 | 126 | ||
| @@ -128,7 +129,7 @@ pub(super) async fn list( | |||
| 128 | 129 | // deep-scan, the same DoS-shaped cost the limit clamp guards against. | |
| 129 | 130 | let offset = (req.offset.unwrap_or(0) as i64).clamp(0, 1_000_000_000); | |
| 130 | 131 | ||
| 131 | - | let rows = synckit_billing::list_active_keys(&state.db, app.id, limit, offset).await?; | |
| 132 | + | let rows = synckit_billing::list_active_keys(&db, app.id, limit, offset).await?; | |
| 132 | 133 | ||
| 133 | 134 | let keys = rows | |
| 134 | 135 | .into_iter() |
| @@ -22,7 +22,6 @@ use crate::{ | |||
| 22 | 22 | db::{SyncAppId, UserId}, | |
| 23 | 23 | error::{AppError, Result}, | |
| 24 | 24 | synckit_auth::SyncUser, | |
| 25 | - | AppState, | |
| 26 | 25 | }; | |
| 27 | 26 | ||
| 28 | 27 | /// Drop guard that decrements the per-user SSE connection counter. | |
| @@ -92,7 +91,7 @@ pub struct SubscribeQuery { | |||
| 92 | 91 | /// A keepalive comment is sent every 30 seconds to prevent proxy timeouts. | |
| 93 | 92 | #[tracing::instrument(skip_all, name = "synckit::subscribe")] | |
| 94 | 93 | pub(super) async fn sync_subscribe( | |
| 95 | - | State(state): State<AppState>, | |
| 94 | + | State(sync): State<crate::Sync>, | |
| 96 | 95 | sync_user: SyncUser, | |
| 97 | 96 | Query(query): Query<SubscribeQuery>, | |
| 98 | 97 | ) -> Result<Sse<impl tokio_stream::Stream<Item = std::result::Result<Event, Infallible>>>> { | |
| @@ -104,7 +103,7 @@ pub(super) async fn sync_subscribe( | |||
| 104 | 103 | } | |
| 105 | 104 | ||
| 106 | 105 | // Enforce per-user SSE connection limit | |
| 107 | - | let counter = state.caches.sse_connections | |
| 106 | + | let counter = sync.sse_connections | |
| 108 | 107 | .entry(sync_user.user_id) | |
| 109 | 108 | .or_insert_with(|| std::sync::atomic::AtomicUsize::new(0)); | |
| 110 | 109 | let current = counter.value().fetch_add(1, Ordering::AcqRel); | |
| @@ -118,15 +117,15 @@ pub(super) async fn sync_subscribe( | |||
| 118 | 117 | let key = (sync_user.app_id, sync_user.user_id); | |
| 119 | 118 | ||
| 120 | 119 | let guard = SseConnectionGuard { | |
| 121 | - | sse_connections: state.caches.sse_connections.clone(), | |
| 122 | - | sync_notify: state.caches.sync_notify.clone(), | |
| 120 | + | sse_connections: sync.sse_connections.clone(), | |
| 121 | + | sync_notify: sync.sync_notify.clone(), | |
| 123 | 122 | user_id: sync_user.user_id, | |
| 124 | 123 | app_id: sync_user.app_id, | |
| 125 | 124 | }; | |
| 126 | 125 | ||
| 127 | 126 | // Get or create the broadcast channel for this app+user | |
| 128 | 127 | let rx = { | |
| 129 | - | let entry = state.caches.sync_notify.entry(key).or_insert_with(|| { | |
| 128 | + | let entry = sync.sync_notify.entry(key).or_insert_with(|| { | |
| 130 | 129 | let (tx, _) = tokio::sync::broadcast::channel(16); | |
| 131 | 130 | tx | |
| 132 | 131 | }); |
| @@ -8,14 +8,16 @@ use axum::{ | |||
| 8 | 8 | }; | |
| 9 | 9 | use serde_json::json; | |
| 10 | 10 | ||
| 11 | + | use sqlx::PgPool; | |
| 12 | + | ||
| 11 | 13 | use crate::{ | |
| 14 | + | config::Config, | |
| 12 | 15 | constants, | |
| 13 | 16 | db::{self, SyncDeviceId}, | |
| 14 | 17 | error::{AppError, Result}, | |
| 15 | 18 | payments::{self, SyncBillingInterval}, | |
| 16 | 19 | synckit_auth::SyncUser, | |
| 17 | 20 | validation, | |
| 18 | - | AppState, | |
| 19 | 21 | }; | |
| 20 | 22 | ||
| 21 | 23 | use super::{ | |
| @@ -38,7 +40,8 @@ use super::{ | |||
| 38 | 40 | )] | |
| 39 | 41 | #[tracing::instrument(skip_all, name = "synckit::sync_push", fields(app_id, user_id))] | |
| 40 | 42 | pub(super) async fn sync_push( | |
| 41 | - | State(state): State<AppState>, | |
| 43 | + | State(db): State<PgPool>, | |
| 44 | + | State(sync): State<crate::Sync>, | |
| 42 | 45 | sync_user: SyncUser, | |
| 43 | 46 | Json(req): Json<PushRequest>, | |
| 44 | 47 | ) -> Result<Response> { | |
| @@ -50,7 +53,7 @@ pub(super) async fn sync_push( | |||
| 50 | 53 | // Paid-only sync: first-party apps require an active end-user subscription | |
| 51 | 54 | // to write. Reads (pull) stay open so a lapsed user can still export their | |
| 52 | 55 | // data. Non-internal (developer-billed) apps are always allowed here. | |
| 53 | - | if !db::synckit::internal_write_allowed(&state.db, app_id, user_id).await? { | |
| 56 | + | if !db::synckit::internal_write_allowed(&db, app_id, user_id).await? { | |
| 54 | 57 | return Ok(( | |
| 55 | 58 | StatusCode::PAYMENT_REQUIRED, | |
| 56 | 59 | Json(json!({ "reason": "no_subscription" })), | |
| @@ -81,12 +84,12 @@ pub(super) async fn sync_push( | |||
| 81 | 84 | ||
| 82 | 85 | // Verify device belongs to this user + app (indexed point lookup, not a | |
| 83 | 86 | // full device fetch + linear scan). | |
| 84 | - | if !db::synckit::sync_device_belongs(&state.db, req.device_id, app_id, user_id).await? { | |
| 87 | + | if !db::synckit::sync_device_belongs(&db, req.device_id, app_id, user_id).await? { | |
| 85 | 88 | return Err(AppError::BadRequest("Unknown device".to_string())); | |
| 86 | 89 | } | |
| 87 | 90 | ||
| 88 | 91 | // Touch device | |
| 89 | - | db::synckit::touch_sync_device(&state.db, req.device_id).await?; | |
| 92 | + | db::synckit::touch_sync_device(&db, req.device_id).await?; | |
| 90 | 93 | ||
| 91 | 94 | // Build change tuples (op converted to string for TEXT column) | |
| 92 | 95 | let changes: Vec<_> = req | |
| @@ -104,7 +107,7 @@ pub(super) async fn sync_push( | |||
| 104 | 107 | .collect(); | |
| 105 | 108 | ||
| 106 | 109 | let cursor = db::synckit::push_sync_changes( | |
| 107 | - | &state.db, | |
| 110 | + | &db, | |
| 108 | 111 | app_id, | |
| 109 | 112 | user_id, | |
| 110 | 113 | req.device_id, | |
| @@ -116,7 +119,7 @@ pub(super) async fn sync_push( | |||
| 116 | 119 | // Notify SSE subscribers, carrying the new max seq so a device already at | |
| 117 | 120 | // (or past) this cursor can skip a redundant pull — avoids the thundering | |
| 118 | 121 | // herd where every subscriber pulls on every push. | |
| 119 | - | if let Some(sender) = state.caches.sync_notify.get(&(app_id, user_id)) { | |
| 122 | + | if let Some(sender) = sync.sync_notify.get(&(app_id, user_id)) { | |
| 120 | 123 | let _ = sender.send(cursor); // Ignore errors (no subscribers = ok) | |
| 121 | 124 | } | |
| 122 | 125 | ||
| @@ -131,7 +134,7 @@ pub(super) async fn sync_push( | |||
| 131 | 134 | )] | |
| 132 | 135 | #[tracing::instrument(skip_all, name = "synckit::sync_pull", fields(app_id, user_id))] | |
| 133 | 136 | pub(super) async fn sync_pull( | |
| 134 | - | State(state): State<AppState>, | |
| 137 | + | State(db): State<PgPool>, | |
| 135 | 138 | sync_user: SyncUser, | |
| 136 | 139 | Json(req): Json<PullRequest>, | |
| 137 | 140 | ) -> Result<impl IntoResponse> { | |
| @@ -142,7 +145,7 @@ pub(super) async fn sync_pull( | |||
| 142 | 145 | ||
| 143 | 146 | // Verify device belongs to this user + app (indexed point lookup, not a | |
| 144 | 147 | // full device fetch + linear scan). | |
| 145 | - | if !db::synckit::sync_device_belongs(&state.db, req.device_id, app_id, user_id).await? { | |
| 148 | + | if !db::synckit::sync_device_belongs(&db, req.device_id, app_id, user_id).await? { | |
| 146 | 149 | return Err(AppError::BadRequest("Unknown device".to_string())); | |
| 147 | 150 | } | |
| 148 | 151 | ||
| @@ -160,7 +163,7 @@ pub(super) async fn sync_pull( | |||
| 160 | 163 | ||
| 161 | 164 | let page_size = constants::SYNCKIT_PULL_PAGE_SIZE; | |
| 162 | 165 | let entries = db::synckit::pull_sync_changes_filtered( | |
| 163 | - | &state.db, | |
| 166 | + | &db, | |
| 164 | 167 | app_id, | |
| 165 | 168 | user_id, | |
| 166 | 169 | req.cursor, | |
| @@ -175,7 +178,7 @@ pub(super) async fn sync_pull( | |||
| 175 | 178 | ||
| 176 | 179 | // Mark the device seen and advance its compaction cursor in one statement. | |
| 177 | 180 | // GREATEST keeps the cursor monotonic even if `new_cursor == req.cursor`. | |
| 178 | - | db::synckit::touch_and_advance_cursor(&state.db, req.device_id, new_cursor).await?; | |
| 181 | + | db::synckit::touch_and_advance_cursor(&db, req.device_id, new_cursor).await?; | |
| 179 | 182 | ||
| 180 | 183 | let changes: Vec<PullChangeEntry> = entries | |
| 181 | 184 | .into_iter() | |
| @@ -205,14 +208,14 @@ pub(super) async fn sync_pull( | |||
| 205 | 208 | )] | |
| 206 | 209 | #[tracing::instrument(skip_all, name = "synckit::sync_status")] | |
| 207 | 210 | pub(super) async fn sync_status( | |
| 208 | - | State(state): State<AppState>, | |
| 211 | + | State(db): State<PgPool>, | |
| 209 | 212 | sync_user: SyncUser, | |
| 210 | 213 | ) -> Result<impl IntoResponse> { | |
| 211 | 214 | let app_id = sync_user.app_id; | |
| 212 | 215 | let user_id = sync_user.user_id; | |
| 213 | 216 | ||
| 214 | 217 | let (total_changes, latest_cursor) = | |
| 215 | - | db::synckit::get_sync_status(&state.db, app_id, user_id).await?; | |
| 218 | + | db::synckit::get_sync_status(&db, app_id, user_id).await?; | |
| 216 | 219 | ||
| 217 | 220 | Ok(Json(SyncStatusResponse { | |
| 218 | 221 | total_changes, | |
| @@ -228,10 +231,10 @@ pub(super) async fn sync_status( | |||
| 228 | 231 | )] | |
| 229 | 232 | #[tracing::instrument(skip_all, name = "synckit::sync_account")] | |
| 230 | 233 | pub(super) async fn sync_account( | |
| 231 | - | State(state): State<AppState>, | |
| 234 | + | State(db): State<PgPool>, | |
| 232 | 235 | sync_user: SyncUser, | |
| 233 | 236 | ) -> Result<impl IntoResponse> { | |
| 234 | - | let user = db::users::get_user_by_id(&state.db, sync_user.user_id) | |
| 237 | + | let user = db::users::get_user_by_id(&db, sync_user.user_id) | |
| 235 | 238 | .await? | |
| 236 | 239 | .ok_or(AppError::NotFound)?; | |
| 237 | 240 | ||
| @@ -250,11 +253,11 @@ pub(super) async fn sync_account( | |||
| 250 | 253 | )] | |
| 251 | 254 | #[tracing::instrument(skip_all, name = "synckit::sync_subscription_status")] | |
| 252 | 255 | pub(super) async fn sync_subscription_status( | |
| 253 | - | State(state): State<AppState>, | |
| 256 | + | State(db): State<PgPool>, | |
| 254 | 257 | sync_user: SyncUser, | |
| 255 | 258 | ) -> Result<impl IntoResponse> { | |
| 256 | 259 | let sub = db::synckit::get_user_app_subscription( | |
| 257 | - | &state.db, | |
| 260 | + | &db, | |
| 258 | 261 | sync_user.user_id, | |
| 259 | 262 | sync_user.app_id, | |
| 260 | 263 | ) | |
| @@ -293,10 +296,10 @@ pub(super) async fn sync_subscription_status( | |||
| 293 | 296 | )] | |
| 294 | 297 | #[tracing::instrument(skip_all, name = "synckit::get_app_pricing")] | |
| 295 | 298 | pub(super) async fn get_app_pricing( | |
| 296 | - | State(state): State<AppState>, | |
| 299 | + | State(db): State<PgPool>, | |
| 297 | 300 | Json(req): Json<AppPricingRequest>, | |
| 298 | 301 | ) -> Result<impl IntoResponse> { | |
| 299 | - | let app = db::synckit::get_sync_app_by_api_key(&state.db, &req.api_key) | |
| 302 | + | let app = db::synckit::get_sync_app_by_api_key(&db, &req.api_key) | |
| 300 | 303 | .await? | |
| 301 | 304 | .ok_or(AppError::NotFound)?; | |
| 302 | 305 | ||
| @@ -320,7 +323,6 @@ pub(super) async fn get_app_pricing( | |||
| 320 | 323 | )] | |
| 321 | 324 | #[tracing::instrument(skip_all, name = "synckit::quote_subscription_price")] | |
| 322 | 325 | pub(super) async fn quote_subscription_price( | |
| 323 | - | State(_state): State<AppState>, | |
| 324 | 326 | _sync_user: SyncUser, | |
| 325 | 327 | Json(req): Json<SyncQuoteRequest>, | |
| 326 | 328 | ) -> Result<impl IntoResponse> { | |
| @@ -343,11 +345,13 @@ pub(super) async fn quote_subscription_price( | |||
| 343 | 345 | )] | |
| 344 | 346 | #[tracing::instrument(skip_all, name = "synckit::create_subscription_checkout")] | |
| 345 | 347 | pub(super) async fn create_subscription_checkout( | |
| 346 | - | State(state): State<AppState>, | |
| 348 | + | State(db): State<PgPool>, | |
| 349 | + | State(payments): State<crate::Billing>, | |
| 350 | + | State(config): State<Config>, | |
| 347 | 351 | sync_user: SyncUser, | |
| 348 | 352 | Json(req): Json<SyncSubscribeRequest>, | |
| 349 | 353 | ) -> Result<impl IntoResponse> { | |
| 350 | - | if db::synckit::get_user_app_subscription(&state.db, sync_user.user_id, sync_user.app_id) | |
| 354 | + | if db::synckit::get_user_app_subscription(&db, sync_user.user_id, sync_user.app_id) | |
| 351 | 355 | .await? | |
| 352 | 356 | .is_some() | |
| 353 | 357 | { | |
| @@ -359,19 +363,18 @@ pub(super) async fn create_subscription_checkout( | |||
| 359 | 363 | let interval = SyncBillingInterval::parse(&req.interval)?; | |
| 360 | 364 | let amount_cents = payments::quote_price_cents(req.cap_bytes, interval)?; | |
| 361 | 365 | ||
| 362 | - | let app = db::synckit::get_sync_app_by_id(&state.db, sync_user.app_id) | |
| 366 | + | let app = db::synckit::get_sync_app_by_id(&db, sync_user.app_id) | |
| 363 | 367 | .await? | |
| 364 | 368 | .ok_or(AppError::NotFound)?; | |
| 365 | 369 | let cap_gib = req.cap_bytes / (1024 * 1024 * 1024); | |
| 366 | 370 | let product_name = format!("{} cloud sync — {} GiB", app.name, cap_gib); | |
| 367 | 371 | ||
| 368 | - | let stripe = state | |
| 369 | - | .stripe | |
| 372 | + | let stripe = payments.stripe | |
| 370 | 373 | .as_ref() | |
| 371 | 374 | .ok_or_else(|| AppError::BadRequest("Stripe is not configured".to_string()))?; | |
| 372 | 375 | ||
| 373 | - | let success_url = format!("{}/sync/subscribed", state.config.host_url); | |
| 374 | - | let cancel_url = format!("{}/sync/canceled", state.config.host_url); | |
| 376 | + | let success_url = format!("{}/sync/subscribed", config.host_url); | |
| 377 | + | let cancel_url = format!("{}/sync/canceled", config.host_url); | |
| 375 | 378 | ||
| 376 | 379 | let result = stripe | |
| 377 | 380 | .create_synckit_app_sub_checkout_session(&crate::payments::SynckitAppSubCheckoutParams { | |
| @@ -409,11 +412,12 @@ pub(super) async fn create_subscription_checkout( | |||
| 409 | 412 | )] | |
| 410 | 413 | #[tracing::instrument(skip_all, name = "synckit::queue_storage_cap_change")] | |
| 411 | 414 | pub(super) async fn queue_storage_cap_change( | |
| 412 | - | State(state): State<AppState>, | |
| 415 | + | State(db): State<PgPool>, | |
| 416 | + | State(payments): State<crate::Billing>, | |
| 413 | 417 | sync_user: SyncUser, | |
| 414 | 418 | Json(req): Json<SyncCapChangeRequest>, | |
| 415 | 419 | ) -> Result<impl IntoResponse> { | |
| 416 | - | let sub = db::synckit::get_user_app_subscription(&state.db, sync_user.user_id, sync_user.app_id) | |
| 420 | + | let sub = db::synckit::get_user_app_subscription(&db, sync_user.user_id, sync_user.app_id) | |
| 417 | 421 | .await? | |
| 418 | 422 | .ok_or_else(|| AppError::BadRequest("No active subscription to adjust".to_string()))?; | |
| 419 | 423 | ||
| @@ -428,12 +432,11 @@ pub(super) async fn queue_storage_cap_change( | |||
| 428 | 432 | let interval = payments::SyncBillingInterval::parse(&sub.interval)?; | |
| 429 | 433 | let new_price_cents = payments::quote_price_cents(req.cap_bytes, interval)?; | |
| 430 | 434 | ||
| 431 | - | let stripe = state | |
| 432 | - | .stripe | |
| 435 | + | let stripe = payments.stripe | |
| 433 | 436 | .as_ref() | |
| 434 | 437 | .ok_or_else(|| AppError::BadRequest("Stripe is not configured".to_string()))?; | |
| 435 | 438 | ||
| 436 | - | let app = db::synckit::get_sync_app_by_id(&state.db, sync_user.app_id) | |
| 439 | + | let app = db::synckit::get_sync_app_by_id(&db, sync_user.app_id) | |
| 437 | 440 | .await? | |
| 438 | 441 | .ok_or(AppError::NotFound)?; | |
| 439 | 442 | let cap_gib = req.cap_bytes / (1024 * 1024 * 1024); | |
| @@ -451,7 +454,7 @@ pub(super) async fn queue_storage_cap_change( | |||
| 451 | 454 | .await?; | |
| 452 | 455 | ||
| 453 | 456 | db::synckit::set_pending_storage_cap( | |
| 454 | - | &state.db, | |
| 457 | + | &db, | |
| 455 | 458 | sync_user.user_id, | |
| 456 | 459 | sync_user.app_id, | |
| 457 | 460 | req.cap_bytes, | |
| @@ -479,7 +482,7 @@ pub(super) async fn queue_storage_cap_change( | |||
| 479 | 482 | )] | |
| 480 | 483 | #[tracing::instrument(skip_all, name = "synckit::register_device")] | |
| 481 | 484 | pub(super) async fn register_device( | |
| 482 | - | State(state): State<AppState>, | |
| 485 | + | State(db): State<PgPool>, | |
| 483 | 486 | sync_user: SyncUser, | |
| 484 | 487 | Json(req): Json<RegisterDeviceRequest>, | |
| 485 | 488 | ) -> Result<impl IntoResponse> { | |
| @@ -490,7 +493,7 @@ pub(super) async fn register_device( | |||
| 490 | 493 | // scoped; holding `_lock_tx` to the end of the handler keeps a second | |
| 491 | 494 | // concurrent registration blocked until this one's upsert commits, so it | |
| 492 | 495 | // sees the updated count and can't push past the cap (Run 21 concurrency). | |
| 493 | - | let mut _lock_tx = state.db.begin().await?; | |
| 496 | + | let mut _lock_tx = db.begin().await?; | |
| 494 | 497 | sqlx::query( | |
| 495 | 498 | "SELECT pg_advisory_xact_lock(hashtextextended('synckit_device:' || $1::text || ':' || $2::text, 0))", | |
| 496 | 499 | ) | |
| @@ -500,10 +503,10 @@ pub(super) async fn register_device( | |||
| 500 | 503 | .await?; | |
| 501 | 504 | ||
| 502 | 505 | // Enforce device limit (upsert on existing name is fine, only new names count) | |
| 503 | - | let count = db::synckit::count_sync_devices(&state.db, sync_user.app_id, sync_user.user_id).await?; | |
| 506 | + | let count = db::synckit::count_sync_devices(&db, sync_user.app_id, sync_user.user_id).await?; | |
| 504 | 507 | if count >= constants::SYNCKIT_MAX_DEVICES_PER_APP { | |
| 505 | 508 | // Check if this is an existing device (upsert) — allow updates | |
| 506 | - | let existing = db::synckit::get_sync_devices(&state.db, sync_user.app_id, sync_user.user_id).await?; | |
| 509 | + | let existing = db::synckit::get_sync_devices(&db, sync_user.app_id, sync_user.user_id).await?; | |
| 507 | 510 | if !existing.iter().any(|d| d.device_name == req.device_name) { | |
| 508 | 511 | return Err(AppError::BadRequest(format!( | |
| 509 | 512 | "Maximum {} devices per app", | |
| @@ -513,7 +516,7 @@ pub(super) async fn register_device( | |||
| 513 | 516 | } | |
| 514 | 517 | ||
| 515 | 518 | let device = db::synckit::upsert_sync_device( | |
| 516 | - | &state.db, | |
| 519 | + | &db, | |
| 517 | 520 | sync_user.app_id, | |
| 518 | 521 | sync_user.user_id, | |
| 519 | 522 | &req.device_name, | |
| @@ -539,11 +542,11 @@ pub(super) async fn register_device( | |||
| 539 | 542 | )] | |
| 540 | 543 | #[tracing::instrument(skip_all, name = "synckit::list_devices")] | |
| 541 | 544 | pub(super) async fn list_devices( | |
| 542 | - | State(state): State<AppState>, | |
| 545 | + | State(db): State<PgPool>, | |
| 543 | 546 | sync_user: SyncUser, | |
| 544 | 547 | ) -> Result<impl IntoResponse> { | |
| 545 | 548 | let devices = | |
| 546 | - | db::synckit::get_sync_devices(&state.db, sync_user.app_id, sync_user.user_id).await?; | |
| 549 | + | db::synckit::get_sync_devices(&db, sync_user.app_id, sync_user.user_id).await?; | |
| 547 | 550 | ||
| 548 | 551 | let response: Vec<SyncDeviceResponse> = devices.into_iter().map(|d| SyncDeviceResponse { | |
| 549 | 552 | id: d.id, | |
| @@ -566,13 +569,13 @@ pub(super) async fn list_devices( | |||
| 566 | 569 | )] | |
| 567 | 570 | #[tracing::instrument(skip_all, name = "synckit::delete_device")] | |
| 568 | 571 | pub(super) async fn delete_device( | |
| 569 | - | State(state): State<AppState>, | |
| 572 | + | State(db): State<PgPool>, | |
| 570 | 573 | sync_user: SyncUser, | |
| 571 | 574 | headers: axum::http::HeaderMap, | |
| 572 | 575 | Path(device_id): Path<SyncDeviceId>, | |
| 573 | 576 | ) -> Result<impl IntoResponse> { | |
| 574 | 577 | let deleted = | |
| 575 | - | db::synckit::delete_sync_device(&state.db, device_id, sync_user.app_id, sync_user.user_id) | |
| 578 | + | db::synckit::delete_sync_device(&db, device_id, sync_user.app_id, sync_user.user_id) | |
| 576 | 579 | .await?; | |
| 577 | 580 | ||
| 578 | 581 | if !deleted { | |
| @@ -584,12 +587,12 @@ pub(super) async fn delete_device( | |||
| 584 | 587 | // expiry), and record the event for the audit log. Best-effort — a logging | |
| 585 | 588 | // or invalidation hiccup must not fail the delete the user asked for. | |
| 586 | 589 | let ip = crate::helpers::extract_client_ip(&headers); | |
| 587 | - | if let Err(e) = db::synckit::invalidate_user_sync_tokens(&state.db, sync_user.user_id).await { | |
| 590 | + | if let Err(e) = db::synckit::invalidate_user_sync_tokens(&db, sync_user.user_id).await { | |
| 588 | 591 | tracing::error!(error = ?e, user_id = %sync_user.user_id, | |
| 589 | 592 | "failed to invalidate sync tokens after device removal"); | |
| 590 | 593 | } | |
| 591 | 594 | if let Err(e) = db::synckit::record_security_event( | |
| 592 | - | &state.db, | |
| 595 | + | &db, | |
| 593 | 596 | sync_user.app_id, | |
| 594 | 597 | Some(sync_user.user_id), | |
| 595 | 598 | db::synckit::sync_security_event::DEVICE_REMOVED, | |
| @@ -614,7 +617,7 @@ pub(super) async fn delete_device( | |||
| 614 | 617 | )] | |
| 615 | 618 | #[tracing::instrument(skip_all, name = "synckit::put_sync_key")] | |
| 616 | 619 | pub(super) async fn put_sync_key( | |
| 617 | - | State(state): State<AppState>, | |
| 620 | + | State(db): State<PgPool>, | |
| 618 | 621 | sync_user: SyncUser, | |
| 619 | 622 | Json(req): Json<PutKeyRequest>, | |
| 620 | 623 | ) -> Result<impl IntoResponse> { | |
| @@ -626,7 +629,7 @@ pub(super) async fn put_sync_key( | |||
| 626 | 629 | } | |
| 627 | 630 | ||
| 628 | 631 | let updated = db::synckit::upsert_sync_key( | |
| 629 | - | &state.db, | |
| 632 | + | &db, | |
| 630 | 633 | sync_user.app_id, | |
| 631 | 634 | sync_user.user_id, | |
| 632 | 635 | &req.encrypted_key, | |
| @@ -650,11 +653,11 @@ pub(super) async fn put_sync_key( | |||
| 650 | 653 | )] | |
| 651 | 654 | #[tracing::instrument(skip_all, name = "synckit::get_sync_key")] | |
| 652 | 655 | pub(super) async fn get_sync_key( | |
| 653 | - | State(state): State<AppState>, | |
| 656 | + | State(db): State<PgPool>, | |
| 654 | 657 | sync_user: SyncUser, | |
| 655 | 658 | ) -> Result<impl IntoResponse> { | |
| 656 | 659 | let info = | |
| 657 | - | db::synckit::get_sync_key(&state.db, sync_user.app_id, sync_user.user_id) | |
| 660 | + | db::synckit::get_sync_key(&db, sync_user.app_id, sync_user.user_id) | |
| 658 | 661 | .await? | |
| 659 | 662 | .ok_or(AppError::NotFound)?; | |
| 660 | 663 | ||
| @@ -683,7 +686,7 @@ pub(super) async fn get_sync_key( | |||
| 683 | 686 | )] | |
| 684 | 687 | #[tracing::instrument(skip_all, name = "synckit::begin_rotation")] | |
| 685 | 688 | pub(super) async fn begin_rotation( | |
| 686 | - | State(state): State<AppState>, | |
| 689 | + | State(db): State<PgPool>, | |
| 687 | 690 | sync_user: SyncUser, | |
| 688 | 691 | Json(req): Json<BeginRotationRequest>, | |
| 689 | 692 | ) -> Result<impl IntoResponse> { | |
| @@ -695,12 +698,12 @@ pub(super) async fn begin_rotation( | |||
| 695 | 698 | ||
| 696 | 699 | // Verify device belongs to this user + app via an indexed point lookup, not a | |
| 697 | 700 | // fetch-all-then-scan (ultra-fuzz Run 4 Perf). | |
| 698 | - | if !db::synckit::sync_device_belongs(&state.db, req.device_id, sync_user.app_id, sync_user.user_id).await? { | |
| 701 | + | if !db::synckit::sync_device_belongs(&db, req.device_id, sync_user.app_id, sync_user.user_id).await? { | |
| 699 | 702 | return Err(AppError::BadRequest("Unknown device".to_string())); | |
| 700 | 703 | } | |
| 701 | 704 | ||
| 702 | 705 | let result = db::synckit::begin_key_rotation( | |
| 703 | - | &state.db, | |
| 706 | + | &db, | |
| 704 | 707 | sync_user.app_id, | |
| 705 | 708 | sync_user.user_id, | |
| 706 | 709 | req.device_id, | |
| @@ -727,12 +730,12 @@ pub(super) async fn begin_rotation( | |||
| 727 | 730 | )] | |
| 728 | 731 | #[tracing::instrument(skip_all, name = "synckit::rotation_entries")] | |
| 729 | 732 | pub(super) async fn rotation_entries( | |
| 730 | - | State(state): State<AppState>, | |
| 733 | + | State(db): State<PgPool>, | |
| 731 | 734 | sync_user: SyncUser, | |
| 732 | 735 | Json(req): Json<RotationEntriesRequest>, | |
| 733 | 736 | ) -> Result<impl IntoResponse> { | |
| 734 | 737 | let rotation = db::synckit::get_key_rotation( | |
| 735 | - | &state.db, | |
| 738 | + | &db, | |
| 736 | 739 | sync_user.app_id, | |
| 737 | 740 | sync_user.user_id, | |
| 738 | 741 | ) | |
| @@ -745,7 +748,7 @@ pub(super) async fn rotation_entries( | |||
| 745 | 748 | ||
| 746 | 749 | let page_size = constants::SYNCKIT_ROTATION_BATCH_MAX as i64; | |
| 747 | 750 | let raw_entries = db::synckit::get_rotation_entries( | |
| 748 | - | &state.db, | |
| 751 | + | &db, | |
| 749 | 752 | sync_user.app_id, | |
| 750 | 753 | sync_user.user_id, | |
| 751 | 754 | rotation.new_key_id, | |
| @@ -771,7 +774,7 @@ pub(super) async fn rotation_entries( | |||
| 771 | 774 | )] | |
| 772 | 775 | #[tracing::instrument(skip_all, name = "synckit::rotation_batch")] | |
| 773 | 776 | pub(super) async fn rotation_batch( | |
| 774 | - | State(state): State<AppState>, | |
| 777 | + | State(db): State<PgPool>, | |
| 775 | 778 | sync_user: SyncUser, | |
| 776 | 779 | Json(req): Json<RotationBatchRequest>, | |
| 777 | 780 | ) -> Result<impl IntoResponse> { | |
| @@ -786,7 +789,7 @@ pub(super) async fn rotation_batch( | |||
| 786 | 789 | } | |
| 787 | 790 | ||
| 788 | 791 | let rotation = db::synckit::get_key_rotation( | |
| 789 | - | &state.db, | |
| 792 | + | &db, | |
| 790 | 793 | sync_user.app_id, | |
| 791 | 794 | sync_user.user_id, | |
| 792 | 795 | ) | |
| @@ -804,7 +807,7 @@ pub(super) async fn rotation_batch( | |||
| 804 | 807 | .collect(); | |
| 805 | 808 | ||
| 806 | 809 | let updated_count = db::synckit::submit_rotation_batch( | |
| 807 | - | &state.db, | |
| 810 | + | &db, | |
| 808 | 811 | sync_user.app_id, | |
| 809 | 812 | sync_user.user_id, | |
| 810 | 813 | rotation.id, | |
| @@ -827,12 +830,12 @@ pub(super) async fn rotation_batch( | |||
| 827 | 830 | )] | |
| 828 | 831 | #[tracing::instrument(skip_all, name = "synckit::complete_rotation")] | |
| 829 | 832 | pub(super) async fn complete_rotation( | |
| 830 | - | State(state): State<AppState>, | |
| 833 | + | State(db): State<PgPool>, | |
| 831 | 834 | sync_user: SyncUser, | |
| 832 | 835 | Json(req): Json<CompleteRotationRequest>, | |
| 833 | 836 | ) -> Result<impl IntoResponse> { | |
| 834 | 837 | let result = db::synckit::complete_key_rotation( | |
| 835 | - | &state.db, | |
| 838 | + | &db, | |
| 836 | 839 | sync_user.app_id, | |
| 837 | 840 | sync_user.user_id, | |
| 838 | 841 | req.rotation_id, | |
| @@ -843,7 +846,7 @@ pub(super) async fn complete_rotation( | |||
| 843 | 846 | Ok(_new_key_id) => { | |
| 844 | 847 | // Audit the completed rotation (best-effort). | |
| 845 | 848 | if let Err(e) = db::synckit::record_security_event( | |
| 846 | - | &state.db, | |
| 849 | + | &db, | |
| 847 | 850 | sync_user.app_id, | |
| 848 | 851 | Some(sync_user.user_id), | |
| 849 | 852 | db::synckit::sync_security_event::KEY_ROTATION_COMPLETED, | |
| @@ -874,11 +877,11 @@ pub(super) async fn complete_rotation( | |||
| 874 | 877 | )] | |
| 875 | 878 | #[tracing::instrument(skip_all, name = "synckit::cancel_rotation")] | |
| 876 | 879 | pub(super) async fn cancel_rotation( | |
| 877 | - | State(state): State<AppState>, | |
| 880 | + | State(db): State<PgPool>, | |
| 878 | 881 | sync_user: SyncUser, | |
| 879 | 882 | ) -> Result<impl IntoResponse> { | |
| 880 | 883 | let cancelled = db::synckit::cancel_stale_rotation( | |
| 881 | - | &state.db, | |
| 884 | + | &db, | |
| 882 | 885 | sync_user.app_id, | |
| 883 | 886 | sync_user.user_id, | |
| 884 | 887 | constants::SYNCKIT_ROTATION_STALE_HOURS, |