mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-09-04 01:06:55 +00:00
Misc fixes for blobs and invites
This commit is contained in:
+9
-2
@@ -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)
|
||||
|
||||
+34
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user