added basic auth

This commit is contained in:
Mohamad 2024-12-30 13:02:57 +01:00
parent d3f829880a
commit 5214b6b88a
4 changed files with 210 additions and 3 deletions

View File

@ -24,5 +24,13 @@ pub fn init_db() -> Result<Connection> {
[],
)?;
conn.execute(
"CREATE TABLE IF NOT EXISTS admin_users (
username TEXT PRIMARY KEY,
password_hash TEXT NOT NULL
)",
[],
)?;
Ok(conn)
}

View File

@ -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<Arc<Mutex<Connection>>>,
@ -95,3 +103,89 @@ pub async fn get_submissions(
let submissions: Vec<Submission> = submissions_iter.filter_map(|s| s.ok()).collect();
HttpResponse::Ok().json(submissions)
}
pub async fn admin_login(
db: web::Data<Arc<Mutex<Connection>>>,
credentials: web::Json<LoginCredentials>,
) -> 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<AdminUser> = 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<Arc<Mutex<Connection>>>,
user: web::Json<LoginCredentials>,
) -> 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)),
}
}

87
backend/src/middleware.rs Normal file
View File

@ -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<S, B> Transform<S, ServiceRequest> for AuthMiddleware
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: MessageBody + 'static,
{
type Response = ServiceResponse<BoxBody>; // Changed to BoxBody
type Error = Error;
type Transform = AuthMiddlewareService<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(AuthMiddlewareService { service })
}
}
pub struct AuthMiddlewareService<S> {
service: S,
}
impl<S, B> Service<ServiceRequest> for AuthMiddlewareService<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: MessageBody + 'static,
{
type Response = ServiceResponse<BoxBody>; // Changed to BoxBody
type Error = Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
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::<Claims>(token, &key, &validation).is_ok()
}

View File

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