diff --git a/Cargo.lock b/Cargo.lock index b1e6fc3..b196c61 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,37 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "api" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "axum", + "axum-login", + "chrono", + "config", + "domain", + "dotenvy", + "infra", + "k-core", + "password-auth", + "serde", + "serde_json", + "sqlx", + "thiserror 2.0.17", + "time", + "tokio", + "tower", + "tower-http", + "tower-sessions", + "tower-sessions-sqlx-store", + "tracing", + "tracing-subscriber", + "uuid", + "validator", +] + [[package]] name = "argon2" version = "0.5.3" @@ -558,6 +589,22 @@ dependencies = [ "const-random", ] +[[package]] +name = "domain" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "chrono", + "futures-core", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tracing", + "uuid", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -1125,6 +1172,28 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "infra" +version = "0.1.0" +dependencies = [ + "async-nats", + "async-trait", + "chrono", + "domain", + "futures-core", + "futures-util", + "k-core", + "serde", + "serde_json", + "sqlx", + "thiserror 2.0.17", + "tokio", + "tower-sessions", + "tower-sessions-sqlx-store", + "tracing", + "uuid", +] + [[package]] name = "itoa" version = "1.0.17" @@ -2401,75 +2470,6 @@ dependencies = [ "syn", ] -[[package]] -name = "template-api" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "axum", - "axum-login", - "chrono", - "config", - "dotenvy", - "k-core", - "password-auth", - "serde", - "serde_json", - "sqlx", - "template-domain", - "template-infra", - "thiserror 2.0.17", - "time", - "tokio", - "tower", - "tower-http", - "tower-sessions", - "tower-sessions-sqlx-store", - "tracing", - "tracing-subscriber", - "uuid", - "validator", -] - -[[package]] -name = "template-domain" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "chrono", - "futures-core", - "serde", - "serde_json", - "thiserror 2.0.17", - "tokio", - "tracing", - "uuid", -] - -[[package]] -name = "template-infra" -version = "0.1.0" -dependencies = [ - "async-nats", - "async-trait", - "chrono", - "futures-core", - "futures-util", - "k-core", - "serde", - "serde_json", - "sqlx", - "template-domain", - "thiserror 2.0.17", - "tokio", - "tower-sessions", - "tower-sessions-sqlx-store", - "tracing", - "uuid", -] - [[package]] name = "thiserror" version = "1.0.69" diff --git a/Cargo.toml b/Cargo.toml index 26bb5f7..045eb21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] -members = ["template-domain", "template-infra", "template-api"] +members = ["domain", "infra", "api"] resolver = "2" diff --git a/Dockerfile b/Dockerfile index e1c08d7..3d34f64 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ WORKDIR /app COPY . . # Build the release binary -RUN cargo build --release -p template-api +RUN cargo build --release -p api FROM debian:bookworm-slim @@ -13,7 +13,7 @@ WORKDIR /app # Install OpenSSL (required for many Rust networking crates) and CA certificates RUN apt-get update && apt-get install -y libssl3 ca-certificates && rm -rf /var/lib/apt/lists/* -COPY --from=builder /app/target/release/template-api . +COPY --from=builder /app/target/release/api . # Create data directory for SQLite @@ -24,4 +24,4 @@ ENV SESSION_SECRET=supersecretchangeinproduction EXPOSE 3000 -CMD ["./template-api"] +CMD ["./api"] diff --git a/template-api/Cargo.toml b/api/Cargo.toml similarity index 82% rename from template-api/Cargo.toml rename to api/Cargo.toml index eb09f63..7ef143a 100644 --- a/template-api/Cargo.toml +++ b/api/Cargo.toml @@ -1,31 +1,31 @@ [package] -name = "template-api" +name = "api" version = "0.1.0" edition = "2024" -default-run = "template-api" +default-run = "api" [features] default = ["sqlite"] sqlite = [ - "template-infra/sqlite", + "infra/sqlite", "tower-sessions-sqlx-store/sqlite", "sqlx/sqlite", ] postgres = [ - "template-infra/postgres", + "infra/postgres", "tower-sessions-sqlx-store/postgres", "sqlx/postgres", "k-core/postgres", ] -broker-nats = ["template-infra/broker-nats"] +broker-nats = ["infra/broker-nats"] [dependencies] k-core = { git = "https://git.gabrielkaszewski.dev/GKaszewski/k-core", features = [ "logging", "db-sqlx", ] } -template-domain = { path = "../template-domain" } -template-infra = { path = "../template-infra", default-features = false, features = [ +domain = { path = "../domain" } +infra = { path = "../infra", default-features = false, features = [ "sqlite", ] } diff --git a/template-api/src/auth.rs b/api/src/auth.rs similarity index 96% rename from template-api/src/auth.rs rename to api/src/auth.rs index cc70cea..a877fbc 100644 --- a/template-api/src/auth.rs +++ b/api/src/auth.rs @@ -3,14 +3,14 @@ use std::sync::Arc; use axum_login::{AuthnBackend, UserId}; +use infra::session_store::InfraSessionStore; use password_auth::verify_password; use serde::{Deserialize, Serialize}; -use uuid::Uuid; -use template_infra::session_store::InfraSessionStore; use tower_sessions::SessionManagerLayer; +use uuid::Uuid; use crate::error::ApiError; -use template_domain::{User, UserRepository}; +use domain::{User, UserRepository}; /// Wrapper around domain User to implement AuthUser #[derive(Debug, Clone, Serialize, Deserialize)] @@ -95,7 +95,7 @@ pub async fn setup_auth_layer( user_repo: Arc, ) -> Result, ApiError> { let backend = AuthBackend::new(user_repo); - + let auth_layer = axum_login::AuthManagerLayerBuilder::new(backend, session_layer).build(); Ok(auth_layer) } diff --git a/template-api/src/config.rs b/api/src/config.rs similarity index 100% rename from template-api/src/config.rs rename to api/src/config.rs diff --git a/template-api/src/dto.rs b/api/src/dto.rs similarity index 100% rename from template-api/src/dto.rs rename to api/src/dto.rs diff --git a/template-api/src/error.rs b/api/src/error.rs similarity index 98% rename from template-api/src/error.rs rename to api/src/error.rs index 8fb0b80..d3f0aff 100644 --- a/template-api/src/error.rs +++ b/api/src/error.rs @@ -3,14 +3,14 @@ //! Maps domain errors to HTTP responses use axum::{ + Json, http::StatusCode, response::{IntoResponse, Response}, - Json, }; use serde::Serialize; use thiserror::Error; -use template_domain::DomainError; +use domain::DomainError; /// API-level errors #[derive(Debug, Error)] diff --git a/template-api/src/main.rs b/api/src/main.rs similarity index 91% rename from template-api/src/main.rs rename to api/src/main.rs index aabbd77..300caa9 100644 --- a/template-api/src/main.rs +++ b/api/src/main.rs @@ -1,10 +1,10 @@ use std::net::SocketAddr; use std::time::Duration as StdDuration; +use domain::UserService; +use infra::factory::build_session_store; +use infra::factory::build_user_repository; use k_core::logging; -use template_domain::UserService; -use template_infra::factory::build_session_store; -use template_infra::factory::build_user_repository; use tokio::net::TcpListener; use tower_sessions::{Expiry, SessionManagerLayer}; use tracing::info; @@ -44,7 +44,7 @@ async fn main() -> anyhow::Result<()> { let db_pool = k_core::db::connect(&db_config).await?; // 4. Run migrations (using the re-export if you kept it, or direct k_core) - template_infra::db::run_migrations(&db_pool).await?; + infra::db::run_migrations(&db_pool).await?; // 5. Initialize Services let user_repo = build_user_repository(&db_pool).await?; diff --git a/template-api/src/routes/auth.rs b/api/src/routes/auth.rs similarity index 57% rename from template-api/src/routes/auth.rs rename to api/src/routes/auth.rs index be1cbab..ee003d7 100644 --- a/template-api/src/routes/auth.rs +++ b/api/src/routes/auth.rs @@ -1,16 +1,17 @@ -use axum::{ - extract::{State, Json}, - response::IntoResponse, - Router, routing::post, -}; use axum::http::StatusCode; +use axum::{ + Router, + extract::{Json, State}, + response::IntoResponse, + routing::post, +}; use crate::{ dto::{LoginRequest, RegisterRequest, UserResponse}, error::ApiError, state::AppState, }; -use template_domain::{DomainError, Email}; +use domain::{DomainError, Email}; pub fn router() -> Router { Router::new() @@ -24,22 +25,31 @@ async fn login( mut auth_session: crate::auth::AuthSession, Json(payload): Json, ) -> Result { - let user = match auth_session.authenticate(crate::auth::Credentials { - email: payload.email, - password: payload.password, - }).await { + let user = match auth_session + .authenticate(crate::auth::Credentials { + email: payload.email, + password: payload.password, + }) + .await + { Ok(Some(user)) => user, Ok(None) => return Err(ApiError::Validation("Invalid credentials".to_string())), Err(_) => return Err(ApiError::Internal("Authentication failed".to_string())), }; - auth_session.login(&user).await.map_err(|_| ApiError::Internal("Login failed".to_string()))?; + auth_session + .login(&user) + .await + .map_err(|_| ApiError::Internal("Login failed".to_string()))?; - Ok((StatusCode::OK, Json(UserResponse { - id: user.0.id, - email: user.0.email.into_inner(), - created_at: user.0.created_at, - }))) + Ok(( + StatusCode::OK, + Json(UserResponse { + id: user.0.id, + email: user.0.email.into_inner(), + created_at: user.0.created_at, + }), + )) } async fn register( @@ -47,28 +57,44 @@ async fn register( mut auth_session: crate::auth::AuthSession, Json(payload): Json, ) -> Result { - if state.user_service.find_by_email(&payload.email).await?.is_some() { - return Err(ApiError::Domain(DomainError::UserAlreadyExists(payload.email))); + if state + .user_service + .find_by_email(&payload.email) + .await? + .is_some() + { + return Err(ApiError::Domain(DomainError::UserAlreadyExists( + payload.email, + ))); } - // Note: In a real app, you would hash the password here. + // Note: In a real app, you would hash the password here. // This template uses a simplified User::new which doesn't take password. // You should extend User to handle passwords or use an OIDC flow. let email = Email::try_from(payload.email).map_err(|e| ApiError::Validation(e.to_string()))?; - + // Using email as subject for local auth for now - let user = state.user_service.find_or_create(&email.as_ref().to_string(), email.as_ref()).await?; - + let user = state + .user_service + .find_or_create(&email.as_ref().to_string(), email.as_ref()) + .await?; + // Log the user in let auth_user = crate::auth::AuthUser(user.clone()); - - auth_session.login(&auth_user).await.map_err(|_| ApiError::Internal("Login failed".to_string()))?; - Ok((StatusCode::CREATED, Json(UserResponse { - id: user.id, - email: user.email.into_inner(), - created_at: user.created_at, - }))) + auth_session + .login(&auth_user) + .await + .map_err(|_| ApiError::Internal("Login failed".to_string()))?; + + Ok(( + StatusCode::CREATED, + Json(UserResponse { + id: user.id, + email: user.email.into_inner(), + created_at: user.created_at, + }), + )) } async fn logout(mut auth_session: crate::auth::AuthSession) -> impl IntoResponse { @@ -79,11 +105,13 @@ async fn logout(mut auth_session: crate::auth::AuthSession) -> impl IntoResponse } async fn me(auth_session: crate::auth::AuthSession) -> Result { - let user = auth_session.user.ok_or(ApiError::Unauthorized("Not logged in".to_string()))?; - + let user = auth_session + .user + .ok_or(ApiError::Unauthorized("Not logged in".to_string()))?; + Ok(Json(UserResponse { id: user.0.id, email: user.0.email.into_inner(), - created_at: user.0.created_at, + created_at: user.0.created_at, })) } diff --git a/template-api/src/routes/config.rs b/api/src/routes/config.rs similarity index 100% rename from template-api/src/routes/config.rs rename to api/src/routes/config.rs diff --git a/template-api/src/routes/mod.rs b/api/src/routes/mod.rs similarity index 100% rename from template-api/src/routes/mod.rs rename to api/src/routes/mod.rs diff --git a/template-api/src/state.rs b/api/src/state.rs similarity index 95% rename from template-api/src/state.rs rename to api/src/state.rs index 8520d57..02b9a85 100644 --- a/template-api/src/state.rs +++ b/api/src/state.rs @@ -6,7 +6,7 @@ use axum::extract::FromRef; use std::sync::Arc; use crate::config::Config; -use template_domain::UserService; +use domain::UserService; #[derive(Clone)] pub struct AppState { diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..0882ceb --- /dev/null +++ b/compose.yml @@ -0,0 +1,50 @@ +services: + backend: + build: . + ports: + - "3000:3000" + environment: + # In production, use a secure secret + - SESSION_SECRET=dev_secret_key_12345 + - DATABASE_URL=sqlite:///app/data/notes.db + - CORS_ALLOWED_ORIGINS=http://localhost:8080,http://localhost:5173 + - HOST=0.0.0.0 + - PORT=3000 + volumes: + - ./data:/app/data + + worker: + build: . + command: ["./notes-worker"] + environment: + - DATABASE_URL=sqlite:///app/data/notes.db + - BROKER_URL=nats://nats:4222 + - QDRANT_URL=http://qdrant:6334 + - EMBEDDING_PROVIDER=fastembed + depends_on: + - backend + - nats + - qdrant + volumes: + - ./data:/app/data + + nats: + image: nats:alpine + ports: + - "4222:4222" + - "6222:6222" + - "8222:8222" + restart: unless-stopped + db: + image: postgres:15-alpine + environment: + POSTGRES_USER: user + POSTGRES_PASSWORD: password + POSTGRES_DB: k_template_db + ports: + - "5432:5432" + volumes: + - db_data:/var/lib/postgresql/data + +volumes: + db_data: diff --git a/template-domain/Cargo.toml b/domain/Cargo.toml similarity index 94% rename from template-domain/Cargo.toml rename to domain/Cargo.toml index 1c6799d..64f497c 100644 --- a/template-domain/Cargo.toml +++ b/domain/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "template-domain" +name = "domain" version = "0.1.0" edition = "2024" diff --git a/template-domain/src/entities.rs b/domain/src/entities.rs similarity index 100% rename from template-domain/src/entities.rs rename to domain/src/entities.rs diff --git a/template-domain/src/errors.rs b/domain/src/errors.rs similarity index 100% rename from template-domain/src/errors.rs rename to domain/src/errors.rs diff --git a/template-domain/src/lib.rs b/domain/src/lib.rs similarity index 100% rename from template-domain/src/lib.rs rename to domain/src/lib.rs diff --git a/template-domain/src/repositories.rs b/domain/src/repositories.rs similarity index 100% rename from template-domain/src/repositories.rs rename to domain/src/repositories.rs diff --git a/template-domain/src/services.rs b/domain/src/services.rs similarity index 100% rename from template-domain/src/services.rs rename to domain/src/services.rs diff --git a/template-domain/src/value_objects.rs b/domain/src/value_objects.rs similarity index 100% rename from template-domain/src/value_objects.rs rename to domain/src/value_objects.rs diff --git a/template-infra/Cargo.toml b/infra/Cargo.toml similarity index 93% rename from template-infra/Cargo.toml rename to infra/Cargo.toml index 6515987..023dab2 100644 --- a/template-infra/Cargo.toml +++ b/infra/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "template-infra" +name = "infra" version = "0.1.0" edition = "2024" @@ -17,7 +17,7 @@ broker-nats = ["dep:async-nats", "dep:futures-util"] k-core = { git = "https://git.gabrielkaszewski.dev/GKaszewski/k-core", features = [ "db-sqlx", ] } -template-domain = { path = "../template-domain" } +domain = { path = "../domain" } async-trait = "0.1.89" chrono = { version = "0.4.42", features = ["serde"] } sqlx = { version = "0.8.6", features = ["runtime-tokio", "chrono", "migrate"] } diff --git a/template-infra/src/db.rs b/infra/src/db.rs similarity index 100% rename from template-infra/src/db.rs rename to infra/src/db.rs diff --git a/template-infra/src/factory.rs b/infra/src/factory.rs similarity index 94% rename from template-infra/src/factory.rs rename to infra/src/factory.rs index 1e45e01..d60c94a 100644 --- a/template-infra/src/factory.rs +++ b/infra/src/factory.rs @@ -3,7 +3,7 @@ use std::sync::Arc; #[cfg(feature = "sqlite")] use crate::SqliteUserRepository; use crate::db::DatabasePool; -use template_domain::UserRepository; +use domain::UserRepository; #[derive(Debug, thiserror::Error)] pub enum FactoryError { @@ -12,7 +12,7 @@ pub enum FactoryError { #[error("Not implemented: {0}")] NotImplemented(String), #[error("Infrastructure error: {0}")] - Infrastructure(#[from] template_domain::DomainError), + Infrastructure(#[from] domain::DomainError), } pub type FactoryResult = Result; diff --git a/template-infra/src/lib.rs b/infra/src/lib.rs similarity index 100% rename from template-infra/src/lib.rs rename to infra/src/lib.rs diff --git a/template-infra/src/session_store.rs b/infra/src/session_store.rs similarity index 100% rename from template-infra/src/session_store.rs rename to infra/src/session_store.rs diff --git a/template-infra/src/user_repository.rs b/infra/src/user_repository.rs similarity index 98% rename from template-infra/src/user_repository.rs rename to infra/src/user_repository.rs index 9feb56b..64919f5 100644 --- a/template-infra/src/user_repository.rs +++ b/infra/src/user_repository.rs @@ -5,7 +5,7 @@ use chrono::{DateTime, Utc}; use sqlx::{FromRow, SqlitePool}; use uuid::Uuid; -use template_domain::{DomainError, DomainResult, Email, User, UserRepository}; +use domain::{DomainError, DomainResult, Email, User, UserRepository}; /// SQLite adapter for UserRepository #[cfg(feature = "sqlite")] @@ -145,7 +145,7 @@ mod tests { use k_core::db::connect; // Import k_core::db::connect async fn setup_test_db() -> SqlitePool { - let config = DatabaseConfig::in_memory(); + let config = DatabaseConfig::default(); // connect returns DatabasePool directly now let db_pool = connect(&config).await.expect("Failed to create pool"); run_migrations(&db_pool).await.unwrap();