this repo has no description

Misc fixes for blobs and invites

lewis ed387916 114cc8a8

+9 -2
.env.example
··· 82 82 # SIGNAL_CLI_PATH=/usr/local/bin/signal-cli 83 83 # SIGNAL_SENDER_NUMBER=+1234567890 84 84 # ============================================================================= 85 + # Upload Limits 86 + # ============================================================================= 87 + # Maximum blob/body size in bytes (default: 10GB) 88 + # This controls both the Axum body limit and blob upload limits. 89 + # Make sure your nginx client_max_body_size matches or exceeds this value. 90 + # MAX_BLOB_SIZE=10737418240 91 + # ============================================================================= 85 92 # Repository Import 86 93 # ============================================================================= 87 94 # Set to "true" to accept repository imports 88 95 # ACCEPTING_REPO_IMPORTS=false 89 - # Maximum import size in bytes (default: 50MB) 90 - # MAX_IMPORT_SIZE=52428800 96 + # Maximum import size in bytes (default: 100MB) 97 + # MAX_IMPORT_SIZE=104857600 91 98 # Maximum blocks per import (default: 100000) 92 99 # MAX_IMPORT_BLOCKS=100000 93 100 # Skip verification during import (testing only)
+34
.sqlx/query-6a3a5d1d2cf871652a9d4d8ddb79cf26d24d9acb67e48123ca98423502eaac47.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "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 ", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "did", 9 + "type_info": "Text" 10 + }, 11 + { 12 + "ordinal": 1, 13 + "name": "handle", 14 + "type_info": "Text" 15 + }, 16 + { 17 + "ordinal": 2, 18 + "name": "used_at", 19 + "type_info": "Timestamptz" 20 + } 21 + ], 22 + "parameters": { 23 + "Left": [ 24 + "Text" 25 + ] 26 + }, 27 + "nullable": [ 28 + false, 29 + false, 30 + false 31 + ] 32 + }, 33 + "hash": "6a3a5d1d2cf871652a9d4d8ddb79cf26d24d9acb67e48123ca98423502eaac47" 34 + }
+1 -1
README.md
··· 8 8 9 9 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. 10 10 11 - Another excellent PDS is [Cocoon](https://github.com/haileyok/cocoon), written in go. 11 + Another excellent PDS is [Cocoon](https://tangled.org/hailey.at/cocoon), written in go. 12 12 13 13 ## What's different about Tranquil PDS 14 14
+1 -1
deploy/nginx/nginx-quadlet.conf
··· 33 33 server_name _; 34 34 ssl_certificate /etc/nginx/certs/fullchain.pem; 35 35 ssl_certificate_key /etc/nginx/certs/privkey.pem; 36 - client_max_body_size 100M; 36 + client_max_body_size 10G; 37 37 location / { 38 38 proxy_pass http://127.0.0.1:3000; 39 39 proxy_http_version 1.1;
+1 -1
frontend/src/lib/api.ts
··· 105 105 forAccount: string; 106 106 createdBy: string; 107 107 createdAt: string; 108 - uses: { usedBy: string; usedAt: string }[]; 108 + uses: { usedBy: string; usedByHandle?: string; usedAt: string }[]; 109 109 } 110 110 111 111 export type VerificationChannel = "email" | "discord" | "telegram" | "signal";
+25 -4
frontend/src/routes/InviteCodes.svelte
··· 12 12 let error = $state<string | null>(null) 13 13 let creating = $state(false) 14 14 let createdCode = $state<string | null>(null) 15 + let createdCodeCopied = $state(false) 16 + let copiedCode = $state<string | null>(null) 15 17 let inviteCodesEnabled = $state<boolean | null>(null) 16 18 17 19 onMount(async () => { ··· 65 67 } 66 68 function dismissCreated() { 67 69 createdCode = null 70 + createdCodeCopied = false 71 + } 72 + function copyCreatedCode() { 73 + if (createdCode) { 74 + navigator.clipboard.writeText(createdCode) 75 + createdCodeCopied = true 76 + } 68 77 } 69 78 function copyCode(code: string) { 70 79 navigator.clipboard.writeText(code) 80 + copiedCode = code 81 + setTimeout(() => { 82 + if (copiedCode === code) { 83 + copiedCode = null 84 + } 85 + }, 2000) 71 86 } 72 87 </script> 73 88 <div class="page"> ··· 86 101 <h3>{$_('inviteCodes.created')}</h3> 87 102 <div class="code-display"> 88 103 <code>{createdCode}</code> 89 - <button class="copy" onclick={() => copyCode(createdCode!)}>{$_('inviteCodes.copy')}</button> 104 + <button class="copy" onclick={copyCreatedCode}> 105 + {createdCodeCopied ? $_('common.copied') : $_('common.copyToClipboard')} 106 + </button> 90 107 </div> 91 108 <button onclick={dismissCreated}>{$_('common.done')}</button> 92 109 </div> ··· 110 127 <li class:disabled={code.disabled} class:used={code.uses.length > 0 && code.available === 0}> 111 128 <div class="code-main"> 112 129 <code>{code.code}</code> 113 - <button class="copy-small" onclick={() => copyCode(code.code)} title={$_('inviteCodes.copy')}> 114 - {$_('inviteCodes.copy')} 130 + <button 131 + class="copy-small" 132 + onclick={() => copyCode(code.code)} 133 + title={copiedCode === code.code ? $_('common.copied') : $_('inviteCodes.copy')} 134 + > 135 + {copiedCode === code.code ? $_('common.copied') : $_('inviteCodes.copy')} 115 136 </button> 116 137 </div> 117 138 <div class="code-meta"> ··· 119 140 {#if code.disabled} 120 141 <span class="status disabled">{$_('inviteCodes.disabled')}</span> 121 142 {:else if code.uses.length > 0} 122 - <span class="status used">{$_('inviteCodes.used', { values: { handle: code.uses[0].usedBy.split(':').pop() } })}</span> 143 + <span class="status used">{$_('inviteCodes.used', { values: { handle: code.uses[0].usedByHandle || code.uses[0].usedBy.split(':').pop() } })}</span> 123 144 {:else} 124 145 <span class="status available">{$_('inviteCodes.available')}</span> 125 146 {/if}
+1 -1
nginx.prod.conf
··· 55 55 server_name _; 56 56 ssl_certificate /etc/nginx/certs/live/${PDS_HOSTNAME}/fullchain.pem; 57 57 ssl_certificate_key /etc/nginx/certs/live/${PDS_HOSTNAME}/privkey.pem; 58 - client_max_body_size 100M; 58 + client_max_body_size 10G; 59 59 location / { 60 60 proxy_pass http://tranquil-pds; 61 61 proxy_http_version 1.1;
+3 -9
src/api/repo/blob.rs
··· 1 1 use crate::auth::{ServiceTokenVerifier, is_service_token}; 2 2 use crate::delegation::{self, DelegationActionType}; 3 3 use crate::state::AppState; 4 + use crate::util::get_max_blob_size; 4 5 use axum::body::Bytes; 5 6 use axum::{ 6 7 Json, ··· 14 15 use serde_json::json; 15 16 use sha2::{Digest, Sha256}; 16 17 use tracing::{debug, error}; 17 - 18 - const MAX_BLOB_SIZE: usize = 10_000_000_000; 19 - const MAX_VIDEO_BLOB_SIZE: usize = 10_000_000_000; 20 18 21 19 pub async fn upload_blob( 22 20 State(state): State<AppState>, ··· 38 36 39 37 let is_service_auth = is_service_token(&token); 40 38 41 - let (did, is_migration, controller_did) = if is_service_auth { 39 + let (did, _is_migration, controller_did) = if is_service_auth { 42 40 debug!("Verifying service token for blob upload"); 43 41 let verifier = ServiceTokenVerifier::new(); 44 42 match verifier ··· 94 92 } 95 93 }; 96 94 97 - let max_size = if is_service_auth || is_migration { 98 - MAX_VIDEO_BLOB_SIZE 99 - } else { 100 - MAX_BLOB_SIZE 101 - }; 95 + let max_size = get_max_blob_size(); 102 96 103 97 if body.len() > max_size { 104 98 return (
+8 -5
src/api/server/invite.rs
··· 46 46 47 47 pub async fn create_invite_code( 48 48 State(state): State<AppState>, 49 - BearerAuthAdmin(_auth_user): BearerAuthAdmin, 49 + BearerAuthAdmin(auth_user): BearerAuthAdmin, 50 50 Json(input): Json<CreateInviteCodeInput>, 51 51 ) -> Response { 52 52 if input.use_count < 1 { 53 53 return ApiError::InvalidRequest("useCount must be at least 1".into()).into_response(); 54 54 } 55 55 56 - let for_account = input.for_account.unwrap_or_else(|| "admin".to_string()); 56 + let for_account = input.for_account.unwrap_or_else(|| auth_user.did.clone()); 57 57 let code = gen_invite_code(); 58 58 59 59 match sqlx::query!( ··· 101 101 102 102 pub async fn create_invite_codes( 103 103 State(state): State<AppState>, 104 - BearerAuthAdmin(_auth_user): BearerAuthAdmin, 104 + BearerAuthAdmin(auth_user): BearerAuthAdmin, 105 105 Json(input): Json<CreateInviteCodesInput>, 106 106 ) -> Response { 107 107 if input.use_count < 1 { ··· 112 112 let for_accounts = input 113 113 .for_accounts 114 114 .filter(|v| !v.is_empty()) 115 - .unwrap_or_else(|| vec!["admin".to_string()]); 115 + .unwrap_or_else(|| vec![auth_user.did.clone()]); 116 116 117 117 let admin_user_id = match sqlx::query_scalar!( 118 118 "SELECT id FROM users WHERE is_admin = true LIMIT 1" ··· 184 184 #[serde(rename_all = "camelCase")] 185 185 pub struct InviteCodeUse { 186 186 pub used_by: String, 187 + #[serde(skip_serializing_if = "Option::is_none")] 188 + pub used_by_handle: Option<String>, 187 189 pub used_at: String, 188 190 } 189 191 ··· 238 240 239 241 let uses = sqlx::query!( 240 242 r#" 241 - SELECT u.did, icu.used_at 243 + SELECT u.did, u.handle, icu.used_at 242 244 FROM invite_code_uses icu 243 245 JOIN users u ON icu.used_by_user = u.id 244 246 WHERE icu.code = $1 ··· 253 255 .iter() 254 256 .map(|u| InviteCodeUse { 255 257 used_by: u.did.clone(), 258 + used_by_handle: Some(u.handle.clone()), 256 259 used_at: u.used_at.to_rfc3339(), 257 260 }) 258 261 .collect()
+2
src/lib.rs
··· 24 24 25 25 use axum::{ 26 26 Router, 27 + extract::DefaultBodyLimit, 27 28 http::Method, 28 29 middleware, 29 30 routing::{any, get, post}, ··· 618 619 post(api::delegation::create_delegated_account), 619 620 ) 620 621 .route("/xrpc/{*method}", any(api::proxy::proxy_handler)) 622 + .layer(DefaultBodyLimit::max(util::get_max_blob_size())) 621 623 .layer(middleware::from_fn(metrics::metrics_middleware)) 622 624 .layer( 623 625 CorsLayer::new()
+13
src/util.rs
··· 1 1 use axum::http::HeaderMap; 2 2 use rand::Rng; 3 3 use sqlx::PgPool; 4 + use std::sync::OnceLock; 4 5 use uuid::Uuid; 5 6 6 7 const BASE32_ALPHABET: &str = "abcdefghijklmnopqrstuvwxyz234567"; 8 + const DEFAULT_MAX_BLOB_SIZE: usize = 10 * 1024 * 1024 * 1024; 9 + 10 + static MAX_BLOB_SIZE: OnceLock<usize> = OnceLock::new(); 11 + 12 + pub fn get_max_blob_size() -> usize { 13 + *MAX_BLOB_SIZE.get_or_init(|| { 14 + std::env::var("MAX_BLOB_SIZE") 15 + .ok() 16 + .and_then(|s| s.parse().ok()) 17 + .unwrap_or(DEFAULT_MAX_BLOB_SIZE) 18 + }) 19 + } 7 20 8 21 pub fn generate_token_code() -> String { 9 22 generate_token_code_parts(2, 5)