From c5d262c68fa326803f6f2ca8f64979de21e18c8c Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Thu, 14 May 2026 04:06:17 +0200 Subject: [PATCH] =?UTF-8?q?feat(presentation):=20routes=20and=20main=20?= =?UTF-8?q?=E2=80=94=20Plan=201=20complete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/presentation/Cargo.toml | 1 + crates/presentation/src/lib.rs | 37 +++++++++++++++++++ crates/presentation/src/main.rs | 29 ++++++++++++++- crates/presentation/src/routes.rs | 60 +++++++++++++++++++++++++++++++ 4 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 crates/presentation/src/routes.rs diff --git a/crates/presentation/Cargo.toml b/crates/presentation/Cargo.toml index 6ac1a14..8290c44 100644 --- a/crates/presentation/Cargo.toml +++ b/crates/presentation/Cargo.toml @@ -14,6 +14,7 @@ api-types = { workspace = true } postgres = { workspace = true } auth = { workspace = true } axum = { workspace = true } +sqlx = { workspace = true } tower-http = { workspace = true } tokio = { workspace = true, features = ["full"] } serde = { workspace = true } diff --git a/crates/presentation/src/lib.rs b/crates/presentation/src/lib.rs index e53c3f9..e757005 100644 --- a/crates/presentation/src/lib.rs +++ b/crates/presentation/src/lib.rs @@ -1,4 +1,41 @@ pub mod errors; pub mod extractors; pub mod handlers; +pub mod routes; pub mod state; + +use std::sync::Arc; +use sqlx::PgPool; +use state::AppState; + +use async_trait::async_trait; +use domain::{errors::DomainError, events::DomainEvent, ports::EventPublisher}; + +struct NoOpEventPublisher; + +#[async_trait] +impl EventPublisher for NoOpEventPublisher { + async fn publish(&self, _e: &DomainEvent) -> Result<(), DomainError> { + Ok(()) + } +} + +pub fn build_state(pool: PgPool, jwt_secret: String) -> AppState { + AppState { + users: Arc::new(postgres::user::PgUserRepository::new(pool.clone())), + thoughts: Arc::new(postgres::thought::PgThoughtRepository::new(pool.clone())), + likes: Arc::new(postgres::like::PgLikeRepository::new(pool.clone())), + boosts: Arc::new(postgres::boost::PgBoostRepository::new(pool.clone())), + follows: Arc::new(postgres::follow::PgFollowRepository::new(pool.clone())), + blocks: Arc::new(postgres::block::PgBlockRepository::new(pool.clone())), + tags: Arc::new(postgres::tag::PgTagRepository::new(pool.clone())), + api_keys: Arc::new(postgres::api_key::PgApiKeyRepository::new(pool.clone())), + top_friends: Arc::new(postgres::top_friend::PgTopFriendRepository::new(pool.clone())), + notifications: Arc::new(postgres::notification::PgNotificationRepository::new(pool.clone())), + remote_actors: Arc::new(postgres::remote_actor::PgRemoteActorRepository::new(pool.clone())), + feed: Arc::new(postgres::feed::PgFeedRepository::new(pool.clone())), + auth: Arc::new(auth::JwtAuthService::new(jwt_secret, 86400 * 30)), + hasher: Arc::new(auth::Argon2PasswordHasher), + events: Arc::new(NoOpEventPublisher), + } +} diff --git a/crates/presentation/src/main.rs b/crates/presentation/src/main.rs index f328e4d..d29e32d 100644 --- a/crates/presentation/src/main.rs +++ b/crates/presentation/src/main.rs @@ -1 +1,28 @@ -fn main() {} +use sqlx::PgPool; +use tower_http::cors::CorsLayer; +use tracing_subscriber::EnvFilter; + +#[tokio::main] +async fn main() { + dotenvy::dotenv().ok(); + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .init(); + + let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL required"); + let jwt_secret = std::env::var("JWT_SECRET").expect("JWT_SECRET required"); + let port = std::env::var("PORT").unwrap_or_else(|_| "3000".into()); + + let pool = PgPool::connect(&database_url).await.expect("DB connect failed"); + sqlx::migrate!("../adapters/postgres/migrations").run(&pool).await.expect("Migrations failed"); + + let state = presentation::build_state(pool, jwt_secret); + let app = presentation::routes::router() + .with_state(state) + .layer(CorsLayer::permissive()); + + let addr = format!("0.0.0.0:{port}"); + tracing::info!("Listening on {addr}"); + let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); + axum::serve(listener, app).await.unwrap(); +} diff --git a/crates/presentation/src/routes.rs b/crates/presentation/src/routes.rs new file mode 100644 index 0000000..b449bf6 --- /dev/null +++ b/crates/presentation/src/routes.rs @@ -0,0 +1,60 @@ +use axum::{ + routing::{delete, get, patch, post, put}, + Router, +}; +use crate::{handlers::*, state::AppState}; + +pub fn router() -> Router { + Router::new() + // auth + .route("/auth/register", post(auth::post_register)) + .route("/auth/login", post(auth::post_login)) + // users — static paths before parameterised + .route("/users/me", patch(users::patch_profile)) + .route("/users/me/top-friends", put(social::put_top_friends)) + .route("/users/{username}", get(users::get_user)) + .route("/users/{username}/following", get(feed::get_following_handler)) + .route("/users/{username}/followers", get(feed::get_followers_handler)) + .route("/users/{username}/top-friends", get(social::get_top_friends_handler)) + // follows & blocks (use {id} param) + .route( + "/users/{id}/follow", + post(social::post_follow).delete(social::delete_follow), + ) + .route( + "/users/{id}/block", + post(social::post_block).delete(social::delete_block), + ) + // thoughts + .route("/thoughts", post(thoughts::post_thought)) + .route( + "/thoughts/{id}", + get(thoughts::get_thought_handler) + .patch(thoughts::patch_thought) + .delete(thoughts::delete_thought_handler), + ) + .route("/thoughts/{id}/thread", get(thoughts::get_thread_handler)) + // likes & boosts + .route( + "/thoughts/{id}/like", + post(social::post_like).delete(social::delete_like), + ) + .route( + "/thoughts/{id}/boost", + post(social::post_boost).delete(social::delete_boost), + ) + // feeds + .route("/feed", get(feed::home_feed)) + .route("/feed/public", get(feed::public_feed)) + .route("/search", get(feed::search_handler)) + // notifications + .route("/notifications", get(notifications::list_notifications)) + .route("/notifications/read-all", post(notifications::mark_all_read)) + .route("/notifications/{id}/read", post(notifications::mark_notification_read)) + // api keys + .route( + "/api-keys", + get(api_keys::get_api_keys).post(api_keys::post_api_key), + ) + .route("/api-keys/{id}", delete(api_keys::delete_api_key_handler)) +}