feat: add image upload for avatar and banner

This commit is contained in:
2026-05-24 02:06:47 +02:00
parent 1874954ad7
commit 01932cf337
40 changed files with 1396 additions and 112 deletions

View File

@@ -14,10 +14,12 @@ postgres = { workspace = true }
postgres-search = { workspace = true }
postgres-federation = { workspace = true }
activitypub = { workspace = true }
k-ap = { git = "https://git.gabrielkaszewski.dev/GKaszewski/k-ap.git", tag = "v0.1.2" }
k-ap = { git = "https://git.gabrielkaszewski.dev/GKaszewski/k-ap.git", tag = "v0.1.3" }
nats = { workspace = true }
event-transport = { workspace = true }
auth = { workspace = true }
storage = { workspace = true }
application = { workspace = true }
sqlx = { workspace = true }
async-nats = { workspace = true }
async-trait = { workspace = true }

View File

@@ -11,6 +11,18 @@ pub struct Config {
pub host: String,
pub cors_origins: String,
pub rate_limit: Option<u32>,
// Storage
pub storage_backend: String,
pub storage_path: Option<String>,
pub storage_prefix: String,
pub s3_endpoint: Option<String>,
pub s3_access_key_id: Option<String>,
pub s3_secret_access_key: Option<String>,
pub s3_bucket: Option<String>,
pub s3_region: Option<String>,
// Upload limits
pub upload_max_bytes: usize,
pub upload_allowed_types: Vec<String>,
}
impl Config {
@@ -36,6 +48,23 @@ impl Config {
rate_limit: std::env::var("RATE_LIMIT")
.ok()
.and_then(|v| v.parse().ok()),
storage_backend: std::env::var("STORAGE_BACKEND").unwrap_or_else(|_| "local".into()),
storage_path: std::env::var("STORAGE_PATH").ok(),
storage_prefix: std::env::var("STORAGE_PREFIX").unwrap_or_default(),
s3_endpoint: std::env::var("S3_ENDPOINT").ok(),
s3_access_key_id: std::env::var("S3_ACCESS_KEY_ID").ok(),
s3_secret_access_key: std::env::var("S3_SECRET_ACCESS_KEY").ok(),
s3_bucket: std::env::var("S3_BUCKET").ok(),
s3_region: std::env::var("S3_REGION").ok(),
upload_max_bytes: std::env::var("UPLOAD_MAX_BYTES")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(5 * 1024 * 1024),
upload_allowed_types: std::env::var("UPLOAD_ALLOWED_TYPES")
.unwrap_or_else(|_| "image/jpeg,image/png,image/gif,image/webp,image/avif".into())
.split(',')
.map(|s| s.trim().to_string())
.collect(),
}
}
}

View File

@@ -5,8 +5,10 @@ use async_trait::async_trait;
use sqlx::PgPool;
use std::sync::Arc;
use application::use_cases::profile::UploadConfig;
use storage::{build_store, ObjectStorageAdapter, StorageConfig};
use activitypub::{ApFederationAdapter, ThoughtsObjectHandler};
use k_ap::ActivityPubService;
use auth::ApiKeyServiceImpl;
use domain::{
errors::DomainError,
@@ -14,6 +16,7 @@ use domain::{
ports::{EventPublisher, OutboxWriter},
};
use event_transport::EventPublisherAdapter;
use k_ap::ActivityPubService;
use nats::NatsTransport;
use postgres::activitypub::PgActivityPubRepository;
use postgres::engagement::PgEngagementRepository;
@@ -72,8 +75,7 @@ pub async fn build(cfg: &Config) -> Infrastructure {
};
// 3. ActivityPub federation
let connections_repo =
Arc::new(PgRemoteActorConnectionRepository::new(pool.clone()));
let connections_repo = Arc::new(PgRemoteActorConnectionRepository::new(pool.clone()));
let raw_ap_service = Arc::new(
ActivityPubService::builder(
Arc::new(PostgresFederationRepository::new(pool.clone())),
@@ -98,7 +100,27 @@ pub async fn build(cfg: &Config) -> Infrastructure {
);
let ap_service = Arc::new(ApFederationAdapter::new(raw_ap_service, connections_repo));
// 4. Application state
// 4. Storage adapter
let storage_cfg = StorageConfig {
backend: cfg.storage_backend.clone(),
local_path: cfg.storage_path.clone(),
s3_endpoint: cfg.s3_endpoint.clone(),
s3_access_key_id: cfg.s3_access_key_id.clone(),
s3_secret_access_key: cfg.s3_secret_access_key.clone(),
s3_bucket: cfg.s3_bucket.clone(),
s3_region: cfg.s3_region.clone(),
};
let object_store = build_store(&storage_cfg).expect("Failed to build object store");
let media_adapter: Arc<dyn domain::ports::MediaStore> = Arc::new(
ObjectStorageAdapter::new(object_store, cfg.storage_prefix.clone())
.expect("Failed to create storage adapter"),
);
let upload_config = UploadConfig {
max_bytes: cfg.upload_max_bytes,
allowed_content_types: cfg.upload_allowed_types.clone(),
};
// 5. Application state
let state = AppState {
users: Arc::new(postgres::user::PgUserRepository::new(pool.clone())),
thoughts: Arc::new(postgres::thought::PgThoughtRepository::new(pool.clone())),
@@ -140,6 +162,9 @@ pub async fn build(cfg: &Config) -> Infrastructure {
postgres::api_key::PgApiKeyRepository::new(pool.clone()),
))),
engagement: Arc::new(PgEngagementRepository::new(pool.clone())),
media: media_adapter,
upload_config,
base_url: cfg.base_url.clone(),
};
Infrastructure { state, ap_service }