feat(bootstrap): config, factory wiring, main entry point
This commit is contained in:
@@ -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"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "DELETE FROM users WHERE id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "73ffdf5be39aa5c4c160c2f77d6634a6970eeb4e1d3395f045ded747f0ce9d2a"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
22
crates/bootstrap/Cargo.toml
Normal file
22
crates/bootstrap/Cargo.toml
Normal file
@@ -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 }
|
||||||
28
crates/bootstrap/src/config.rs
Normal file
28
crates/bootstrap/src/config.rs
Normal file
@@ -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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
crates/bootstrap/src/factory.rs
Normal file
43
crates/bootstrap/src/factory.rs
Normal file
@@ -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<Router> {
|
||||||
|
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::<HeaderValue>().ok())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
.allow_methods(Any)
|
||||||
|
.allow_headers(Any);
|
||||||
|
|
||||||
|
Ok(app_router()
|
||||||
|
.with_state(state)
|
||||||
|
.layer(TraceLayer::new_for_http())
|
||||||
|
.layer(cors))
|
||||||
|
}
|
||||||
0
crates/bootstrap/src/lib.rs
Normal file
0
crates/bootstrap/src/lib.rs
Normal file
28
crates/bootstrap/src/main.rs
Normal file
28
crates/bootstrap/src/main.rs
Normal file
@@ -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(())
|
||||||
|
}
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
-- Create users table
|
-- Create users table
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
id TEXT PRIMARY KEY NOT NULL,
|
id TEXT PRIMARY KEY NOT NULL,
|
||||||
subject TEXT NOT NULL,
|
|
||||||
email 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
|
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);
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||||
|
|||||||
Reference in New Issue
Block a user