diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 194ebb2..a09a176 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -543,6 +543,7 @@ dependencies = [ "actix-files", "actix-web", "env_logger", + "futures", "log", "rusqlite", "serde", @@ -550,12 +551,65 @@ dependencies = [ "uuid", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -574,8 +628,13 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 900be72..fead35d 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -12,4 +12,5 @@ uuid = { version = "1.0", features = ["v4"] } actix-files = "0.6" actix-cors = "0.6" env_logger = "0.10" # Check for the latest version -log = "0.4" \ No newline at end of file +log = "0.4" +futures = "0.3" \ No newline at end of file diff --git a/backend/src/auth.rs b/backend/src/auth.rs new file mode 100644 index 0000000..ca2f9d7 --- /dev/null +++ b/backend/src/auth.rs @@ -0,0 +1,36 @@ +use actix_web::{dev::Payload, http::header::AUTHORIZATION, web, Error, FromRequest, HttpRequest}; +use futures::future::{ready, Ready}; +use rusqlite::Connection; +use std::sync::{Arc, Mutex}; + +pub struct Auth { + pub user_id: String, +} + +impl FromRequest for Auth { + type Error = Error; + type Future = Ready>; + + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + let db = req + .app_data::>>>() + .expect("Database connection missing"); + + if let Some(auth_header) = req.headers().get(AUTHORIZATION) { + if let Ok(auth_str) = auth_header.to_str() { + if auth_str.starts_with("Bearer ") { + let token = &auth_str[7..]; + let conn = db.lock().unwrap(); + + match super::db::validate_token(&conn, token) { + Ok(Some(user_id)) => return ready(Ok(Auth { user_id })), + Ok(None) | Err(_) => { + return ready(Err(actix_web::error::ErrorUnauthorized("Invalid token"))) + } + } + } + } + } + ready(Err(actix_web::error::ErrorUnauthorized("Missing token"))) + } +} diff --git a/backend/src/db.rs b/backend/src/db.rs index c00758a..1ba665a 100644 --- a/backend/src/db.rs +++ b/backend/src/db.rs @@ -1,4 +1,4 @@ -use rusqlite::{Connection, Result}; +use rusqlite::{params, Connection, OptionalExtension, Result}; pub fn init_db() -> Result { let conn = Connection::open("form_data.db")?; @@ -24,5 +24,55 @@ pub fn init_db() -> Result { [], )?; + // Add a table for users + conn.execute( + "CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + username TEXT NOT NULL UNIQUE, + password TEXT NOT NULL, -- Store a hashed password + token TEXT UNIQUE -- Optional: For token-based auth + )", + [], + )?; + Ok(conn) } + +// Add a function to validate a token +pub fn validate_token(conn: &Connection, token: &str) -> Result> { + let mut stmt = conn.prepare("SELECT id FROM users WHERE token = ?1")?; + let user_id: Option = stmt + .query_row(params![token], |row| row.get(0)) + .optional()?; + Ok(user_id) +} + +// Add a function to authenticate users (for login) +pub fn authenticate_user( + conn: &Connection, + username: &str, + password: &str, +) -> Result> { + let mut stmt = conn.prepare("SELECT id, password FROM users WHERE username = ?1")?; + let mut rows = stmt.query(params![username])?; + + if let Some(row) = rows.next()? { + let user_id: String = row.get(0)?; + let stored_password: String = row.get(1)?; + + // Replace this with a secure password hashing and verification mechanism + if stored_password == password { + return Ok(Some(user_id)); + } + } + Ok(None) +} + +// Add a function to generate and save a token for a user +pub fn generate_token_for_user(conn: &Connection, user_id: &str, token: &str) -> Result<()> { + conn.execute( + "UPDATE users SET token = ?1 WHERE id = ?2", + params![token, user_id], + )?; + Ok(()) +} diff --git a/backend/src/handlers.rs b/backend/src/handlers.rs index 4d03648..db5f2ec 100644 --- a/backend/src/handlers.rs +++ b/backend/src/handlers.rs @@ -1,17 +1,73 @@ use actix_web::{web, HttpResponse, Responder}; use rusqlite::{params, Connection}; +use serde::{Deserialize, Serialize}; use std::sync::{Arc, Mutex}; use uuid::Uuid; -use crate::models::{Form, Submission}; +use crate::auth::Auth; -// Create a new form +// Structs for requests and responses +#[derive(Deserialize)] +pub struct LoginRequest { + pub username: String, + pub password: String, +} + +#[derive(Serialize)] +pub struct LoginResponse { + pub token: String, +} + +// Public: Login handler +pub async fn login( + db: web::Data>>, + login_request: web::Json, +) -> impl Responder { + let conn = db.lock().unwrap(); + let user_id = + match crate::db::authenticate_user(&conn, &login_request.username, &login_request.password) + { + Ok(Some(user_id)) => user_id, + Ok(None) => return HttpResponse::Unauthorized().body("Invalid username or password"), + Err(_) => return HttpResponse::InternalServerError().body("Database error"), + }; + + let token = Uuid::new_v4().to_string(); + if let Err(_) = crate::db::generate_token_for_user(&conn, &user_id, &token) { + return HttpResponse::InternalServerError().body("Failed to generate token"); + } + + HttpResponse::Ok().json(LoginResponse { token }) +} + +// Public: Submit a form +pub async fn submit_form( + db: web::Data>>, + path: web::Path, + submission: web::Form, +) -> impl Responder { + let conn = db.lock().unwrap(); + let submission_id = Uuid::new_v4().to_string(); + let form_id = path.into_inner(); + let submission_json = serde_json::to_string(&submission.into_inner()).unwrap(); + + match conn.execute( + "INSERT INTO submissions (id, form_id, data) VALUES (?1, ?2, ?3)", + params![submission_id, form_id, submission_json], + ) { + Ok(_) => HttpResponse::Ok().json(submission_id), + Err(e) => HttpResponse::InternalServerError().body(format!("Error: {}", e)), + } +} + +// Protected: Create a new form pub async fn create_form( db: web::Data>>, - form: web::Json
, + auth: Auth, + form: web::Json, ) -> impl Responder { - println!("Received form: {:?}", form); - let conn = db.lock().unwrap(); // Lock the Mutex to access the database + println!("Authenticated user: {}", auth.user_id); + let conn = db.lock().unwrap(); let form_id = Uuid::new_v4().to_string(); let form_json = serde_json::to_string(&form.fields).unwrap(); @@ -24,9 +80,10 @@ pub async fn create_form( } } -// Get all forms -pub async fn get_forms(db: web::Data>>) -> impl Responder { - let conn = db.lock().unwrap(); // Lock the Mutex to access the database +// Protected: Get all forms +pub async fn get_forms(db: web::Data>>, auth: Auth) -> impl Responder { + println!("Authenticated user: {}", auth.user_id); + let conn = db.lock().unwrap(); let mut stmt = match conn.prepare("SELECT id, name, fields FROM forms") { Ok(stmt) => stmt, @@ -39,41 +96,22 @@ pub async fn get_forms(db: web::Data>>) -> impl Responder let name: String = row.get(1)?; let fields: String = row.get(2)?; let fields = serde_json::from_str(&fields).unwrap(); - Ok(Form { id, name, fields }) + Ok(crate::models::Form { id, name, fields }) }) .unwrap(); - let forms: Vec = forms_iter.filter_map(|f| f.ok()).collect(); + let forms: Vec = forms_iter.filter_map(|f| f.ok()).collect(); HttpResponse::Ok().json(forms) } -// Submit a form -pub async fn submit_form( - db: web::Data>>, - path: web::Path, - submission: web::Form, -) -> impl Responder { - let conn = db.lock().unwrap(); // Lock the Mutex to access the database - let submission_id = Uuid::new_v4().to_string(); - let form_id = path.into_inner(); - - let submission_json = serde_json::to_string(&submission.into_inner()).unwrap(); - - match conn.execute( - "INSERT INTO submissions (id, form_id, data) VALUES (?1, ?2, ?3)", - params![submission_id, form_id, submission_json], - ) { - Ok(_) => HttpResponse::Ok().json(submission_id), - Err(e) => HttpResponse::InternalServerError().body(format!("Error: {}", e)), - } -} - -// Get submissions for a form +// Protected: Get submissions for a form pub async fn get_submissions( db: web::Data>>, + auth: Auth, path: web::Path, ) -> impl Responder { - let conn = db.lock().unwrap(); // Lock the Mutex to access the database + println!("Authenticated user: {}", auth.user_id); + let conn = db.lock().unwrap(); let form_id = path.into_inner(); let mut stmt = @@ -88,10 +126,11 @@ pub async fn get_submissions( let form_id: String = row.get(1)?; let data: String = row.get(2)?; let data = serde_json::from_str(&data).unwrap(); - Ok(Submission { id, form_id, data }) + Ok(crate::models::Submission { id, form_id, data }) }) .unwrap(); - let submissions: Vec = submissions_iter.filter_map(|s| s.ok()).collect(); + let submissions: Vec = + submissions_iter.filter_map(|s| s.ok()).collect(); HttpResponse::Ok().json(submissions) } diff --git a/backend/src/main.rs b/backend/src/main.rs index 341c4cc..f42dc39 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,22 +1,19 @@ use actix_cors::Cors; -// use actix_files as fs; use actix_web::{web, App, HttpServer}; use std::sync::{Arc, Mutex}; +mod auth; mod db; mod handlers; mod models; #[actix_web::main] async fn main() -> std::io::Result<()> { - // Initialize the database connection env_logger::init(); - let db = Arc::new(Mutex::new( db::init_db().expect("Failed to initialize the database"), )); - // Start the Actix-Web server HttpServer::new(move || { App::new() .wrap( @@ -26,16 +23,16 @@ async fn main() -> std::io::Result<()> { .allow_any_method(), ) .app_data(web::Data::new(db.clone())) - // .service(fs::Files::new("/", "./frontend/public").index_file("index.html")) - .route("/forms", web::post().to(handlers::create_form)) - .route("/forms", web::get().to(handlers::get_forms)) + .route("/login", web::post().to(handlers::login)) // Public: Login .route( "/forms/{id}/submissions", - web::post().to(handlers::submit_form), + web::post().to(handlers::submit_form), // Public: Submit form ) + .route("/forms", web::post().to(handlers::create_form)) // Protected + .route("/forms", web::get().to(handlers::get_forms)) // Protected .route( "/forms/{id}/submissions", - web::get().to(handlers::get_submissions), + web::get().to(handlers::get_submissions), // Protected ) }) .bind("127.0.0.1:8080")?