From ea55590b6c3f034594fd5d3f5b5ad6dcba7feca1 Mon Sep 17 00:00:00 2001 From: lewis Date: Tue, 30 Dec 2025 18:46:31 +0200 Subject: [PATCH] Misc fixes for blobs and invites --- .env.example | 11 ++++-- ...9cf26d24d9acb67e48123ca98423502eaac47.json | 34 +++++++++++++++++++ README.md | 2 +- deploy/nginx/nginx-quadlet.conf | 2 +- frontend/src/lib/api.ts | 2 +- frontend/src/routes/InviteCodes.svelte | 29 +++++++++++++--- nginx.prod.conf | 2 +- src/api/repo/blob.rs | 12 ++----- src/api/server/invite.rs | 13 ++++--- src/lib.rs | 2 ++ src/util.rs | 13 +++++++ 11 files changed, 98 insertions(+), 24 deletions(-) create mode 100644 .sqlx/query-6a3a5d1d2cf871652a9d4d8ddb79cf26d24d9acb67e48123ca98423502eaac47.json diff --git a/.env.example b/.env.example index 5b0126b..4187515 100644 --- a/.env.example +++ b/.env.example @@ -82,12 +82,19 @@ AWS_SECRET_ACCESS_KEY=minioadmin # SIGNAL_CLI_PATH=/usr/local/bin/signal-cli # SIGNAL_SENDER_NUMBER=+1234567890 # ============================================================================= +# Upload Limits +# ============================================================================= +# Maximum blob/body size in bytes (default: 10GB) +# This controls both the Axum body limit and blob upload limits. +# Make sure your nginx client_max_body_size matches or exceeds this value. +# MAX_BLOB_SIZE=10737418240 +# ============================================================================= # Repository Import # ============================================================================= # Set to "true" to accept repository imports # ACCEPTING_REPO_IMPORTS=false -# Maximum import size in bytes (default: 50MB) -# MAX_IMPORT_SIZE=52428800 +# Maximum import size in bytes (default: 100MB) +# MAX_IMPORT_SIZE=104857600 # Maximum blocks per import (default: 100000) # MAX_IMPORT_BLOCKS=100000 # Skip verification during import (testing only) diff --git a/.sqlx/query-6a3a5d1d2cf871652a9d4d8ddb79cf26d24d9acb67e48123ca98423502eaac47.json b/.sqlx/query-6a3a5d1d2cf871652a9d4d8ddb79cf26d24d9acb67e48123ca98423502eaac47.json new file mode 100644 index 0000000..875b427 --- /dev/null +++ b/.sqlx/query-6a3a5d1d2cf871652a9d4d8ddb79cf26d24d9acb67e48123ca98423502eaac47.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT u.did, u.handle, icu.used_at\n FROM invite_code_uses icu\n JOIN users u ON icu.used_by_user = u.id\n WHERE icu.code = $1\n ORDER BY icu.used_at DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "did", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "handle", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "used_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "6a3a5d1d2cf871652a9d4d8ddb79cf26d24d9acb67e48123ca98423502eaac47" +} diff --git a/README.md b/README.md index 7451e70..9775fd7 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This particular PDS thrives under harsh conditions. It is a dandelion growing th It has full compatibility with Bluesky's reference PDS: same endpoints, same behavior, same client compatibility. Everything works: repo operations, blob storage, firehose, OAuth, handle resolution, account migration, the lot. -Another excellent PDS is [Cocoon](https://github.com/haileyok/cocoon), written in go. +Another excellent PDS is [Cocoon](https://tangled.org/hailey.at/cocoon), written in go. ## What's different about Tranquil PDS diff --git a/deploy/nginx/nginx-quadlet.conf b/deploy/nginx/nginx-quadlet.conf index 7a91421..03db9b3 100644 --- a/deploy/nginx/nginx-quadlet.conf +++ b/deploy/nginx/nginx-quadlet.conf @@ -33,7 +33,7 @@ http { server_name _; ssl_certificate /etc/nginx/certs/fullchain.pem; ssl_certificate_key /etc/nginx/certs/privkey.pem; - client_max_body_size 100M; + client_max_body_size 10G; location / { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 0f2136a..406ab63 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -105,7 +105,7 @@ export interface InviteCode { forAccount: string; createdBy: string; createdAt: string; - uses: { usedBy: string; usedAt: string }[]; + uses: { usedBy: string; usedByHandle?: string; usedAt: string }[]; } export type VerificationChannel = "email" | "discord" | "telegram" | "signal"; diff --git a/frontend/src/routes/InviteCodes.svelte b/frontend/src/routes/InviteCodes.svelte index f7086d7..6f3a408 100644 --- a/frontend/src/routes/InviteCodes.svelte +++ b/frontend/src/routes/InviteCodes.svelte @@ -12,6 +12,8 @@ let error = $state(null) let creating = $state(false) let createdCode = $state(null) + let createdCodeCopied = $state(false) + let copiedCode = $state(null) let inviteCodesEnabled = $state(null) onMount(async () => { @@ -65,9 +67,22 @@ } function dismissCreated() { createdCode = null + createdCodeCopied = false + } + function copyCreatedCode() { + if (createdCode) { + navigator.clipboard.writeText(createdCode) + createdCodeCopied = true + } } function copyCode(code: string) { navigator.clipboard.writeText(code) + copiedCode = code + setTimeout(() => { + if (copiedCode === code) { + copiedCode = null + } + }, 2000) }
@@ -86,7 +101,9 @@

{$_('inviteCodes.created')}

{createdCode} - +
@@ -110,8 +127,12 @@
  • 0 && code.available === 0}>
    {code.code} -
    @@ -119,7 +140,7 @@ {#if code.disabled} {$_('inviteCodes.disabled')} {:else if code.uses.length > 0} - {$_('inviteCodes.used', { values: { handle: code.uses[0].usedBy.split(':').pop() } })} + {$_('inviteCodes.used', { values: { handle: code.uses[0].usedByHandle || code.uses[0].usedBy.split(':').pop() } })} {:else} {$_('inviteCodes.available')} {/if} diff --git a/nginx.prod.conf b/nginx.prod.conf index 9a2a28b..9134bd1 100644 --- a/nginx.prod.conf +++ b/nginx.prod.conf @@ -55,7 +55,7 @@ http { server_name _; ssl_certificate /etc/nginx/certs/live/${PDS_HOSTNAME}/fullchain.pem; ssl_certificate_key /etc/nginx/certs/live/${PDS_HOSTNAME}/privkey.pem; - client_max_body_size 100M; + client_max_body_size 10G; location / { proxy_pass http://tranquil-pds; proxy_http_version 1.1; diff --git a/src/api/repo/blob.rs b/src/api/repo/blob.rs index 68811b3..c53fc87 100644 --- a/src/api/repo/blob.rs +++ b/src/api/repo/blob.rs @@ -1,6 +1,7 @@ use crate::auth::{ServiceTokenVerifier, is_service_token}; use crate::delegation::{self, DelegationActionType}; use crate::state::AppState; +use crate::util::get_max_blob_size; use axum::body::Bytes; use axum::{ Json, @@ -15,9 +16,6 @@ use serde_json::json; use sha2::{Digest, Sha256}; use tracing::{debug, error}; -const MAX_BLOB_SIZE: usize = 10_000_000_000; -const MAX_VIDEO_BLOB_SIZE: usize = 10_000_000_000; - pub async fn upload_blob( State(state): State, headers: axum::http::HeaderMap, @@ -38,7 +36,7 @@ pub async fn upload_blob( let is_service_auth = is_service_token(&token); - let (did, is_migration, controller_did) = if is_service_auth { + let (did, _is_migration, controller_did) = if is_service_auth { debug!("Verifying service token for blob upload"); let verifier = ServiceTokenVerifier::new(); match verifier @@ -94,11 +92,7 @@ pub async fn upload_blob( } }; - let max_size = if is_service_auth || is_migration { - MAX_VIDEO_BLOB_SIZE - } else { - MAX_BLOB_SIZE - }; + let max_size = get_max_blob_size(); if body.len() > max_size { return ( diff --git a/src/api/server/invite.rs b/src/api/server/invite.rs index 031604c..4a1a214 100644 --- a/src/api/server/invite.rs +++ b/src/api/server/invite.rs @@ -46,14 +46,14 @@ pub struct CreateInviteCodeOutput { pub async fn create_invite_code( State(state): State, - BearerAuthAdmin(_auth_user): BearerAuthAdmin, + BearerAuthAdmin(auth_user): BearerAuthAdmin, Json(input): Json, ) -> Response { if input.use_count < 1 { return ApiError::InvalidRequest("useCount must be at least 1".into()).into_response(); } - let for_account = input.for_account.unwrap_or_else(|| "admin".to_string()); + let for_account = input.for_account.unwrap_or_else(|| auth_user.did.clone()); let code = gen_invite_code(); match sqlx::query!( @@ -101,7 +101,7 @@ pub struct AccountCodes { pub async fn create_invite_codes( State(state): State, - BearerAuthAdmin(_auth_user): BearerAuthAdmin, + BearerAuthAdmin(auth_user): BearerAuthAdmin, Json(input): Json, ) -> Response { if input.use_count < 1 { @@ -112,7 +112,7 @@ pub async fn create_invite_codes( let for_accounts = input .for_accounts .filter(|v| !v.is_empty()) - .unwrap_or_else(|| vec!["admin".to_string()]); + .unwrap_or_else(|| vec![auth_user.did.clone()]); let admin_user_id = match sqlx::query_scalar!( "SELECT id FROM users WHERE is_admin = true LIMIT 1" @@ -184,6 +184,8 @@ pub struct InviteCode { #[serde(rename_all = "camelCase")] pub struct InviteCodeUse { pub used_by: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub used_by_handle: Option, pub used_at: String, } @@ -238,7 +240,7 @@ pub async fn get_account_invite_codes( let uses = sqlx::query!( r#" - SELECT u.did, icu.used_at + SELECT u.did, u.handle, icu.used_at FROM invite_code_uses icu JOIN users u ON icu.used_by_user = u.id WHERE icu.code = $1 @@ -253,6 +255,7 @@ pub async fn get_account_invite_codes( .iter() .map(|u| InviteCodeUse { used_by: u.did.clone(), + used_by_handle: Some(u.handle.clone()), used_at: u.used_at.to_rfc3339(), }) .collect() diff --git a/src/lib.rs b/src/lib.rs index 6ded3be..4db4c95 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,6 +24,7 @@ pub mod validation; use axum::{ Router, + extract::DefaultBodyLimit, http::Method, middleware, routing::{any, get, post}, @@ -618,6 +619,7 @@ pub fn app(state: AppState) -> Router { post(api::delegation::create_delegated_account), ) .route("/xrpc/{*method}", any(api::proxy::proxy_handler)) + .layer(DefaultBodyLimit::max(util::get_max_blob_size())) .layer(middleware::from_fn(metrics::metrics_middleware)) .layer( CorsLayer::new() diff --git a/src/util.rs b/src/util.rs index 9b009ca..dfb3d4d 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,9 +1,22 @@ use axum::http::HeaderMap; use rand::Rng; use sqlx::PgPool; +use std::sync::OnceLock; use uuid::Uuid; const BASE32_ALPHABET: &str = "abcdefghijklmnopqrstuvwxyz234567"; +const DEFAULT_MAX_BLOB_SIZE: usize = 10 * 1024 * 1024 * 1024; + +static MAX_BLOB_SIZE: OnceLock = OnceLock::new(); + +pub fn get_max_blob_size() -> usize { + *MAX_BLOB_SIZE.get_or_init(|| { + std::env::var("MAX_BLOB_SIZE") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(DEFAULT_MAX_BLOB_SIZE) + }) +} pub fn generate_token_code() -> String { generate_token_code_parts(2, 5)