Phase 2.3: JWT Authentication implementation

- Implemented JWT-based authentication system with access and refresh tokens
- Added password hashing service using PBKDF2
- Created authentication handlers: register, login, refresh, logout
- Added protected routes with JWT middleware
- Created user profile handlers
- Fixed all compilation errors
- Added integration tests for authentication endpoints
- Added reqwest dependency for testing
- Created test script and environment example documentation

All changes:
- backend/src/auth/: Complete auth module (JWT, password, claims)
- backend/src/handlers/: Auth, users, and health handlers
- backend/src/middleware/: JWT authentication middleware
- backend/src/config/: Added AppState with Clone derive
- backend/src/main.rs: Fixed imports and added auth routes
- backend/src/db/mod.rs: Changed error handling to anyhow::Result
- backend/Cargo.toml: Added reqwest for testing
- backend/tests/auth_tests.rs: Integration tests
- thoughts/: Documentation updates (STATUS.md, env.example, test_auth.sh)
This commit is contained in:
goose 2026-02-14 20:03:11 -03:00
parent 154c3d1152
commit 8b2c13501f
19 changed files with 935 additions and 98 deletions

View file

@ -1,89 +1,75 @@
use axum::{
routing::get,
Router,
response::Json,
extract::State,
};
use serde_json::json;
use tower_http::trace::TraceLayer;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use std::sync::Arc;
mod config;
mod db;
mod models;
mod auth;
mod handlers;
mod middleware;
use axum::{
routing::{get, post},
Router,
middleware as axum_middleware,
};
use tower::ServiceBuilder;
use tower_http::{
cors::CorsLayer,
trace::TraceLayer,
};
use config::Config;
use db::MongoDb;
#[derive(Clone)]
struct AppState {
db: Arc<MongoDb>,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Load configuration
dotenv::dotenv().ok();
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "normogen_backend=debug,tower_http=debug,axum=debug".into())
)
.init();
let config = Config::from_env()?;
// Initialize tracing
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "normogen_backend=debug,tower_http=debug,axum=debug".into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
tracing::info!("Starting Normogen backend server");
tracing::info!("MongoDB URI: {}", config.database.uri);
tracing::info!("Database: {}", config.database.database);
// Connect to MongoDB
let mongodb = MongoDb::new(&config.database.uri, &config.database.database).await?;
tracing::info!("Connected to MongoDB");
// Health check
let health_status = mongodb.health_check().await?;
tracing::info!("MongoDB health: {}", health_status);
let app_state = AppState {
db: Arc::new(mongodb),
tracing::info!("Connecting to MongoDB at {}", config.database.uri);
let db = db::MongoDb::new(&config.database.uri, &config.database.database).await?;
tracing::info!("Connected to MongoDB database: {}", config.database.database);
let health_status = db.health_check().await?;
tracing::info!("MongoDB health check: {}", health_status);
let jwt_service = auth::JwtService::new(config.jwt.clone());
let app_state = config::AppState {
db,
jwt_service,
config: config.clone(),
};
let app = Router::new()
.route("/health", get(health_check))
.route("/ready", get(readiness_check))
.with_state(app_state)
.layer(TraceLayer::new_for_http());
let addr = format!("{}:{}", config.server.host, config.server.port);
tracing::info!("Listening on {}", addr);
.route("/health", get(handlers::health_check))
.route("/ready", get(handlers::ready_check))
.route("/api/auth/register", post(handlers::register))
.route("/api/auth/login", post(handlers::login))
.route("/api/auth/refresh", post(handlers::refresh_token))
.route("/api/auth/logout", post(handlers::logout))
.route("/api/users/me", get(handlers::get_profile))
.layer(
ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.layer(CorsLayer::new())
)
.route_layer(axum_middleware::from_fn_with_state(
app_state.clone(),
crate::middleware::auth::jwt_auth_middleware
))
.with_state(app_state);
let listener = tokio::net::TcpListener::bind(&addr)
.await
.expect("Failed to bind address");
let listener = tokio::net::TcpListener::bind(&format!("{}:{}", config.server.host, config.server.port))
.await?;
tracing::info!("Server listening on {}:{}", config.server.host, config.server.port);
axum::serve(listener, app).await?;
axum::serve(listener, app)
.await
.expect("Server error");
Ok(())
}
async fn health_check() -> Json<serde_json::Value> {
Json(json!({
"status": "ok",
"timestamp": chrono::Utc::now().to_rfc3339(),
}))
}
async fn readiness_check(State(state): State<AppState>) -> Json<serde_json::Value> {
let db_status = state.db.health_check().await.unwrap_or_else(|_| "disconnected".to_string());
Json(json!({
"status": "ready",
"database": db_status,
"timestamp": chrono::Utc::now().to_rfc3339(),
}))
}