Misc fixes for blobs and invites

This commit is contained in:
lewis
2025-12-30 18:46:31 +02:00
parent 7be60ea2d1
commit ea55590b6c
11 changed files with 98 additions and 24 deletions
+9 -2
View File
@@ -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)
@@ -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"
}
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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";
+25 -4
View File
@@ -12,6 +12,8 @@
let error = $state<string | null>(null)
let creating = $state(false)
let createdCode = $state<string | null>(null)
let createdCodeCopied = $state(false)
let copiedCode = $state<string | null>(null)
let inviteCodesEnabled = $state<boolean | null>(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)
}
</script>
<div class="page">
@@ -86,7 +101,9 @@
<h3>{$_('inviteCodes.created')}</h3>
<div class="code-display">
<code>{createdCode}</code>
<button class="copy" onclick={() => copyCode(createdCode!)}>{$_('inviteCodes.copy')}</button>
<button class="copy" onclick={copyCreatedCode}>
{createdCodeCopied ? $_('common.copied') : $_('common.copyToClipboard')}
</button>
</div>
<button onclick={dismissCreated}>{$_('common.done')}</button>
</div>
@@ -110,8 +127,12 @@
<li class:disabled={code.disabled} class:used={code.uses.length > 0 && code.available === 0}>
<div class="code-main">
<code>{code.code}</code>
<button class="copy-small" onclick={() => copyCode(code.code)} title={$_('inviteCodes.copy')}>
{$_('inviteCodes.copy')}
<button
class="copy-small"
onclick={() => copyCode(code.code)}
title={copiedCode === code.code ? $_('common.copied') : $_('inviteCodes.copy')}
>
{copiedCode === code.code ? $_('common.copied') : $_('inviteCodes.copy')}
</button>
</div>
<div class="code-meta">
@@ -119,7 +140,7 @@
{#if code.disabled}
<span class="status disabled">{$_('inviteCodes.disabled')}</span>
{:else if code.uses.length > 0}
<span class="status used">{$_('inviteCodes.used', { values: { handle: code.uses[0].usedBy.split(':').pop() } })}</span>
<span class="status used">{$_('inviteCodes.used', { values: { handle: code.uses[0].usedByHandle || code.uses[0].usedBy.split(':').pop() } })}</span>
{:else}
<span class="status available">{$_('inviteCodes.available')}</span>
{/if}
+1 -1
View File
@@ -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;
+3 -9
View File
@@ -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<AppState>,
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 (
+8 -5
View File
@@ -46,14 +46,14 @@ pub struct CreateInviteCodeOutput {
pub async fn create_invite_code(
State(state): State<AppState>,
BearerAuthAdmin(_auth_user): BearerAuthAdmin,
BearerAuthAdmin(auth_user): BearerAuthAdmin,
Json(input): Json<CreateInviteCodeInput>,
) -> 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<AppState>,
BearerAuthAdmin(_auth_user): BearerAuthAdmin,
BearerAuthAdmin(auth_user): BearerAuthAdmin,
Json(input): Json<CreateInviteCodesInput>,
) -> 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<String>,
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()
+2
View File
@@ -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()
+13
View File
@@ -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<usize> = 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)