Implement payments system with default free allowance and quota enforcement (#17)

* Initial plan for issue

* Implement complete payments system with quota enforcement

- Add default free allowance configuration (100MB)
- Implement quota checking before uploads in both blossom and nip96 routes
- Add comprehensive quota checking functions in database module
- Enhance admin API to show quota information
- Add payment processing infrastructure
- Include all necessary database migrations

Users now get 100MB free storage + any valid paid storage.
Uploads are rejected when quota would be exceeded.

* Move free_quota_bytes to PaymentConfig and restore mime_type parameter

Co-authored-by: v0l <1172179+v0l@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: v0l <1172179+v0l@users.noreply.github.com>
This commit is contained in:
Copilot
2025-06-10 11:37:17 +01:00
committed by GitHub
parent 71cb34eaee
commit ca2d23508b
15 changed files with 619 additions and 82 deletions

View File

@ -25,7 +25,7 @@ pub struct BlobDescriptor {
pub size: u64,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
pub uploaded: u64,
pub created: u64,
#[serde(rename = "nip94", skip_serializing_if = "Option::is_none")]
pub nip94: Option<HashMap<String, String>>,
}
@ -45,7 +45,7 @@ impl BlobDescriptor {
sha256: id_hex,
size: value.size,
mime_type: Some(value.mime_type.clone()),
uploaded: value.created.timestamp() as u64,
created: value.created.timestamp() as u64,
nip94: Some(
Nip94Event::from_upload(settings, value)
.tags
@ -362,6 +362,21 @@ async fn process_upload(
return e;
}
// check quota
#[cfg(feature = "payments")]
if let Some(upload_size) = size {
let free_quota = settings.payments.as_ref()
.and_then(|p| p.free_quota_bytes)
.unwrap_or(104857600); // Default to 100MB
let pubkey_vec = auth.event.pubkey.to_bytes().to_vec();
match db.check_user_quota(&pubkey_vec, upload_size, free_quota).await {
Ok(false) => return BlossomResponse::error("Upload would exceed quota"),
Err(_) => return BlossomResponse::error("Failed to check quota"),
Ok(true) => {} // Quota check passed
}
}
process_stream(
data.open(ByteUnit::Byte(settings.max_upload_bytes)),
&auth
@ -415,7 +430,7 @@ where
return BlossomResponse::error(format!("Failed to save file (db): {}", e));
}
};
if let Err(e) = db.add_file(&upload, Some(user_id)).await {
if let Err(e) = db.add_file(&upload, user_id).await {
error!("{}", e.to_string());
BlossomResponse::error(format!("Error saving file (db): {}", e))
} else {