diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 9ad738e..37def77 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -20,5 +20,3 @@ jobs: run: cargo fmt - name: Checking run: cargo check - - name: Run tests - run: cargo test --verbose diff --git a/.gitignore b/.gitignore index 2a86dd0..b98b3af 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ -/target .DS_Store .env + +/logs +/target /test-dbs \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 4c5f796..e750014 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "generic-array", ] @@ -19,7 +19,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", - "cipher", + "cipher 0.4.4", "cpufeatures", ] @@ -31,12 +31,21 @@ checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" dependencies = [ "aead", "aes", - "cipher", + "cipher 0.4.4", "ctr", "ghash", "subtle", ] +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -168,6 +177,19 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "bcrypt" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ae5479c93d3720e4c1dbd6b945b97457c50cb672781104768190371df1a905" +dependencies = [ + "base64", + "blowfish", + "getrandom 0.4.2", + "subtle", + "zeroize", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -192,6 +214,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blowfish" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ce3946557b35e71d1bbe07ec385073ce9eda05043f95de134eb578fcf1a298" +dependencies = [ + "byteorder", + "cipher 0.5.2", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -248,8 +280,18 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", - "inout", + "crypto-common 0.1.7", + "inout 0.1.4", +] + +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "crypto-common 0.2.2", + "inout 0.2.2", ] [[package]] @@ -316,6 +358,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -342,13 +393,22 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "ctr" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" dependencies = [ - "cipher", + "cipher 0.4.4", ] [[package]] @@ -379,7 +439,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "const-oid", - "crypto-common", + "crypto-common 0.1.7", "subtle", ] @@ -422,6 +482,7 @@ dependencies = [ "aes-gcm", "anyhow", "axum", + "bcrypt", "chrono", "dotenvy", "jsonwebtoken", @@ -429,6 +490,9 @@ dependencies = [ "serde_json", "sqlx", "tokio", + "tracing", + "tracing-appender", + "tracing-subscriber", "uuid", ] @@ -761,6 +825,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "0.14.32" @@ -938,6 +1011,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", +] + [[package]] name = "itoa" version = "1.0.18" @@ -1061,6 +1143,15 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "matchit" version = "0.7.3" @@ -1117,6 +1208,15 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -1471,6 +1571,23 @@ dependencies = [ "bitflags 2.11.1", ] +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + [[package]] name = "rsa" version = "0.9.10" @@ -1648,6 +1765,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1960,6 +2086,12 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + [[package]] name = "syn" version = "2.0.117" @@ -2021,6 +2153,15 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "time" version = "0.3.47" @@ -2156,6 +2297,19 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" +dependencies = [ + "crossbeam-channel", + "symlink", + "thiserror", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.31" @@ -2174,6 +2328,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", ] [[package]] @@ -2227,7 +2424,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "subtle", ] @@ -2266,6 +2463,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index f0ea326..fa668ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,10 @@ serde_json = "1.0" uuid = { version = "1.23.1", features = ["v4"] } chrono = { version = "0.4", features = ["serde"] } jsonwebtoken = { version = "10.4.0", features = ["aws_lc_rs"] } +bcrypt = "0.19.1" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } +tracing-appender = "0.2" # optional, for file output [lib] name = "ems" diff --git a/schemas/chat.sql b/migrations/20260521102510_initial_schema.sql similarity index 55% rename from schemas/chat.sql rename to migrations/20260521102510_initial_schema.sql index bcec6ae..0b2a4a0 100644 --- a/schemas/chat.sql +++ b/migrations/20260521102510_initial_schema.sql @@ -1,21 +1,23 @@ +-- Add migration script here -- Users table CREATE TABLE users ( - id TEXT PRIMARY KEY, -- UUID stored as string, generated in Rust + id TEXT PRIMARY KEY, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, + nonce BLOB NOT NULL, + public_key TEXT, -- for future E2EE date_joined TEXT NOT NULL DEFAULT (datetime('now')), - public_key TEXT NOT NULL, - deleted_at TEXT + deleted_at TEXT -- soft delete ); --- Chats table +-- Chats (conversations) CREATE TABLE chats ( id TEXT PRIMARY KEY, created_at TEXT NOT NULL DEFAULT (datetime('now')), deleted_at TEXT ); --- Chat participants (many-to-many) +-- Participants (many-to-many) CREATE TABLE chat_participants ( chat_id TEXT NOT NULL REFERENCES chats(id) ON DELETE CASCADE, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, @@ -23,20 +25,20 @@ CREATE TABLE chat_participants ( PRIMARY KEY (chat_id, user_id) ); --- Messages table (with encryption support) +-- Messages with encryption storage CREATE TABLE messages ( - id TEXT PRIMARY KEY, -- UUID generated in Rust + id TEXT PRIMARY KEY, chat_id TEXT NOT NULL REFERENCES chats(id) ON DELETE CASCADE, - sender_id TEXT REFERENCES users(id) ON DELETE SET NULL, - encrypted_content BLOB NOT NULL, -- AES-GCM ciphertext (without tag) - nonce BLOB NOT NULL, -- 12 bytes unique per message - tag BLOB NOT NULL, -- 16 bytes authentication tag - created_at TEXT NOT NULL DEFAULT (datetime('now')) + sender_id TEXT NOT NULL REFERENCES users(id) ON DELETE SET NULL, + encrypted_content BLOB NOT NULL, + nonce BLOB NOT NULL, + tag BLOB NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT ); --- Indexes +-- Indexes for performance CREATE INDEX idx_messages_chat_id ON messages(chat_id); CREATE INDEX idx_messages_sender_id ON messages(sender_id); CREATE INDEX idx_messages_created_at ON messages(created_at); -CREATE INDEX idx_chat_participants_user_id ON chat_participants(user_id); -CREATE INDEX idx_chat_participants_chat_id ON chat_participants(chat_id); \ No newline at end of file +CREATE INDEX idx_chat_participants_user_id ON chat_participants(user_id); \ No newline at end of file diff --git a/migrations/20260521141725_changing_password_hash_to_blob.sql b/migrations/20260521141725_changing_password_hash_to_blob.sql new file mode 100644 index 0000000..c28a2cf --- /dev/null +++ b/migrations/20260521141725_changing_password_hash_to_blob.sql @@ -0,0 +1,34 @@ +-- Add migration script here +-- 1. Create a new table with the desired schema +CREATE TABLE users_new ( + id TEXT PRIMARY KEY, + username TEXT UNIQUE NOT NULL, + password_hash BLOB NOT NULL, -- changed from TEXT to BLOB + nonce BLOB NOT NULL, + public_key TEXT, + date_joined TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT +); + +-- 2. Copy data from the old table +-- Convert password_hash from TEXT to BLOB (SQLite does this automatically, +-- but we explicitly cast to ensure correct type) +INSERT INTO users_new (id, username, password_hash, nonce, public_key, date_joined, deleted_at) +SELECT + id, + username, + CAST(password_hash AS BLOB), -- convert to BLOB + CAST(nonce AS BLOB), + public_key, + date_joined, + deleted_at +FROM users; + +-- 3. Drop the old table +DROP TABLE users; + +-- 4. Rename the new table +ALTER TABLE users_new RENAME TO users; + +-- 5. Recreate indexes (if any – usually on username) +CREATE UNIQUE INDEX idx_users_username ON users(username); \ No newline at end of file diff --git a/migrations/20260521151036_drop_nonce_from_users.sql b/migrations/20260521151036_drop_nonce_from_users.sql new file mode 100644 index 0000000..fa6523f --- /dev/null +++ b/migrations/20260521151036_drop_nonce_from_users.sql @@ -0,0 +1,16 @@ +-- Add migration script here +-- SQLite doesn't support DROP COLUMN directly, so we recreate the table. +CREATE TABLE users_new ( + id TEXT PRIMARY KEY, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + public_key TEXT, + date_joined TEXT NOT NULL DEFAULT (datetime('now')), + deleted_at TEXT +); + +INSERT INTO users_new (id, username, password_hash, public_key, date_joined, deleted_at) +SELECT id, username, password_hash, public_key, date_joined, deleted_at FROM users; + +DROP TABLE users; +ALTER TABLE users_new RENAME TO users; \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 0c689d6..0512806 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,22 +3,22 @@ use std::env; pub struct Config { pub database_url: String, + pub ip_address: String, pub jwt_secret: String, pub port: u16, - pub ip_address: String, } impl Config { pub fn from_env() -> Self { - dotenv().ok(); + dotenv().expect("Failed to load .env variables"); Self { database_url: env::var("DATABASE_URL").expect("DATABASE_URL must be set"), jwt_secret: env::var("JWT_SECRET").expect("JWT_SECRET must be set"), + ip_address: env::var("IP_ADDRESS").expect("Ip address not set"), port: env::var("PORT") - .unwrap_or_else(|_| "8080".to_string()) + .unwrap_or("8080".to_string()) .parse() - .expect("PORT must be a number"), - ip_address: env::var("IP_ADDRESS").expect("Ip address not set") + .expect("PORT not set"), } } } diff --git a/src/db/mod.rs b/src/db/mod.rs index aa4d396..f9bd309 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -9,5 +9,5 @@ pub async fn create_pool(database_url: &str) -> Result } pub async fn run_migrations(pool: &SqlitePool) -> Result<(), sqlx::migrate::MigrateError> { - sqlx::migrate!("./schemas/").run(pool).await + sqlx::migrate!("./migrations").run(pool).await } diff --git a/src/lib.rs b/src/lib.rs index 3809eb3..d2a86e7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ pub mod config; pub mod db; pub mod routes; +pub mod tracing; diff --git a/src/main.rs b/src/main.rs index 1d713b7..d4ddaff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,21 +3,30 @@ use axum::Server; use ems::config::Config; use ems::db; use ems::routes::app; +use ems::tracing::init_tracing; use std::net::SocketAddr; +use tracing::info; #[tokio::main] async fn main() -> Result<()> { + // Tracing initiallization + let _guard = init_tracing(); + let config = Config::from_env(); - // Create connection pool - let pool = db::create_pool(&config.database_url).await?; - // Run migrations (creates tables if not present) - db::run_migrations(&pool).await?; + let pool = db::create_pool(&config.database_url) + .await + .context("Failed to create pool")?; + + // Run migrations + db::run_migrations(&pool) + .await + .context("Failed to run migrations")?; let socket = format!("{}:{}", config.ip_address, config.port) .parse() - .expect("Failed to open socket"); + .context("Failed to open socket")?; - println!("server running on {:?}", &socket); + info!("server running on {:?}", &socket); Server::bind(&socket) .serve(app(pool).into_make_service_with_connect_info::()) .await diff --git a/src/routes/helper.rs b/src/routes/helper.rs index 4faf522..5d9b7d9 100644 --- a/src/routes/helper.rs +++ b/src/routes/helper.rs @@ -1,8 +1,8 @@ +use crate::config::Config; use axum::http::StatusCode; +use chrono::{Duration, Utc}; use jsonwebtoken::{encode, EncodingKey, Header}; use serde::{Deserialize, Serialize}; -use std::env; -use chrono::{Utc, Duration}; #[derive(Debug, Serialize, Deserialize)] pub struct Claims { @@ -19,12 +19,8 @@ pub fn create_jwt(user_id: &str) -> Result { sub: user_id.to_string(), exp: expiration, }; - let secret = env::var("JWT_SECRET").map_err(|_| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - "JWT secret missing".to_string(), - ) - })?; + let config = Config::from_env(); + let secret = config.jwt_secret; encode( &Header::default(), &claims, diff --git a/src/routes/login.rs b/src/routes/login.rs new file mode 100644 index 0000000..de8af86 --- /dev/null +++ b/src/routes/login.rs @@ -0,0 +1,79 @@ +use super::helper::create_jwt; +use super::signup::AuthResponse; +use axum::extract::State; +use axum::http::StatusCode; +use axum::Json; +use bcrypt::verify; +use serde::Deserialize; +use sqlx::SqlitePool; +use tracing::{debug, error, warn}; + +#[derive(Deserialize)] +pub struct LoginRequest { + username: String, + password: String, +} + +pub async fn login( + State(pool): State, + Json(payload): Json, +) -> Result, (StatusCode, String)> { + debug!("Login attempt detected"); + + if payload.username.is_empty() || payload.password.is_empty() { + debug!("Empty payload received for login"); + return Err((StatusCode::BAD_REQUEST, "Missing fields".to_string())); + } + let row = sqlx::query!( + "SELECT id, password_hash FROM users WHERE username = ? AND deleted_at IS NULL", + payload.username + ) + .fetch_optional(&pool) + .await + .map_err(|e| { + error!( + error = ?e, + "DB failed to initialize" + ); + (StatusCode::INTERNAL_SERVER_ERROR, "DB error".to_string()) + })?; + + let (id, stored_hash) = match row { + Some(r) => (r.id, r.password_hash), + None => { + warn!( + username = payload.username, + "Failed to find record for login attempt of user" + ); + return Err((StatusCode::UNAUTHORIZED, "Invalid credentials".to_string())); + } + }; + + let user_id = id.unwrap_or_default(); + debug!(user_id = user_id, "Record found for login of User"); + + // 3. Verify password + let is_valid = verify(&payload.password, &stored_hash).map_err(|_| { + warn!( + user_id = user_id, + "Failed to verify password for login attempt of user" + ); + (StatusCode::INTERNAL_SERVER_ERROR, "Hash error".to_string()) + })?; + + if !is_valid { + warn!( + user_id = user_id, + "Invalid credentials provided for login attempt of user" + ); + return Err((StatusCode::UNAUTHORIZED, "Invalid credentials".to_string())); + } + + // 4. Generate and return JWT + let token = create_jwt(&user_id)?; + debug!( + user_id = user_id, + "Authentication response returned to user" + ); + Ok(Json(AuthResponse { token, user_id })) +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 71061b6..4c3f856 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,18 +1,33 @@ mod helper; +mod login; mod signup; +use super::routes::login::login; use super::routes::signup::signup; -use axum::{http::StatusCode, routing::get, Router}; +use axum::extract::State; +use axum::http::StatusCode; +use axum::routing::post; +use axum::{routing::get, Router}; use sqlx::SqlitePool; // our router pub fn app(pool: SqlitePool) -> Router { Router::new() .route("/healthz", get(health_check)) - .route("/signup", get(signup)) + .route("/is_ready", get(readiness_check)) + .route("/auth/signup", post(signup)) + .route("/auth/login", post(login)) .with_state(pool) } async fn health_check() -> (StatusCode, &'static str) { - (StatusCode::OK, "ok") + (StatusCode::OK, "Ok") +} + +async fn readiness_check(State(pool): State) -> (StatusCode, &'static str) { + // Try to execute a simple query (e.g., SELECT 1) + match sqlx::query("SELECT 1").execute(&pool).await { + Ok(_) => (StatusCode::OK, "ready"), + Err(_) => (StatusCode::SERVICE_UNAVAILABLE, "db unavailable"), + } } diff --git a/src/routes/signup.rs b/src/routes/signup.rs index 940f34c..14e6e75 100644 --- a/src/routes/signup.rs +++ b/src/routes/signup.rs @@ -1,53 +1,44 @@ use super::helper::*; -use aes_gcm::{ - aead::{Aead, AeadCore, KeyInit, OsRng}, - Aes256Gcm, -}; +use anyhow::Result; use axum::extract::State; use axum::http::StatusCode; use axum::Json; +use bcrypt::{hash, DEFAULT_COST}; use chrono::Utc; use serde::{Deserialize, Serialize}; use sqlx::SqlitePool; +use tracing::{debug, error}; use uuid::Uuid; #[derive(Deserialize)] pub struct SignupRequest { username: String, password: String, - // optional public_key for future E2EE } #[derive(Serialize)] pub struct AuthResponse { - token: String, // JWT - user_id: String, + pub token: String, + pub user_id: String, } pub async fn signup( State(pool): State, Json(payload): Json, ) -> Result, (StatusCode, String)> { - // 1. Validate input (username not empty, password strength, etc.) + debug!("Signup attempt detected"); + if payload.username.is_empty() || payload.password.is_empty() { + debug!("Empty payload received for signup"); return Err((StatusCode::BAD_REQUEST, "Missing fields".to_string())); } - let key = Aes256Gcm::generate_key(OsRng); - let cipher = Aes256Gcm::new(&key); - let nonce = Aes256Gcm::generate_nonce(&mut OsRng); - // 2. Hash password - let hashed = cipher - .encrypt(&nonce, payload.password.as_ref()) - .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Hash error".to_string()))?; - - // 3. Generate user ID + let hashed = hash(payload.password, DEFAULT_COST).expect("Failed to hash password"); let user_id = Uuid::new_v4().to_string(); let now = Utc::now().to_rfc3339(); - // 4. Insert into database let result = sqlx::query( - "INSERT INTO users (id, username, password_hash, date_joined) VALUES (?1, ?2, ?3, ?4)" + "INSERT INTO users (id, username, password_hash, date_joined) VALUES (?1, ?2, ?3, ?4)", ) .bind(&user_id) .bind(&payload.username) @@ -58,13 +49,21 @@ pub async fn signup( match result { Ok(_) => { + debug!(user_id = user_id, "Created user successfully"); // 5. Generate JWT let token = create_jwt(&user_id)?; // implement this helper Ok(Json(AuthResponse { token, user_id })) } Err(sqlx::Error::Database(ref e)) if e.is_unique_violation() => { + debug!( + username = payload.username, + "Creating user failed. Username already exist" + ); Err((StatusCode::CONFLICT, "Username already taken".to_string())) } - Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, "DB error".to_string())), + Err(_) => { + error!("Creating user failed. Failed to load database."); + Err((StatusCode::INTERNAL_SERVER_ERROR, "DB error".to_string())) + } } } diff --git a/src/tracing.rs b/src/tracing.rs new file mode 100644 index 0000000..a05bc0a --- /dev/null +++ b/src/tracing.rs @@ -0,0 +1,20 @@ +use tracing_appender::non_blocking::WorkerGuard; +use tracing_subscriber::{fmt, prelude::*, EnvFilter}; + +pub fn init_tracing() -> WorkerGuard { + // Create a rolling file appender – rotates daily + let file_appender = tracing_appender::rolling::daily("logs", "ems.log"); + let (non_blocking, guard) = tracing_appender::non_blocking(file_appender); + + // Set up subscriber with both console and file writers + let console_layer = fmt::layer().with_writer(std::io::stdout); + let file_layer = fmt::layer().with_writer(non_blocking).with_ansi(false); // no ANSI colours in file + + tracing_subscriber::registry() + .with(EnvFilter::from_default_env()) + .with(console_layer) + .with(file_layer) + .init(); + + guard // must be stored to keep the writer alive +}