From 5214b6b88a4f821b7caa942928be61b4d7382886 Mon Sep 17 00:00:00 2001 From: Mohamad Date: Mon, 30 Dec 2024 13:02:57 +0100 Subject: [PATCH] added basic auth --- backend/src/db.rs | 8 +++ backend/src/handlers.rs | 100 ++++++++++++++++++++++++++++++++++++-- backend/src/middleware.rs | 87 +++++++++++++++++++++++++++++++++ backend/src/models.rs | 18 +++++++ 4 files changed, 210 insertions(+), 3 deletions(-) create mode 100644 backend/src/middleware.rs diff --git a/backend/src/db.rs b/backend/src/db.rs index c00758a..2d4907b 100644 --- a/backend/src/db.rs +++ b/backend/src/db.rs @@ -24,5 +24,13 @@ pub fn init_db() -> Result { [], )?; + conn.execute( + "CREATE TABLE IF NOT EXISTS admin_users ( + username TEXT PRIMARY KEY, + password_hash TEXT NOT NULL + )", + [], + )?; + Ok(conn) } diff --git a/backend/src/handlers.rs b/backend/src/handlers.rs index 4d03648..fd02df7 100644 --- a/backend/src/handlers.rs +++ b/backend/src/handlers.rs @@ -1,10 +1,18 @@ +use crate::models::{AdminUser, Claims, Form, LoginCredentials, Submission}; use actix_web::{web, HttpResponse, Responder}; +use argon2::{ + password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, + Argon2, +}; +use jsonwebtoken::{encode, EncodingKey, Header}; use rusqlite::{params, Connection}; -use std::sync::{Arc, Mutex}; +use serde_json::json; +use std::{ + sync::{Arc, Mutex}, + time::{SystemTime, UNIX_EPOCH}, +}; use uuid::Uuid; -use crate::models::{Form, Submission}; - // Create a new form pub async fn create_form( db: web::Data>>, @@ -95,3 +103,89 @@ pub async fn get_submissions( let submissions: Vec = submissions_iter.filter_map(|s| s.ok()).collect(); HttpResponse::Ok().json(submissions) } + +pub async fn admin_login( + db: web::Data>>, + credentials: web::Json, +) -> impl Responder { + let conn = db.lock().unwrap(); + + let mut stmt = + match conn.prepare("SELECT username, password_hash FROM admin_users WHERE username = ?1") { + Ok(stmt) => stmt, + Err(e) => return HttpResponse::InternalServerError().body(format!("Error: {}", e)), + }; + + let admin: Option = stmt + .query_row([&credentials.username], |row| { + Ok(AdminUser { + username: row.get(0)?, + password_hash: row.get(1)?, + }) + }) + .ok(); + + match admin { + Some(user) => { + let parsed_hash = PasswordHash::new(&user.password_hash).unwrap(); + let argon2 = Argon2::default(); + + let is_valid = argon2 + .verify_password(credentials.password.as_bytes(), &parsed_hash) + .is_ok(); + + if is_valid { + let expiration = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() as usize + + 24 * 3600; + + let claims = Claims { + sub: user.username, + exp: expiration, + }; + + let token = encode( + &Header::default(), + &claims, + &EncodingKey::from_secret("your-secret-key".as_ref()), + ) + .unwrap(); + + HttpResponse::Ok().json(json!({ "token": token })) + } else { + HttpResponse::Unauthorized().json(json!({ + "error": "Invalid credentials" + })) + } + } + None => HttpResponse::Unauthorized().json(json!({ + "error": "Invalid credentials" + })), + } +} + +pub async fn create_admin( + db: web::Data>>, + user: web::Json, +) -> impl Responder { + let conn = db.lock().unwrap(); + + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::default(); + let password_hash = argon2 + .hash_password(user.password.as_bytes(), &salt) + .unwrap() + .to_string(); + + match conn.execute( + "INSERT INTO admin_users (username, password_hash) VALUES (?1, ?2)", + params![user.username, password_hash], + ) { + Ok(_) => HttpResponse::Ok().json(json!({ + "message": "Admin user created successfully" + })), + Err(e) => HttpResponse::InternalServerError().body(format!("Error: {}", e)), + } +} diff --git a/backend/src/middleware.rs b/backend/src/middleware.rs new file mode 100644 index 0000000..fbdb7b0 --- /dev/null +++ b/backend/src/middleware.rs @@ -0,0 +1,87 @@ +use crate::models::Claims; +use actix_web::body::{BoxBody, MessageBody}; +use actix_web::dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}; +use actix_web::{Error, HttpResponse}; +use futures::future::{ok, Ready}; +use serde_json::json; +use std::future::Future; +use std::pin::Pin; + +pub struct AuthMiddleware; + +impl Transform for AuthMiddleware +where + S: Service, Error = Error>, + S::Future: 'static, + B: MessageBody + 'static, +{ + type Response = ServiceResponse; // Changed to BoxBody + type Error = Error; + type Transform = AuthMiddlewareService; + type InitError = (); + type Future = Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + ok(AuthMiddlewareService { service }) + } +} + +pub struct AuthMiddlewareService { + service: S, +} + +impl Service for AuthMiddlewareService +where + S: Service, Error = Error>, + S::Future: 'static, + B: MessageBody + 'static, +{ + type Response = ServiceResponse; // Changed to BoxBody + type Error = Error; + type Future = Pin>>>; + + forward_ready!(service); + + fn call(&self, req: ServiceRequest) -> Self::Future { + if req.path() == "/admin/login" || req.path() == "/admin/create" { + let fut = self.service.call(req); + return Box::pin(async move { + let res = fut.await?; + Ok(res.map_into_boxed_body()) // Convert the response body to BoxBody + }); + } + + let auth_header = req.headers().get("Authorization"); + match auth_header { + Some(header) => { + let token = header.to_str().unwrap_or("").replace("Bearer ", ""); + if verify_token(&token) { + let fut = self.service.call(req); + Box::pin(async move { + let res = fut.await?; + Ok(res.map_into_boxed_body()) // Convert the response body to BoxBody + }) + } else { + let (request, _) = req.into_parts(); + let response = HttpResponse::Unauthorized() + .json(json!({"error": "Invalid token"})) + .map_into_boxed_body(); + Box::pin(async move { Ok(ServiceResponse::new(request, response)) }) + } + } + None => { + let (request, _) = req.into_parts(); + let response = HttpResponse::Unauthorized() + .json(json!({"error": "No authorization token"})) + .map_into_boxed_body(); + Box::pin(async move { Ok(ServiceResponse::new(request, response)) }) + } + } + } +} + +pub fn verify_token(token: &str) -> bool { + let validation = jsonwebtoken::Validation::default(); + let key = jsonwebtoken::DecodingKey::from_secret("your-secret-key".as_ref()); + jsonwebtoken::decode::(token, &key, &validation).is_ok() +} diff --git a/backend/src/models.rs b/backend/src/models.rs index 6678636..b837049 100644 --- a/backend/src/models.rs +++ b/backend/src/models.rs @@ -13,3 +13,21 @@ pub struct Submission { pub form_id: String, pub data: serde_json::Value, // JSON of submission data } + +#[derive(Debug, Serialize, Deserialize)] +pub struct AdminUser { + pub username: String, + pub password_hash: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct LoginCredentials { + pub username: String, + pub password: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + pub sub: String, + pub(crate) exp: usize, +}