basic backend auth fr

This commit is contained in:
Mohamad.Elsena 2025-01-02 13:20:10 +01:00
parent d3f829880a
commit 793e136087
6 changed files with 228 additions and 46 deletions

59
backend/Cargo.lock generated
View File

@ -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",

View File

@ -13,3 +13,4 @@ actix-files = "0.6"
actix-cors = "0.6"
env_logger = "0.10" # Check for the latest version
log = "0.4"
futures = "0.3"

36
backend/src/auth.rs Normal file
View File

@ -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<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
let db = req
.app_data::<web::Data<Arc<Mutex<Connection>>>>()
.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")))
}
}

View File

@ -1,4 +1,4 @@
use rusqlite::{Connection, Result};
use rusqlite::{params, Connection, OptionalExtension, Result};
pub fn init_db() -> Result<Connection> {
let conn = Connection::open("form_data.db")?;
@ -24,5 +24,55 @@ pub fn init_db() -> Result<Connection> {
[],
)?;
// 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<Option<String>> {
let mut stmt = conn.prepare("SELECT id FROM users WHERE token = ?1")?;
let user_id: Option<String> = 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<Option<String>> {
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(())
}

View File

@ -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<Arc<Mutex<Connection>>>,
login_request: web::Json<LoginRequest>,
) -> 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<Arc<Mutex<Connection>>>,
path: web::Path<String>,
submission: web::Form<serde_json::Value>,
) -> 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<Arc<Mutex<Connection>>>,
form: web::Json<Form>,
auth: Auth,
form: web::Json<crate::models::Form>,
) -> 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<Arc<Mutex<Connection>>>) -> 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<Arc<Mutex<Connection>>>, 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<Arc<Mutex<Connection>>>) -> 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<Form> = forms_iter.filter_map(|f| f.ok()).collect();
let forms: Vec<crate::models::Form> = forms_iter.filter_map(|f| f.ok()).collect();
HttpResponse::Ok().json(forms)
}
// Submit a form
pub async fn submit_form(
db: web::Data<Arc<Mutex<rusqlite::Connection>>>,
path: web::Path<String>,
submission: web::Form<serde_json::Value>,
) -> 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<Arc<Mutex<Connection>>>,
auth: Auth,
path: web::Path<String>,
) -> 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<Submission> = submissions_iter.filter_map(|s| s.ok()).collect();
let submissions: Vec<crate::models::Submission> =
submissions_iter.filter_map(|s| s.ok()).collect();
HttpResponse::Ok().json(submissions)
}

View File

@ -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")?