diff --git a/crates/adapters/sqlite/.sqlx/query-32a9f3382874860eb5382c5bb6ef08dbfb4ff01e052d88812ae65bb30600388c.json b/crates/adapters/sqlite/.sqlx/query-32a9f3382874860eb5382c5bb6ef08dbfb4ff01e052d88812ae65bb30600388c.json new file mode 100644 index 0000000..6465da0 --- /dev/null +++ b/crates/adapters/sqlite/.sqlx/query-32a9f3382874860eb5382c5bb6ef08dbfb4ff01e052d88812ae65bb30600388c.json @@ -0,0 +1,44 @@ +{ + "db_name": "SQLite", + "query": "SELECT id, email, password_hash, role, created_at FROM users WHERE email = ?", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "email", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "password_hash", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "role", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "created_at", + "ordinal": 4, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false + ] + }, + "hash": "32a9f3382874860eb5382c5bb6ef08dbfb4ff01e052d88812ae65bb30600388c" +} diff --git a/crates/adapters/sqlite/.sqlx/query-73ffdf5be39aa5c4c160c2f77d6634a6970eeb4e1d3395f045ded747f0ce9d2a.json b/crates/adapters/sqlite/.sqlx/query-73ffdf5be39aa5c4c160c2f77d6634a6970eeb4e1d3395f045ded747f0ce9d2a.json new file mode 100644 index 0000000..3427c65 --- /dev/null +++ b/crates/adapters/sqlite/.sqlx/query-73ffdf5be39aa5c4c160c2f77d6634a6970eeb4e1d3395f045ded747f0ce9d2a.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM users WHERE id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "73ffdf5be39aa5c4c160c2f77d6634a6970eeb4e1d3395f045ded747f0ce9d2a" +} diff --git a/crates/adapters/sqlite/.sqlx/query-99f8b694a94f0e01b788cc1c3a2e2ee54ba6e843139de462c8327b084abf151f.json b/crates/adapters/sqlite/.sqlx/query-99f8b694a94f0e01b788cc1c3a2e2ee54ba6e843139de462c8327b084abf151f.json new file mode 100644 index 0000000..f846249 --- /dev/null +++ b/crates/adapters/sqlite/.sqlx/query-99f8b694a94f0e01b788cc1c3a2e2ee54ba6e843139de462c8327b084abf151f.json @@ -0,0 +1,44 @@ +{ + "db_name": "SQLite", + "query": "SELECT id, email, password_hash, role, created_at FROM users WHERE id = ?", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "email", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "password_hash", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "role", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "created_at", + "ordinal": 4, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false + ] + }, + "hash": "99f8b694a94f0e01b788cc1c3a2e2ee54ba6e843139de462c8327b084abf151f" +} diff --git a/crates/adapters/sqlite/.sqlx/query-d20be9ee2cb28025aaa1fd644cda14d209c76269538686dd6e0818922c386dc1.json b/crates/adapters/sqlite/.sqlx/query-d20be9ee2cb28025aaa1fd644cda14d209c76269538686dd6e0818922c386dc1.json new file mode 100644 index 0000000..76d55a7 --- /dev/null +++ b/crates/adapters/sqlite/.sqlx/query-d20be9ee2cb28025aaa1fd644cda14d209c76269538686dd6e0818922c386dc1.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO users (id, email, password_hash, role, created_at)\n VALUES (?, ?, ?, ?, ?)\n ON CONFLICT(id) DO UPDATE SET\n email = excluded.email,\n password_hash = excluded.password_hash,\n role = excluded.role", + "describe": { + "columns": [], + "parameters": { + "Right": 5 + }, + "nullable": [] + }, + "hash": "d20be9ee2cb28025aaa1fd644cda14d209c76269538686dd6e0818922c386dc1" +} diff --git a/crates/bootstrap/Cargo.toml b/crates/bootstrap/Cargo.toml new file mode 100644 index 0000000..1325656 --- /dev/null +++ b/crates/bootstrap/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "bootstrap" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "server" +path = "src/main.rs" + +[dependencies] +domain = { workspace = true } +application = { workspace = true } +adapters-auth = { workspace = true } +presentation = { workspace = true } +adapters-sqlite = { path = "../adapters/sqlite" } +tokio = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +dotenvy = { workspace = true } +tower-http = { workspace = true } +axum = { workspace = true } diff --git a/crates/bootstrap/src/config.rs b/crates/bootstrap/src/config.rs new file mode 100644 index 0000000..6b8ee02 --- /dev/null +++ b/crates/bootstrap/src/config.rs @@ -0,0 +1,28 @@ +#[derive(Debug, Clone)] +pub struct Config { + pub host: String, + pub port: u16, + pub database_url: String, + pub jwt_secret: String, + pub cors_allowed_origins: Vec, +} + +impl Config { + pub fn from_env() -> Self { + dotenvy::dotenv().ok(); + Self { + host: std::env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string()), + port: std::env::var("PORT") + .ok() + .and_then(|p| p.parse().ok()) + .unwrap_or(3000), + database_url: std::env::var("DATABASE_URL").expect("DATABASE_URL must be set"), + jwt_secret: std::env::var("JWT_SECRET").expect("JWT_SECRET must be set"), + cors_allowed_origins: std::env::var("CORS_ALLOWED_ORIGINS") + .unwrap_or_else(|_| "http://localhost:3000".to_string()) + .split(',') + .map(|s| s.trim().to_string()) + .collect(), + } + } +} diff --git a/crates/bootstrap/src/factory.rs b/crates/bootstrap/src/factory.rs new file mode 100644 index 0000000..737d43e --- /dev/null +++ b/crates/bootstrap/src/factory.rs @@ -0,0 +1,43 @@ +// If you chose postgres at cargo generate time, replace adapters_sqlite with +// adapters_postgres throughout this file (connect, run_migrations, PostgresUserRepository). +use std::sync::Arc; +use anyhow::Result; +use axum::Router; +use axum::http::HeaderValue; +use tower_http::{cors::{Any, CorsLayer}, trace::TraceLayer}; + +use adapters_auth::{BcryptPasswordHasher, JwtTokenIssuer}; +use adapters_sqlite::{connect, run_migrations, SqliteUserRepository}; +use application::use_cases::{GetProfile, LoginUser, RegisterUser}; +use presentation::{routes::app_router, state::AppState}; + +use crate::config::Config; + +pub async fn build_app(config: &Config) -> Result { + let pool = connect(&config.database_url).await?; + run_migrations(&pool).await?; + + let user_repo = Arc::new(SqliteUserRepository::new(pool)); + let hasher = Arc::new(BcryptPasswordHasher); + let issuer = Arc::new(JwtTokenIssuer::new(&config.jwt_secret)); + + let register_uc = Arc::new(RegisterUser::new(user_repo.clone(), hasher.clone())); + let login_uc = Arc::new(LoginUser::new(user_repo.clone(), hasher, issuer.clone())); + let get_profile_uc = Arc::new(GetProfile::new(user_repo)); + + let state = AppState::new(register_uc, login_uc, get_profile_uc, issuer); + + let cors = CorsLayer::new() + .allow_origin( + config.cors_allowed_origins.iter() + .filter_map(|o| o.parse::().ok()) + .collect::>(), + ) + .allow_methods(Any) + .allow_headers(Any); + + Ok(app_router() + .with_state(state) + .layer(TraceLayer::new_for_http()) + .layer(cors)) +} diff --git a/crates/bootstrap/src/lib.rs b/crates/bootstrap/src/lib.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/bootstrap/src/main.rs b/crates/bootstrap/src/main.rs new file mode 100644 index 0000000..85117f1 --- /dev/null +++ b/crates/bootstrap/src/main.rs @@ -0,0 +1,28 @@ +use std::net::SocketAddr; +use tracing::info; + +mod config; +mod factory; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::from_default_env() + .add_directive("bootstrap=info".parse()?) + .add_directive("tower_http=debug".parse()?), + ) + .init(); + + let config = config::Config::from_env(); + let app = factory::build_app(&config).await?; + + let addr: SocketAddr = format!("{}:{}", config.host, config.port).parse()?; + let listener = tokio::net::TcpListener::bind(addr).await?; + + info!("🚀 Server running at http://{addr}"); + info!("📖 Scalar docs at http://{addr}/scalar"); + + axum::serve(listener, app).await?; + Ok(()) +} diff --git a/migrations_sqlite/20240101000000_init_users.sql b/migrations_sqlite/20240101000000_init_users.sql index 25dcbdb..bb68532 100644 --- a/migrations_sqlite/20240101000000_init_users.sql +++ b/migrations_sqlite/20240101000000_init_users.sql @@ -1,11 +1,10 @@ -- Create users table CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY NOT NULL, - subject TEXT NOT NULL, email TEXT NOT NULL, - password_hash TEXT, + password_hash TEXT NOT NULL, + role TEXT NOT NULL DEFAULT 'user', created_at TEXT NOT NULL ); -CREATE UNIQUE INDEX IF NOT EXISTS idx_users_subject ON users(subject); CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON users(email);