diff --git a/backend/.sqlx/query-171f07975cf260d23d1077aa57c85bbb8741b6f136122653fefac674eadb3dca.json b/backend/.sqlx/query-171f07975cf260d23d1077aa57c85bbb8741b6f136122653fefac674eadb3dca.json deleted file mode 100644 index 22226f3..0000000 --- a/backend/.sqlx/query-171f07975cf260d23d1077aa57c85bbb8741b6f136122653fefac674eadb3dca.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO users (username, email) VALUES ($1, $2) RETURNING id, username, email", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int4" - }, - { - "ordinal": 1, - "name": "username", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "email", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - false, - false, - false - ] - }, - "hash": "171f07975cf260d23d1077aa57c85bbb8741b6f136122653fefac674eadb3dca" -} diff --git a/backend/.sqlx/query-232ccd067e3781e88d05778d24417e5da23e90f92adbcb658e59aad31d9e4150.json b/backend/.sqlx/query-232ccd067e3781e88d05778d24417e5da23e90f92adbcb658e59aad31d9e4150.json deleted file mode 100644 index a086902..0000000 --- a/backend/.sqlx/query-232ccd067e3781e88d05778d24417e5da23e90f92adbcb658e59aad31d9e4150.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO test_time_series (timestamp, value, metadata) VALUES ($1, $2, $3) RETURNING id, timestamp, value, metadata", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int4" - }, - { - "ordinal": 1, - "name": "timestamp", - "type_info": "Timestamptz" - }, - { - "ordinal": 2, - "name": "value", - "type_info": "Float8" - }, - { - "ordinal": 3, - "name": "metadata", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Timestamptz", - "Float8", - "Text" - ] - }, - "nullable": [ - false, - false, - false, - true - ] - }, - "hash": "232ccd067e3781e88d05778d24417e5da23e90f92adbcb658e59aad31d9e4150" -} diff --git a/backend/.sqlx/query-7bafdc18c840980e7fe701659f2292c9ce6cb8040a0f1c9e880ae9f1ca44eb1b.json b/backend/.sqlx/query-7bafdc18c840980e7fe701659f2292c9ce6cb8040a0f1c9e880ae9f1ca44eb1b.json deleted file mode 100644 index 5f1a24f..0000000 --- a/backend/.sqlx/query-7bafdc18c840980e7fe701659f2292c9ce6cb8040a0f1c9e880ae9f1ca44eb1b.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT id, timestamp, value, metadata FROM test_time_series", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int4" - }, - { - "ordinal": 1, - "name": "timestamp", - "type_info": "Timestamptz" - }, - { - "ordinal": 2, - "name": "value", - "type_info": "Float8" - }, - { - "ordinal": 3, - "name": "metadata", - "type_info": "Text" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false, - false, - true - ] - }, - "hash": "7bafdc18c840980e7fe701659f2292c9ce6cb8040a0f1c9e880ae9f1ca44eb1b" -} diff --git a/backend/.sqlx/query-88f23b59e72f3d364e0c83c1046b05c8fa7243caa6e1a6aba4a9228531156474.json b/backend/.sqlx/query-88f23b59e72f3d364e0c83c1046b05c8fa7243caa6e1a6aba4a9228531156474.json deleted file mode 100644 index 4b34b5b..0000000 --- a/backend/.sqlx/query-88f23b59e72f3d364e0c83c1046b05c8fa7243caa6e1a6aba4a9228531156474.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT id, username, email FROM users", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int4" - }, - { - "ordinal": 1, - "name": "username", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "email", - "type_info": "Text" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false, - false - ] - }, - "hash": "88f23b59e72f3d364e0c83c1046b05c8fa7243caa6e1a6aba4a9228531156474" -} diff --git a/backend/Cargo.lock b/backend/Cargo.lock index f531389..de35fc5 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -114,12 +114,14 @@ dependencies = [ name = "api-server" version = "0.1.0" dependencies = [ + "argon2 0.4.1", "axum", "chrono", "dotenvy", "env_logger", "log", "pyo3", + "rand_core 0.6.4", "serde", "serde_json", "shared-logic", @@ -128,6 +130,29 @@ dependencies = [ "tower-http", ] +[[package]] +name = "argon2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db4ce4441f99dbd377ca8a8f57b698c44d0d6e712d8329b5040da5a64aa1ce73" +dependencies = [ + "base64ct", + "blake2", + "password-hash 0.4.2", +] + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash 0.5.0", +] + [[package]] name = "async-trait" version = "0.1.88" @@ -263,6 +288,15 @@ dependencies = [ "serde", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -1309,6 +1343,28 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "paste" version = "1.0.15" @@ -1792,6 +1848,7 @@ dependencies = [ name = "shared-logic" version = "0.1.0" dependencies = [ + "argon2 0.5.3", "chrono", "dotenvy", "futures-util", @@ -1799,8 +1856,10 @@ dependencies = [ "lsl", "numpy", "once_cell", + "password-hash 0.5.0", "pyo3", "rand 0.8.5", + "rand_core 0.6.4", "serde", "serde_json", "sqlx", diff --git a/backend/api-server/Cargo.toml b/backend/api-server/Cargo.toml index 4215795..0bdef5e 100644 --- a/backend/api-server/Cargo.toml +++ b/backend/api-server/Cargo.toml @@ -9,6 +9,9 @@ edition = "2021" axum = { version = "0.7", features = ["macros"] } # Tower for HTTP services (used by Axum) tower-http = { version = "0.5", features = ["cors"] } # For CORS, logging, etc. +argon2 = "0.4" +rand_core = "0.6" + # Serialization/Deserialization for JSON serde = { version = "1.0", features = ["derive"] } diff --git a/backend/api-server/src/main.rs b/backend/api-server/src/main.rs index 88671c6..8e98605 100644 --- a/backend/api-server/src/main.rs +++ b/backend/api-server/src/main.rs @@ -17,100 +17,153 @@ use pyo3::Python; use pyo3::types::{PyList, PyModule, PyTuple}; use pyo3::PyResult; use pyo3::{IntoPy, ToPyObject}; +use rand_core::OsRng; // shared logic library use shared_logic::db::{initialize_connection, DbClient}; use shared_logic::models::{User, NewUser, UpdateUser, Session, FrontendState}; +// Argon2 imports +use argon2::{ + Argon2, + password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, +}; + + // Define application state #[derive(Clone)] struct AppState { db_client: DbClient, } + +#[derive(Debug, Clone, Deserialize)] +pub struct LoginRequest { + pub email: String, + pub password: String, +} + +#[derive(Debug, Serialize)] +struct PublicUser { + id: i32, + username: String, + email: String, +} + +#[derive(Debug, Deserialize)] +struct DeleteUserRequest { + id: i32, +} + + // creates new user when POST /users is called async fn create_user( State(app_state): State, Json(new_user_data): Json, -) -> Result, (StatusCode, String)> { +) -> Result, (StatusCode, String)> { info!("Received request to create user: {:?}", new_user_data); - match shared_logic::db::add_user( + // Generate a random salt + let salt = SaltString::generate(&mut OsRng); + + // Hash the password + let argon2 = Argon2::default(); + let password_hash = argon2 + .hash_password(new_user_data.password.as_bytes(), &salt) + .map_err(|e| { + error!("Failed to hash password: {}", e); + (StatusCode::INTERNAL_SERVER_ERROR, "Password hashing failed".into()) + })? + .to_string(); + + // Store user in DB + let created_user = shared_logic::db::add_user( &app_state.db_client, new_user_data, + password_hash, ) .await - { - Ok(created_user) => { - info!("User created successfully: {:?}", created_user); - Ok(Json(created_user)) - } - Err(e) => { - error!("Failed to create user: {}", e); - Err(( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Failed to create user: {}", e), - )) - } + .map_err(|e| { + error!("Failed to create user: {}", e); + (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to create user: {}", e)) + })?; + + // Convert to PublicUser to hide password + let public_user = PublicUser { + id: created_user.id, + username: created_user.username, + email: created_user.email, + }; + + info!("User created successfully: {:?}", public_user); + Ok(Json(public_user)) +} + + + +async fn login_user( + State(app_state): State, + Json(login_data): Json, +) -> Result, (StatusCode, String)> { + info!("Login attempt for email: {}", login_data.email); + + // Fetch user from DB by email + let user = match shared_logic::db::get_user_by_email(&app_state.db_client, &login_data.email).await { + Ok(u) => u, + Err(_) => return Err((StatusCode::UNAUTHORIZED, "Invalid email or password".into())), + }; + + // Verify password + let parsed_hash = PasswordHash::new(&user.password_hash) + .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Password parsing failed".into()))?; + + if Argon2::default().verify_password(login_data.password.as_bytes(), &parsed_hash).is_ok() { + let public_user = PublicUser { + id: user.id, + username: user.username, + email: user.email, + }; + Ok(Json(public_user)) + } else { + Err((StatusCode::UNAUTHORIZED, "Invalid email or password".into())) } } + // Handler for GET /users // This function will retrieve all users from the database. async fn get_all_users( - State(app_state): State, // Access shared database client -) -> Result>, (StatusCode, String)> { + State(app_state): State, +) -> Result>, (StatusCode, String)> { info!("Received request to get all users"); - match shared_logic::db::get_users(&app_state.db_client).await { - Ok(users) => { - info!("Retrieved {} users.", users.len()); - Ok(Json(users)) // Return list of users as JSON - } - Err(e) => { - error!("Failed to retrieve users: {}", e); - Err(( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Failed to retrieve users: {}", e), - )) - } - } -} + let users = shared_logic::db::get_users(&app_state.db_client).await.map_err(|e| { + error!("Failed to retrieve users: {}", e); + (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to retrieve users: {}", e)) + })?; -// Handler for PUT /users/:id -async fn update_user( - State(app_state): State, - Path(id): Path, - Json(update_data): Json, -) -> Result, (StatusCode, String)> { - info!("Received request to update user {}: {:?}", id, update_data); - - match shared_logic::db::update_user(&app_state.db_client, id, update_data).await { - Ok(user) => { - info!("User updated successfully: {:?}", user); - Ok(Json(user)) - } - Err(e) => { - error!("Failed to update user: {}", e); - Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to update user: {}", e))) - } - } + let public_users: Vec = users.into_iter().map(|u| PublicUser { + id: u.id, + username: u.username, + email: u.email, + }).collect(); + + Ok(Json(public_users)) } -// Handler for DELETE /users/:id async fn delete_user( State(app_state): State, - Path(id): Path, + Json(payload): Json, ) -> Result { - info!("Received request to delete user {}", id); + let user_id = payload.id; - match shared_logic::db::delete_user(&app_state.db_client, id).await { + match shared_logic::db::delete_user(&app_state.db_client, user_id).await { Ok(_) => { - info!("User {} deleted", id); - Ok(StatusCode::NO_CONTENT) + log::info!("User {} deleted successfully", user_id); + Ok(StatusCode::OK) } Err(e) => { - error!("Failed to delete user: {}", e); + log::error!("Failed to delete user {}: {}", user_id, e); Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to delete user: {}", e))) } } @@ -195,9 +248,12 @@ async fn get_frontend_state( } + + async fn run_python_script_handler() -> Result, (StatusCode, String)> { info!("Received request to run Python script."); + // Python::with_gil needs to be run in a blocking context for async Rust let result = tokio::task::spawn_blocking(move || { Python::with_gil(|py| { @@ -205,6 +261,7 @@ async fn run_python_script_handler() -> Result, (StatusCode, String) let current_dir = env::current_dir()?; info!("API Server CWD for Python scripts: {:?}", current_dir); + // Adjust Python script directory to sys.path // Assuming 'python' and 'scripts' folders are at the workspace root level // relative to the `api-server` crate, it would be `../python` and `../scripts`. @@ -212,50 +269,59 @@ async fn run_python_script_handler() -> Result, (StatusCode, String) // the CWD is `backend-server`. So paths are relative to that. let sys = py.import("sys")?; let paths: &PyList = sys.getattr("path")?.downcast()?; - + // Add the directory containing your EyeBlink Python source // This path is relative to the backend-server/ directory - paths.insert(0, "./python/EyeBlink/src")?; + paths.insert(0, "./python/EyeBlink/src")?; info!("Added './python/EyeBlink/src' to Python sys.path"); + // Read and execute test.py let test_py_path = "./python/EyeBlink/src/test.py"; let test_py_src = fs::read_to_string(test_py_path)?; PyModule::from_code(py, &test_py_src, "test.py", "__main__")?; info!("Executed test.py"); + // Read and execute hello.py let hello_py_path = "./scripts/hello.py"; let hello_py_src = fs::read_to_string(hello_py_path)?; let module = PyModule::from_code(py, &hello_py_src, "hello.py", "hello")?; info!("Loaded hello.py module"); + // Call the 'test' function from hello.py let greet_func = module.getattr("test")?.to_object(py); let args = PyTuple::new(py, &[20_i32.into_py(py), 30_i32.into_py(py)]); let py_result = greet_func.call1(py, args)?; + let result_str: String = py_result.extract(py)?; info!("Result from Python: {}", result_str); + Ok(result_str) as PyResult }) }).await.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Python blocking task failed: {}", e)))?; + match result { Ok(s) => Ok(Json(json!({"python_output": s}))), Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Python script execution failed: {}", e))), } } + #[tokio::main] async fn main() { env_logger::init(); info!("Starting API server..."); + dotenv().ok(); info!("Environment variables loaded."); + let db_client = match initialize_connection().await { Ok(client) => { info!("Database connection initialized successfully."); @@ -267,16 +333,18 @@ async fn main() { } }; + let app_state = AppState { db_client: db_client.clone(), }; + // Build Axum router let app = Router::new() .route("/users", post(create_user)) + .route("/users/login", post(login_user)) // Login route .route("/users", get(get_all_users)) - .route("/users/:id", axum::routing::put(update_user)) - .route("/users/:id", axum::routing::delete(delete_user)) + .route("/users/delete", post(delete_user)) // .route("/run-python-script", get(run_python_script_handler)) .route("/api/sessions", post(create_session)) @@ -288,6 +356,7 @@ async fn main() { // Share application state with all handlers .with_state(app_state); + // Define the address and port for the server to listen on. let host = std::env::var("API_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); let port = std::env::var("API_PORT") @@ -296,13 +365,16 @@ async fn main() { .expect("Invalid API_PORT"); let addr = format!("{}:{}", host, port); + let listener = TcpListener::bind(&addr).await.unwrap_or_else(|e| { error!("Failed to bind to address {}: {}", addr, e); panic!("Exiting due to address binding failure."); }); + info!("API server listening on {}", addr); + // Start the server and wait for it to run. axum::serve(listener, app) .await diff --git a/backend/shared-logic/Cargo.toml b/backend/shared-logic/Cargo.toml index ed01e32..6003901 100644 --- a/backend/shared-logic/Cargo.toml +++ b/backend/shared-logic/Cargo.toml @@ -41,6 +41,11 @@ once_cell = "1.18" #lsl lsl = "0.1.1" +# Argon2 password hashing +argon2 = "0.5" +password-hash = "0.5" +rand_core = "0.6" + # working with python pyo3 = { version = "0.18.0", features = ["auto-initialize"] } numpy = "0.18" \ No newline at end of file diff --git a/backend/shared-logic/src/db.rs b/backend/shared-logic/src/db.rs index b62018b..d082c4b 100644 --- a/backend/shared-logic/src/db.rs +++ b/backend/shared-logic/src/db.rs @@ -11,6 +11,11 @@ use super::models::{User, NewUser, TimeSeriesData, UpdateUser, Session, Frontend use crate::{lsl::EEGDataPacket}; use once_cell::sync::OnceCell; use std::sync::Arc; +use argon2::password_hash::SaltString; +use rand_core::OsRng; +use argon2::{Argon2, password_hash::{PasswordHasher, PasswordHash, PasswordVerifier}}; + + pub static DB_POOL: OnceCell> = OnceCell::new(); @@ -59,13 +64,14 @@ pub fn get_db_client() -> DbClient { DB_POOL.get().expect("DB not initialized").clone() } -pub async fn add_user(client: &DbClient, new_user: NewUser) -> Result { +pub async fn add_user(client: &DbClient, new_user: NewUser, password_hash: String) -> Result { info!("Adding user: {} ({})", new_user.username, new_user.email); let user = sqlx::query_as!( User, - "INSERT INTO users (username, email) VALUES ($1, $2) RETURNING id, username, email", + "INSERT INTO users (username, email, password_hash) VALUES ($1, $2, $3) RETURNING id, username, email, password_hash", new_user.username, new_user.email, + password_hash ) .fetch_one(&**client) .await?; @@ -75,13 +81,24 @@ pub async fn add_user(client: &DbClient, new_user: NewUser) -> Result Result, Error> { info!("Retrieving users..."); - let users = sqlx::query_as!(User, "SELECT id, username, email FROM users") + let users = sqlx::query_as!(User, "SELECT id, username, email, password_hash FROM users") .fetch_all(&**client) .await?; info!("Retrieved {} users.", users.len()); Ok(users) } +pub async fn get_user_by_email(client: &DbClient, email: &str) -> Result { + sqlx::query_as!( + User, + "SELECT id, username, email, password_hash FROM users WHERE email = $1", + email + ) + .fetch_one(&**client) + .await +} + + /// Insert a new record into testtime_series using chrono's DateTime. pub async fn add_testtime_series_data( client: &DbClient, @@ -163,73 +180,114 @@ pub async fn insert_batch_eeg(client: &DbClient, packet: &EEGDataPacket) -> Resu /// Update a user by id. /// /// Returns the updated User on success. -pub async fn update_user(client: &DbClient, user_id: i32, updated: UpdateUser) -> Result { - info!("Updating user id {}", user_id); - - - // see what fields are being updated - if let Some(ref username) = updated.username { - info!("Updating username: {}", username); - } - - if let Some(ref email) = updated.email { - info!("Updating email: {}", email); - } +pub async fn update_user( + client: &DbClient, + user_id: i32, + updated: UpdateUser, +) -> Result { + log::info!("Updating user id {}", user_id); + + // Hash password if provided + let password_hash = if let Some(password) = &updated.password { + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::default(); + Some( + argon2 + .hash_password(password.as_bytes(), &salt) + .map_err(|e| { + log::error!("Password hashing failed: {}", e); + Error::RowNotFound // fallback error + })? + .to_string(), + ) + } else { + None + }; - // sql query to update user - let user = match (updated.username, updated.email) { - (Some(username), Some(email)) => { + // Build SQL query dynamically based on which fields are Some + let user = match (updated.username, updated.email, password_hash) { + (Some(username), Some(email), Some(password_hash)) => { sqlx::query_as!( User, - "UPDATE users SET username = $1, email = $2 WHERE id = $3 RETURNING id, username, email", + "UPDATE users SET username = $1, email = $2, password_hash = $3 WHERE id = $4 RETURNING id, username, email, password_hash", username, email, + password_hash, user_id ) - .fetch_one(&**client) + .fetch_one(&**client) .await? } - (Some(username), None) => { + (Some(username), Some(email), None) => { sqlx::query_as!( User, - "UPDATE users SET username = $1 WHERE id = $2 RETURNING id, username, email", + "UPDATE users SET username = $1, email = $2 WHERE id = $3 RETURNING id, username, email, password_hash", username, + email, user_id ) .fetch_one(&**client) .await? } - (None, Some(email)) => { + (Some(username), None, Some(password_hash)) => { sqlx::query_as!( User, - "UPDATE users SET email = $1 WHERE id = $2 RETURNING id, username, email", + "UPDATE users SET username = $1, password_hash = $2 WHERE id = $3 RETURNING id, username, email, password_hash", + username, + password_hash, + user_id + ) + .fetch_one(&**client) + .await? + } + (None, Some(email), Some(password_hash)) => { + sqlx::query_as!( + User, + "UPDATE users SET email = $1, password_hash = $2 WHERE id = $3 RETURNING id, username, email, password_hash", + email, + password_hash, + user_id + ) + .fetch_one(&**client) + .await? + } + (Some(username), None, None) => { + sqlx::query_as!( + User, + "UPDATE users SET username = $1 WHERE id = $2 RETURNING id, username, email, password_hash", + username, + user_id + ) + .fetch_one(&**client) + .await? + } + (None, Some(email), None) => { + sqlx::query_as!( + User, + "UPDATE users SET email = $1 WHERE id = $2 RETURNING id, username, email, password_hash", email, user_id ) .fetch_one(&**client) .await? } - (None, None) => { - info!("No fields to update for user id {}, user not updated", user_id); // unsure of what behavior to do in this case - return Err(Error::RowNotFound); // returning error for now, should this be allowed/be a different error? + (None, None, Some(password_hash)) => { + sqlx::query_as!( + User, + "UPDATE users SET password_hash = $1 WHERE id = $2 RETURNING id, username, email, password_hash", + password_hash, + user_id + ) + .fetch_one(&**client) + .await? + } + (None, None, None) => { + log::info!("No fields to update for user id {}, user not updated", user_id); + return Err(Error::RowNotFound); } }; - /// let user = sqlx::query_as!( - /// User, - /// r#" UPDATE users SET - /// username = COALESCE($1, username), - // email = COALESCE($2, email) - // WHERE id = $3 - // RETURNING id, username, email "#, - /// updated.username, - /// updated.email, - /// user_id - /// ) - /// .fetch_one(&**client) - /// .await?; - - info!("=User updated: {:?}", user); + log::info!("User updated: {:?}", user); Ok(user) } diff --git a/backend/shared-logic/src/models.rs b/backend/shared-logic/src/models.rs index d9fac77..dcee0e4 100644 --- a/backend/shared-logic/src/models.rs +++ b/backend/shared-logic/src/models.rs @@ -1,5 +1,6 @@ use serde::{Deserialize, Serialize}; use chrono::{DateTime, Utc}; + use serde_json::Value; // Existing User struct (used for data coming OUT of the DB) @@ -8,8 +9,10 @@ pub struct User { pub id: i32, pub username: String, pub email: String, + pub password_hash: String, // store hashed password } + // Struct for creating a user (used for data coming INTO the API) // Because User derived Deserialize, the serde library (which Axum used to process incoming JSON request body) // expected all fields in User struct to be present in JSON you sent (id was not part of payload) @@ -17,14 +20,9 @@ pub struct User { pub struct NewUser { pub username: String, pub email: String, + pub password: String, // raw password comes from API request } -// Struct for updating a user (partial updates allowed) -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct UpdateUser { // the fields are optional, allowing you to update them individually if needed - pub username: Option, - pub email: Option, -} #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] pub struct TimeSeriesData { @@ -34,6 +32,13 @@ pub struct TimeSeriesData { pub metadata: Option, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UpdateUser { + pub username: Option, + pub email: Option, + pub password: Option, // new field for updating password +} + // Struct for session data #[derive(Debug, Serialize, Deserialize, sqlx::FromRow)] pub struct Session { diff --git a/backend/test-lsl/Cargo.toml b/backend/test-lsl/Cargo.toml new file mode 100644 index 0000000..30cb15f --- /dev/null +++ b/backend/test-lsl/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "test-lsl" +version = "0.1.0" +edition = "2024" + +[dependencies] +lsl-sys = "0.1.1" diff --git a/backend/test-lsl/src/main.rs b/backend/test-lsl/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/backend/test-lsl/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +}