add jwt auth, fix session handling, clean up middlewares
This commit is contained in:
		
							
								
								
									
										98
									
								
								actix_mvc_app/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										98
									
								
								actix_mvc_app/Cargo.lock
									
									
									
										generated
									
									
									
								
							@@ -255,6 +255,7 @@ dependencies = [
 | 
			
		||||
 "dotenv",
 | 
			
		||||
 "env_logger",
 | 
			
		||||
 "futures",
 | 
			
		||||
 "jsonwebtoken",
 | 
			
		||||
 "lazy_static",
 | 
			
		||||
 "log",
 | 
			
		||||
 "num_cpus",
 | 
			
		||||
@@ -466,6 +467,12 @@ dependencies = [
 | 
			
		||||
 "windows-targets",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "base64"
 | 
			
		||||
version = "0.13.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "base64"
 | 
			
		||||
version = "0.20.0"
 | 
			
		||||
@@ -1508,6 +1515,20 @@ dependencies = [
 | 
			
		||||
 "serde",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "jsonwebtoken"
 | 
			
		||||
version = "8.3.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "base64 0.21.7",
 | 
			
		||||
 "pem",
 | 
			
		||||
 "ring",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 "simple_asn1",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "language-tags"
 | 
			
		||||
version = "0.3.2"
 | 
			
		||||
@@ -1630,12 +1651,31 @@ dependencies = [
 | 
			
		||||
 "minimal-lexical",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "num-bigint"
 | 
			
		||||
version = "0.4.6"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "num-integer",
 | 
			
		||||
 "num-traits",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "num-conv"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "num-integer"
 | 
			
		||||
version = "0.1.46"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "num-traits",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "num-traits"
 | 
			
		||||
version = "0.2.19"
 | 
			
		||||
@@ -1724,6 +1764,15 @@ version = "0.2.3"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "pem"
 | 
			
		||||
version = "1.1.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "base64 0.13.1",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "percent-encoding"
 | 
			
		||||
version = "2.3.1"
 | 
			
		||||
@@ -2021,6 +2070,21 @@ version = "0.8.5"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "ring"
 | 
			
		||||
version = "0.16.20"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "cc",
 | 
			
		||||
 "libc",
 | 
			
		||||
 "once_cell",
 | 
			
		||||
 "spin",
 | 
			
		||||
 "untrusted",
 | 
			
		||||
 "web-sys",
 | 
			
		||||
 "winapi",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "ron"
 | 
			
		||||
version = "0.8.1"
 | 
			
		||||
@@ -2187,6 +2251,18 @@ dependencies = [
 | 
			
		||||
 "libc",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "simple_asn1"
 | 
			
		||||
version = "0.6.3"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "num-bigint",
 | 
			
		||||
 "num-traits",
 | 
			
		||||
 "thiserror",
 | 
			
		||||
 "time",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "siphasher"
 | 
			
		||||
version = "1.0.1"
 | 
			
		||||
@@ -2238,6 +2314,12 @@ dependencies = [
 | 
			
		||||
 "windows-sys 0.52.0",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "spin"
 | 
			
		||||
version = "0.5.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "stable_deref_trait"
 | 
			
		||||
version = "1.2.0"
 | 
			
		||||
@@ -2556,6 +2638,12 @@ dependencies = [
 | 
			
		||||
 "subtle",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "untrusted"
 | 
			
		||||
version = "0.7.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "url"
 | 
			
		||||
version = "2.5.4"
 | 
			
		||||
@@ -2690,6 +2778,16 @@ dependencies = [
 | 
			
		||||
 "unicode-ident",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "web-sys"
 | 
			
		||||
version = "0.3.77"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "js-sys",
 | 
			
		||||
 "wasm-bindgen",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "winapi"
 | 
			
		||||
version = "0.3.9"
 | 
			
		||||
 
 | 
			
		||||
@@ -22,3 +22,4 @@ bcrypt = "0.15.0"
 | 
			
		||||
uuid = { version = "1.6.1", features = ["v4", "serde"] }
 | 
			
		||||
lazy_static = "1.4.0"
 | 
			
		||||
redis = { version = "0.23.0", features = ["tokio-comp"] }
 | 
			
		||||
jsonwebtoken = "8.3.0"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,129 +0,0 @@
 | 
			
		||||
use actix_web::{
 | 
			
		||||
    dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
 | 
			
		||||
    Error,
 | 
			
		||||
};
 | 
			
		||||
use futures::future::{ready, LocalBoxFuture, Ready};
 | 
			
		||||
use std::{
 | 
			
		||||
    future::Future,
 | 
			
		||||
    pin::Pin,
 | 
			
		||||
    time::Instant,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// Middleware for logging request duration
 | 
			
		||||
pub struct RequestTimer;
 | 
			
		||||
 | 
			
		||||
impl<S, B> Transform<S, ServiceRequest> for RequestTimer
 | 
			
		||||
where
 | 
			
		||||
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
 | 
			
		||||
    S::Future: 'static,
 | 
			
		||||
    B: 'static,
 | 
			
		||||
{
 | 
			
		||||
    type Response = ServiceResponse<B>;
 | 
			
		||||
    type Error = Error;
 | 
			
		||||
    type Transform = RequestTimerMiddleware<S>;
 | 
			
		||||
    type InitError = ();
 | 
			
		||||
    type Future = Ready<Result<Self::Transform, Self::InitError>>;
 | 
			
		||||
 | 
			
		||||
    fn new_transform(&self, service: S) -> Self::Future {
 | 
			
		||||
        ready(Ok(RequestTimerMiddleware { service }))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct RequestTimerMiddleware<S> {
 | 
			
		||||
    service: S,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<S, B> Service<ServiceRequest> for RequestTimerMiddleware<S>
 | 
			
		||||
where
 | 
			
		||||
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
 | 
			
		||||
    S::Future: 'static,
 | 
			
		||||
    B: 'static,
 | 
			
		||||
{
 | 
			
		||||
    type Response = ServiceResponse<B>;
 | 
			
		||||
    type Error = Error;
 | 
			
		||||
    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
 | 
			
		||||
 | 
			
		||||
    forward_ready!(service);
 | 
			
		||||
 | 
			
		||||
    fn call(&self, req: ServiceRequest) -> Self::Future {
 | 
			
		||||
        let start = Instant::now();
 | 
			
		||||
        let path = req.path().to_owned();
 | 
			
		||||
        let method = req.method().to_string();
 | 
			
		||||
 | 
			
		||||
        let fut = self.service.call(req);
 | 
			
		||||
 | 
			
		||||
        Box::pin(async move {
 | 
			
		||||
            let res = fut.await?;
 | 
			
		||||
            let duration = start.elapsed();
 | 
			
		||||
            log::info!(
 | 
			
		||||
                "{} {} - {} - {:?}",
 | 
			
		||||
                method,
 | 
			
		||||
                path,
 | 
			
		||||
                res.status().as_u16(),
 | 
			
		||||
                duration
 | 
			
		||||
            );
 | 
			
		||||
            Ok(res)
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Middleware for adding security headers
 | 
			
		||||
pub struct SecurityHeaders;
 | 
			
		||||
 | 
			
		||||
impl<S, B> Transform<S, ServiceRequest> for SecurityHeaders
 | 
			
		||||
where
 | 
			
		||||
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
 | 
			
		||||
    S::Future: 'static,
 | 
			
		||||
    B: 'static,
 | 
			
		||||
{
 | 
			
		||||
    type Response = ServiceResponse<B>;
 | 
			
		||||
    type Error = Error;
 | 
			
		||||
    type Transform = SecurityHeadersMiddleware<S>;
 | 
			
		||||
    type InitError = ();
 | 
			
		||||
    type Future = Ready<Result<Self::Transform, Self::InitError>>;
 | 
			
		||||
 | 
			
		||||
    fn new_transform(&self, service: S) -> Self::Future {
 | 
			
		||||
        ready(Ok(SecurityHeadersMiddleware { service }))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct SecurityHeadersMiddleware<S> {
 | 
			
		||||
    service: S,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<S, B> Service<ServiceRequest> for SecurityHeadersMiddleware<S>
 | 
			
		||||
where
 | 
			
		||||
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
 | 
			
		||||
    S::Future: 'static,
 | 
			
		||||
    B: 'static,
 | 
			
		||||
{
 | 
			
		||||
    type Response = ServiceResponse<B>;
 | 
			
		||||
    type Error = Error;
 | 
			
		||||
    type Future = Pin<Box<dyn Future<Output = Result<ServiceResponse<B>, Error>>>>;
 | 
			
		||||
 | 
			
		||||
    forward_ready!(service);
 | 
			
		||||
 | 
			
		||||
    fn call(&self, req: ServiceRequest) -> Self::Future {
 | 
			
		||||
        let fut = self.service.call(req);
 | 
			
		||||
 | 
			
		||||
        Box::pin(async move {
 | 
			
		||||
            let mut res = fut.await?;
 | 
			
		||||
            
 | 
			
		||||
            // Add security headers
 | 
			
		||||
            res.headers_mut().insert(
 | 
			
		||||
                actix_web::http::header::X_CONTENT_TYPE_OPTIONS,
 | 
			
		||||
                actix_web::http::header::HeaderValue::from_static("nosniff"),
 | 
			
		||||
            );
 | 
			
		||||
            res.headers_mut().insert(
 | 
			
		||||
                actix_web::http::header::X_FRAME_OPTIONS,
 | 
			
		||||
                actix_web::http::header::HeaderValue::from_static("DENY"),
 | 
			
		||||
            );
 | 
			
		||||
            res.headers_mut().insert(
 | 
			
		||||
                actix_web::http::header::HeaderName::from_static("x-xss-protection"),
 | 
			
		||||
                actix_web::http::header::HeaderValue::from_static("1; mode=block"),
 | 
			
		||||
            );
 | 
			
		||||
            
 | 
			
		||||
            Ok(res)
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,12 +1,91 @@
 | 
			
		||||
use actix_web::{web, HttpResponse, Responder, Result};
 | 
			
		||||
use actix_web::{web, HttpResponse, Responder, Result, http::header, cookie::Cookie};
 | 
			
		||||
use actix_session::Session;
 | 
			
		||||
use tera::Tera;
 | 
			
		||||
use crate::models::user::{User, LoginCredentials, RegistrationData};
 | 
			
		||||
use crate::models::user::{User, LoginCredentials, RegistrationData, UserRole};
 | 
			
		||||
use jsonwebtoken::{encode, decode, Header, Algorithm, Validation, EncodingKey, DecodingKey};
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use chrono::{Utc, Duration};
 | 
			
		||||
use lazy_static::lazy_static;
 | 
			
		||||
 | 
			
		||||
// JWT Claims structure
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize, Clone)]
 | 
			
		||||
pub struct Claims {
 | 
			
		||||
    pub sub: String,  // Subject (email)
 | 
			
		||||
    pub exp: usize,   // Expiration time
 | 
			
		||||
    pub iat: usize,   // Issued at
 | 
			
		||||
    pub role: String, // User role
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// JWT Secret key
 | 
			
		||||
lazy_static! {
 | 
			
		||||
    static ref JWT_SECRET: String = std::env::var("JWT_SECRET").unwrap_or_else(|_| "your_jwt_secret_key".to_string());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Controller for handling authentication-related routes
 | 
			
		||||
pub struct AuthController;
 | 
			
		||||
 | 
			
		||||
impl AuthController {
 | 
			
		||||
    /// Generate a JWT token for a user
 | 
			
		||||
    fn generate_token(email: &str, role: &UserRole) -> Result<String, jsonwebtoken::errors::Error> {
 | 
			
		||||
        let role_str = match role {
 | 
			
		||||
            UserRole::Admin => "admin",
 | 
			
		||||
            UserRole::User => "user",
 | 
			
		||||
        };
 | 
			
		||||
        
 | 
			
		||||
        let expiration = Utc::now()
 | 
			
		||||
            .checked_add_signed(Duration::hours(24))
 | 
			
		||||
            .expect("valid timestamp")
 | 
			
		||||
            .timestamp() as usize;
 | 
			
		||||
            
 | 
			
		||||
        let claims = Claims {
 | 
			
		||||
            sub: email.to_owned(),
 | 
			
		||||
            exp: expiration,
 | 
			
		||||
            iat: Utc::now().timestamp() as usize,
 | 
			
		||||
            role: role_str.to_string(),
 | 
			
		||||
        };
 | 
			
		||||
        
 | 
			
		||||
        encode(
 | 
			
		||||
            &Header::default(),
 | 
			
		||||
            &claims,
 | 
			
		||||
            &EncodingKey::from_secret(JWT_SECRET.as_bytes())
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /// Validate a JWT token
 | 
			
		||||
    pub fn validate_token(token: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
 | 
			
		||||
        let validation = Validation::new(Algorithm::HS256);
 | 
			
		||||
        
 | 
			
		||||
        let token_data = decode::<Claims>(
 | 
			
		||||
            token,
 | 
			
		||||
            &DecodingKey::from_secret(JWT_SECRET.as_bytes()),
 | 
			
		||||
            &validation
 | 
			
		||||
        )?;
 | 
			
		||||
        
 | 
			
		||||
        Ok(token_data.claims)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /// Extract token from session
 | 
			
		||||
    pub fn extract_token_from_session(session: &Session) -> Option<String> {
 | 
			
		||||
        session.get::<String>("auth_token").ok().flatten()
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /// Extract token from cookie
 | 
			
		||||
    pub fn extract_token_from_cookie(req: &actix_web::HttpRequest) -> Option<String> {
 | 
			
		||||
        req.cookie("auth_token").map(|c| c.value().to_string())
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /// Check if user is authenticated from session
 | 
			
		||||
    pub async fn is_authenticated(session: &Session) -> Option<Claims> {
 | 
			
		||||
        if let Some(token) = Self::extract_token_from_session(session) {
 | 
			
		||||
            match Self::validate_token(&token) {
 | 
			
		||||
                Ok(claims) => Some(claims),
 | 
			
		||||
                Err(_) => None,
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            None
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Renders the login page
 | 
			
		||||
    pub async fn login_page(tmpl: web::Data<Tera>) -> Result<impl Responder> {
 | 
			
		||||
        let mut ctx = tera::Context::new();
 | 
			
		||||
@@ -27,10 +106,7 @@ impl AuthController {
 | 
			
		||||
        session: Session,
 | 
			
		||||
        _tmpl: web::Data<Tera>
 | 
			
		||||
    ) -> Result<impl Responder> {
 | 
			
		||||
        // In a real application, you would validate the credentials against a database
 | 
			
		||||
        // For this example, we'll use a hardcoded user
 | 
			
		||||
        
 | 
			
		||||
        // Skip authentication check and always log in the user
 | 
			
		||||
        // For simplicity, always log in the user without checking credentials
 | 
			
		||||
        // Create a user object with admin role
 | 
			
		||||
        let mut test_user = User::new(
 | 
			
		||||
            "Admin User".to_string(),
 | 
			
		||||
@@ -39,17 +115,29 @@ impl AuthController {
 | 
			
		||||
        
 | 
			
		||||
        // Set the ID and admin role
 | 
			
		||||
        test_user.id = Some(1);
 | 
			
		||||
        test_user.role = crate::models::user::UserRole::Admin;
 | 
			
		||||
        test_user.role = UserRole::Admin;
 | 
			
		||||
        
 | 
			
		||||
        // Generate JWT token
 | 
			
		||||
        let token = Self::generate_token(&test_user.email, &test_user.role)
 | 
			
		||||
            .map_err(|_| actix_web::error::ErrorInternalServerError("Failed to generate token"))?;
 | 
			
		||||
        
 | 
			
		||||
        // Store user data in session
 | 
			
		||||
        let user_json = serde_json::to_string(&test_user).unwrap();
 | 
			
		||||
        if let Err(e) = session.insert("user", &user_json) {
 | 
			
		||||
            eprintln!("Session error: {}", e);
 | 
			
		||||
        }
 | 
			
		||||
        session.insert("user", &user_json)?;
 | 
			
		||||
        session.insert("auth_token", &token)?;
 | 
			
		||||
        
 | 
			
		||||
        // Redirect to the home page
 | 
			
		||||
        // Create a cookie with the JWT token
 | 
			
		||||
        let cookie = Cookie::build("auth_token", token)
 | 
			
		||||
            .path("/")
 | 
			
		||||
            .http_only(true)
 | 
			
		||||
            .secure(false) // Set to true in production with HTTPS
 | 
			
		||||
            .max_age(actix_web::cookie::time::Duration::hours(24))
 | 
			
		||||
            .finish();
 | 
			
		||||
        
 | 
			
		||||
        // Redirect to the home page with JWT token in cookie
 | 
			
		||||
        Ok(HttpResponse::Found()
 | 
			
		||||
            .append_header(("Location", "/"))
 | 
			
		||||
            .cookie(cookie)
 | 
			
		||||
            .append_header((header::LOCATION, "/"))
 | 
			
		||||
            .finish())
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
@@ -81,15 +169,29 @@ impl AuthController {
 | 
			
		||||
        
 | 
			
		||||
        // Set the ID and admin role
 | 
			
		||||
        user.id = Some(1);
 | 
			
		||||
        user.role = crate::models::user::UserRole::Admin;
 | 
			
		||||
        user.role = UserRole::Admin;
 | 
			
		||||
        
 | 
			
		||||
        // Generate JWT token
 | 
			
		||||
        let token = Self::generate_token(&user.email, &user.role)
 | 
			
		||||
            .map_err(|_| actix_web::error::ErrorInternalServerError("Failed to generate token"))?;
 | 
			
		||||
        
 | 
			
		||||
        // Store user data in session
 | 
			
		||||
        let user_json = serde_json::to_string(&user).unwrap();
 | 
			
		||||
        session.insert("user", &user_json).unwrap();
 | 
			
		||||
        session.insert("user", &user_json)?;
 | 
			
		||||
        session.insert("auth_token", &token)?;
 | 
			
		||||
        
 | 
			
		||||
        // Redirect to the home page
 | 
			
		||||
        // Create a cookie with the JWT token
 | 
			
		||||
        let cookie = Cookie::build("auth_token", token)
 | 
			
		||||
            .path("/")
 | 
			
		||||
            .http_only(true)
 | 
			
		||||
            .secure(false) // Set to true in production with HTTPS
 | 
			
		||||
            .max_age(actix_web::cookie::time::Duration::hours(24))
 | 
			
		||||
            .finish();
 | 
			
		||||
        
 | 
			
		||||
        // Redirect to the home page with JWT token in cookie
 | 
			
		||||
        Ok(HttpResponse::Found()
 | 
			
		||||
            .append_header(("Location", "/"))
 | 
			
		||||
            .cookie(cookie)
 | 
			
		||||
            .append_header((header::LOCATION, "/"))
 | 
			
		||||
            .finish())
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
@@ -98,9 +200,17 @@ impl AuthController {
 | 
			
		||||
        // Clear the session
 | 
			
		||||
        session.purge();
 | 
			
		||||
        
 | 
			
		||||
        // Redirect to the home page
 | 
			
		||||
        // Create an expired cookie to remove the JWT token
 | 
			
		||||
        let cookie = Cookie::build("auth_token", "")
 | 
			
		||||
            .path("/")
 | 
			
		||||
            .http_only(true)
 | 
			
		||||
            .max_age(actix_web::cookie::time::Duration::seconds(0))
 | 
			
		||||
            .finish();
 | 
			
		||||
        
 | 
			
		||||
        // Redirect to the home page and clear the auth token cookie
 | 
			
		||||
        Ok(HttpResponse::Found()
 | 
			
		||||
            .append_header(("Location", "/"))
 | 
			
		||||
            .cookie(cookie)
 | 
			
		||||
            .append_header((header::LOCATION, "/"))
 | 
			
		||||
            .finish())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,7 +1,9 @@
 | 
			
		||||
use actix_web::{web, HttpResponse, Responder, Result};
 | 
			
		||||
use actix_session::Session;
 | 
			
		||||
use chrono::{DateTime, Datelike, NaiveDate, TimeZone, Utc};
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use tera::Tera;
 | 
			
		||||
use serde_json::Value;
 | 
			
		||||
 | 
			
		||||
use crate::models::{CalendarEvent, CalendarViewMode};
 | 
			
		||||
use crate::utils::RedisCalendarService;
 | 
			
		||||
@@ -10,10 +12,18 @@ use crate::utils::RedisCalendarService;
 | 
			
		||||
pub struct CalendarController;
 | 
			
		||||
 | 
			
		||||
impl CalendarController {
 | 
			
		||||
    /// Helper function to get user from session
 | 
			
		||||
    fn get_user_from_session(session: &Session) -> Option<Value> {
 | 
			
		||||
        session.get::<String>("user").ok().flatten().and_then(|user_json| {
 | 
			
		||||
            serde_json::from_str(&user_json).ok()
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Handles the calendar page route
 | 
			
		||||
    pub async fn calendar(
 | 
			
		||||
        tmpl: web::Data<Tera>,
 | 
			
		||||
        query: web::Query<CalendarQuery>,
 | 
			
		||||
        _session: Session,
 | 
			
		||||
    ) -> Result<impl Responder> {
 | 
			
		||||
        let mut ctx = tera::Context::new();
 | 
			
		||||
        ctx.insert("active_page", "calendar");
 | 
			
		||||
@@ -25,7 +35,7 @@ impl CalendarController {
 | 
			
		||||
        // Parse the date from the query parameters or use the current date
 | 
			
		||||
        let date = if let Some(date_str) = &query.date {
 | 
			
		||||
            match NaiveDate::parse_from_str(date_str, "%Y-%m-%d") {
 | 
			
		||||
                Ok(naive_date) => Utc.from_utc_date(&naive_date).and_hms_opt(0, 0, 0).unwrap(),
 | 
			
		||||
                Ok(naive_date) => Utc.from_utc_datetime(&naive_date.and_hms_opt(0, 0, 0).unwrap()).into(),
 | 
			
		||||
                Err(_) => Utc::now(),
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
@@ -37,6 +47,11 @@ impl CalendarController {
 | 
			
		||||
        ctx.insert("current_month", &date.month());
 | 
			
		||||
        ctx.insert("current_day", &date.day());
 | 
			
		||||
        
 | 
			
		||||
        // Add user to context if available
 | 
			
		||||
        if let Some(user) = Self::get_user_from_session(&_session) {
 | 
			
		||||
            ctx.insert("user", &user);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Get events for the current view
 | 
			
		||||
        let (start_date, end_date) = match view_mode {
 | 
			
		||||
            CalendarViewMode::Year => {
 | 
			
		||||
@@ -52,9 +67,9 @@ impl CalendarController {
 | 
			
		||||
            },
 | 
			
		||||
            CalendarViewMode::Week => {
 | 
			
		||||
                // Calculate the start of the week (Sunday)
 | 
			
		||||
                let weekday = date.weekday().num_days_from_sunday();
 | 
			
		||||
                let _weekday = date.weekday().num_days_from_sunday();
 | 
			
		||||
                let start_date = date.date_naive().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap();
 | 
			
		||||
                let start = Utc.from_utc_date(&start_date).and_hms_opt(0, 0, 0).unwrap();
 | 
			
		||||
                let start = Utc.from_utc_datetime(&start_date.and_hms_opt(0, 0, 0).unwrap());
 | 
			
		||||
                let end = start + chrono::Duration::days(7);
 | 
			
		||||
                (start, end)
 | 
			
		||||
            },
 | 
			
		||||
@@ -210,10 +225,15 @@ impl CalendarController {
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /// Handles the new event page route
 | 
			
		||||
    pub async fn new_event(tmpl: web::Data<Tera>) -> Result<impl Responder> {
 | 
			
		||||
    pub async fn new_event(tmpl: web::Data<Tera>, _session: Session) -> Result<impl Responder> {
 | 
			
		||||
        let mut ctx = tera::Context::new();
 | 
			
		||||
        ctx.insert("active_page", "calendar");
 | 
			
		||||
        
 | 
			
		||||
        // Add user to context if available
 | 
			
		||||
        if let Some(user) = Self::get_user_from_session(&_session) {
 | 
			
		||||
            ctx.insert("user", &user);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        let rendered = tmpl.render("calendar/new_event.html", &ctx)
 | 
			
		||||
            .map_err(|e| {
 | 
			
		||||
                eprintln!("Template rendering error: {}", e);
 | 
			
		||||
@@ -227,6 +247,7 @@ impl CalendarController {
 | 
			
		||||
    pub async fn create_event(
 | 
			
		||||
        form: web::Form<EventForm>,
 | 
			
		||||
        tmpl: web::Data<Tera>,
 | 
			
		||||
        _session: Session,
 | 
			
		||||
    ) -> Result<impl Responder> {
 | 
			
		||||
        // Parse the start and end times
 | 
			
		||||
        let start_time = match DateTime::parse_from_rfc3339(&form.start_time) {
 | 
			
		||||
@@ -272,6 +293,11 @@ impl CalendarController {
 | 
			
		||||
                ctx.insert("active_page", "calendar");
 | 
			
		||||
                ctx.insert("error", "Failed to save event");
 | 
			
		||||
                
 | 
			
		||||
                // Add user to context if available
 | 
			
		||||
                if let Some(user) = Self::get_user_from_session(&_session) {
 | 
			
		||||
                    ctx.insert("user", &user);
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                let rendered = tmpl.render("calendar/new_event.html", &ctx)
 | 
			
		||||
                    .map_err(|e| {
 | 
			
		||||
                        eprintln!("Template rendering error: {}", e);
 | 
			
		||||
@@ -286,6 +312,7 @@ impl CalendarController {
 | 
			
		||||
    /// Handles the delete event route
 | 
			
		||||
    pub async fn delete_event(
 | 
			
		||||
        path: web::Path<String>,
 | 
			
		||||
        _session: Session,
 | 
			
		||||
    ) -> Result<impl Responder> {
 | 
			
		||||
        let id = path.into_inner();
 | 
			
		||||
        
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,29 @@
 | 
			
		||||
use actix_web::{web, HttpResponse, Responder, Result};
 | 
			
		||||
use actix_session::Session;
 | 
			
		||||
use tera::Tera;
 | 
			
		||||
use crate::models::User;
 | 
			
		||||
use serde_json::Value;
 | 
			
		||||
 | 
			
		||||
/// Controller for handling home-related routes
 | 
			
		||||
pub struct HomeController;
 | 
			
		||||
 | 
			
		||||
impl HomeController {
 | 
			
		||||
    /// Helper function to get user from session
 | 
			
		||||
    fn get_user_from_session(session: &Session) -> Option<Value> {
 | 
			
		||||
        session.get::<String>("user").ok().flatten().and_then(|user_json| {
 | 
			
		||||
            serde_json::from_str(&user_json).ok()
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Handles the markdown editor page route
 | 
			
		||||
    pub async fn editor(tmpl: web::Data<Tera>) -> Result<impl Responder> {
 | 
			
		||||
    pub async fn editor(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
 | 
			
		||||
        let mut ctx = tera::Context::new();
 | 
			
		||||
        ctx.insert("active_page", "editor");
 | 
			
		||||
        
 | 
			
		||||
        // Add user to context if available
 | 
			
		||||
        if let Some(user) = Self::get_user_from_session(&session) {
 | 
			
		||||
            ctx.insert("user", &user);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        let rendered = tmpl.render("editor.html", &ctx)
 | 
			
		||||
            .map_err(|e| {
 | 
			
		||||
                eprintln!("Template rendering error: {}", e);
 | 
			
		||||
@@ -21,13 +34,14 @@ impl HomeController {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Handles the home page route
 | 
			
		||||
    pub async fn index(tmpl: web::Data<Tera>) -> Result<impl Responder> {
 | 
			
		||||
    pub async fn index(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
 | 
			
		||||
        let mut ctx = tera::Context::new();
 | 
			
		||||
        ctx.insert("active_page", "home");
 | 
			
		||||
        
 | 
			
		||||
        // Example of using models in controllers
 | 
			
		||||
        let example_user = User::new("John Doe".to_string(), "john@example.com".to_string());
 | 
			
		||||
        ctx.insert("user", &example_user);
 | 
			
		||||
        // Add user to context if available
 | 
			
		||||
        if let Some(user) = Self::get_user_from_session(&session) {
 | 
			
		||||
            ctx.insert("user", &user);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        let rendered = tmpl.render("index.html", &ctx)
 | 
			
		||||
            .map_err(|e| {
 | 
			
		||||
@@ -39,10 +53,15 @@ impl HomeController {
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /// Handles the about page route
 | 
			
		||||
    pub async fn about(tmpl: web::Data<Tera>) -> Result<impl Responder> {
 | 
			
		||||
    pub async fn about(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
 | 
			
		||||
        let mut ctx = tera::Context::new();
 | 
			
		||||
        ctx.insert("active_page", "about");
 | 
			
		||||
        
 | 
			
		||||
        // Add user to context if available
 | 
			
		||||
        if let Some(user) = Self::get_user_from_session(&session) {
 | 
			
		||||
            ctx.insert("user", &user);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        let rendered = tmpl.render("about.html", &ctx)
 | 
			
		||||
            .map_err(|e| {
 | 
			
		||||
                eprintln!("Template rendering error: {}", e);
 | 
			
		||||
@@ -53,10 +72,15 @@ impl HomeController {
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /// Handles the contact page route
 | 
			
		||||
    pub async fn contact(tmpl: web::Data<Tera>) -> Result<impl Responder> {
 | 
			
		||||
    pub async fn contact(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
 | 
			
		||||
        let mut ctx = tera::Context::new();
 | 
			
		||||
        ctx.insert("active_page", "contact");
 | 
			
		||||
        
 | 
			
		||||
        // Add user to context if available
 | 
			
		||||
        if let Some(user) = Self::get_user_from_session(&session) {
 | 
			
		||||
            ctx.insert("user", &user);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        let rendered = tmpl.render("contact.html", &ctx)
 | 
			
		||||
            .map_err(|e| {
 | 
			
		||||
                eprintln!("Template rendering error: {}", e);
 | 
			
		||||
@@ -69,7 +93,8 @@ impl HomeController {
 | 
			
		||||
    /// Handles form submissions from the contact page
 | 
			
		||||
    pub async fn submit_contact(
 | 
			
		||||
        form: web::Form<ContactForm>,
 | 
			
		||||
        tmpl: web::Data<Tera>
 | 
			
		||||
        tmpl: web::Data<Tera>,
 | 
			
		||||
        session: Session
 | 
			
		||||
    ) -> Result<impl Responder> {
 | 
			
		||||
        // In a real application, you would process the form data here
 | 
			
		||||
        // For example, save it to a database or send an email
 | 
			
		||||
@@ -82,6 +107,11 @@ impl HomeController {
 | 
			
		||||
        ctx.insert("active_page", "contact");
 | 
			
		||||
        ctx.insert("success_message", "Your message has been sent successfully!");
 | 
			
		||||
        
 | 
			
		||||
        // Add user to context if available
 | 
			
		||||
        if let Some(user) = Self::get_user_from_session(&session) {
 | 
			
		||||
            ctx.insert("user", &user);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        let rendered = tmpl.render("contact.html", &ctx)
 | 
			
		||||
            .map_err(|e| {
 | 
			
		||||
                eprintln!("Template rendering error: {}", e);
 | 
			
		||||
 
 | 
			
		||||
@@ -2,10 +2,4 @@
 | 
			
		||||
pub mod home;
 | 
			
		||||
pub mod auth;
 | 
			
		||||
pub mod ticket;
 | 
			
		||||
pub mod calendar;
 | 
			
		||||
 | 
			
		||||
// Re-export controllers for easier imports
 | 
			
		||||
pub use home::HomeController;
 | 
			
		||||
pub use auth::AuthController;
 | 
			
		||||
pub use ticket::TicketController;
 | 
			
		||||
pub use calendar::CalendarController;
 | 
			
		||||
pub mod calendar;
 | 
			
		||||
@@ -2,7 +2,8 @@ use actix_web::{web, HttpResponse, Responder, Result};
 | 
			
		||||
use actix_session::Session;
 | 
			
		||||
use tera::Tera;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use crate::models::{User, Ticket, TicketComment, TicketStatus, TicketPriority, TicketFilter};
 | 
			
		||||
use serde_json::Value;
 | 
			
		||||
use crate::models::{User, Ticket, TicketComment, TicketStatus, TicketPriority};
 | 
			
		||||
use std::collections::HashMap;
 | 
			
		||||
use std::sync::{Arc, Mutex};
 | 
			
		||||
 | 
			
		||||
@@ -35,6 +36,13 @@ pub struct TicketFilterForm {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl TicketController {
 | 
			
		||||
    /// Helper function to get user from session
 | 
			
		||||
    fn get_user_from_session(session: &Session) -> Option<Value> {
 | 
			
		||||
        session.get::<String>("user").ok().flatten().and_then(|user_json| {
 | 
			
		||||
            serde_json::from_str(&user_json).ok()
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /// Lists all tickets with optional filtering
 | 
			
		||||
    pub async fn list_tickets(
 | 
			
		||||
        session: Session,
 | 
			
		||||
@@ -42,15 +50,8 @@ impl TicketController {
 | 
			
		||||
        tmpl: web::Data<Tera>
 | 
			
		||||
    ) -> Result<impl Responder> {
 | 
			
		||||
        // Get the current user from the session
 | 
			
		||||
        let user = match session.get::<String>("user")? {
 | 
			
		||||
            Some(user_json) => {
 | 
			
		||||
                match serde_json::from_str::<User>(&user_json) {
 | 
			
		||||
                    Ok(user) => Some(user),
 | 
			
		||||
                    Err(_) => None,
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            None => None,
 | 
			
		||||
        };
 | 
			
		||||
        let user_value = Self::get_user_from_session(&session);
 | 
			
		||||
        let user: Option<User> = user_value.and_then(|v| serde_json::from_value(v).ok());
 | 
			
		||||
        
 | 
			
		||||
        // If the user is not logged in, redirect to the login page
 | 
			
		||||
        if user.is_none() {
 | 
			
		||||
@@ -59,88 +60,75 @@ impl TicketController {
 | 
			
		||||
                .finish());
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        let user = user.unwrap();
 | 
			
		||||
        let user: User = user.unwrap();
 | 
			
		||||
        
 | 
			
		||||
        // Create a filter based on the query parameters
 | 
			
		||||
        let mut filter = TicketFilter::default();
 | 
			
		||||
        // Get all tickets from the in-memory storage
 | 
			
		||||
        let tickets = TICKETS.lock().unwrap();
 | 
			
		||||
        
 | 
			
		||||
        // Filter tickets based on the query parameters
 | 
			
		||||
        let mut filtered_tickets: Vec<Ticket> = tickets.values().cloned().collect();
 | 
			
		||||
        
 | 
			
		||||
        // Apply status filter if provided
 | 
			
		||||
        if let Some(status_str) = &query.status {
 | 
			
		||||
            filter.status = match status_str.as_str() {
 | 
			
		||||
                "open" => Some(TicketStatus::Open),
 | 
			
		||||
                "in_progress" => Some(TicketStatus::InProgress),
 | 
			
		||||
                "waiting_for_customer" => Some(TicketStatus::WaitingForCustomer),
 | 
			
		||||
                "resolved" => Some(TicketStatus::Resolved),
 | 
			
		||||
                "closed" => Some(TicketStatus::Closed),
 | 
			
		||||
                _ => None,
 | 
			
		||||
            };
 | 
			
		||||
            if !status_str.is_empty() {
 | 
			
		||||
                let status = match status_str.as_str() {
 | 
			
		||||
                    "open" => TicketStatus::Open,
 | 
			
		||||
                    "in_progress" => TicketStatus::InProgress,
 | 
			
		||||
                    "resolved" => TicketStatus::Resolved,
 | 
			
		||||
                    "closed" => TicketStatus::Closed,
 | 
			
		||||
                    _ => TicketStatus::Open,
 | 
			
		||||
                };
 | 
			
		||||
                
 | 
			
		||||
                filtered_tickets.retain(|ticket| ticket.status == status);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Apply priority filter if provided
 | 
			
		||||
        if let Some(priority_str) = &query.priority {
 | 
			
		||||
            filter.priority = match priority_str.as_str() {
 | 
			
		||||
                "low" => Some(TicketPriority::Low),
 | 
			
		||||
                "medium" => Some(TicketPriority::Medium),
 | 
			
		||||
                "high" => Some(TicketPriority::High),
 | 
			
		||||
                "critical" => Some(TicketPriority::Critical),
 | 
			
		||||
                _ => None,
 | 
			
		||||
            };
 | 
			
		||||
            if !priority_str.is_empty() {
 | 
			
		||||
                let priority = match priority_str.as_str() {
 | 
			
		||||
                    "low" => TicketPriority::Low,
 | 
			
		||||
                    "medium" => TicketPriority::Medium,
 | 
			
		||||
                    "high" => TicketPriority::High,
 | 
			
		||||
                    "urgent" => TicketPriority::Critical,
 | 
			
		||||
                    _ => TicketPriority::Medium,
 | 
			
		||||
                };
 | 
			
		||||
                
 | 
			
		||||
                filtered_tickets.retain(|ticket| ticket.priority == priority);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        filter.search_term = query.search.clone();
 | 
			
		||||
        
 | 
			
		||||
        // If the user is not an admin, only show their tickets
 | 
			
		||||
        // Regular users can only see their own tickets
 | 
			
		||||
        if user.role != crate::models::user::UserRole::Admin {
 | 
			
		||||
            filter.user_id = user.id;
 | 
			
		||||
            filtered_tickets.retain(|ticket| ticket.user_id == user.id.unwrap_or(0));
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Get the tickets that match the filter
 | 
			
		||||
        let tickets = {
 | 
			
		||||
            let tickets_map = TICKETS.lock().unwrap();
 | 
			
		||||
            tickets_map.values()
 | 
			
		||||
                .filter(|ticket| {
 | 
			
		||||
                    // Filter by status
 | 
			
		||||
                    if let Some(status) = &filter.status {
 | 
			
		||||
                        if ticket.status != *status {
 | 
			
		||||
                            return false;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    
 | 
			
		||||
                    // Filter by priority
 | 
			
		||||
                    if let Some(priority) = &filter.priority {
 | 
			
		||||
                        if ticket.priority != *priority {
 | 
			
		||||
                            return false;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    
 | 
			
		||||
                    // Filter by user ID
 | 
			
		||||
                    if let Some(user_id) = filter.user_id {
 | 
			
		||||
                        if ticket.user_id != user_id {
 | 
			
		||||
                            return false;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    
 | 
			
		||||
                    // Filter by search term
 | 
			
		||||
                    if let Some(term) = &filter.search_term {
 | 
			
		||||
                        if !ticket.title.to_lowercase().contains(&term.to_lowercase()) &&
 | 
			
		||||
                           !ticket.description.to_lowercase().contains(&term.to_lowercase()) {
 | 
			
		||||
                            return false;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    
 | 
			
		||||
                    true
 | 
			
		||||
                })
 | 
			
		||||
                .cloned()
 | 
			
		||||
                .collect::<Vec<_>>()
 | 
			
		||||
        };
 | 
			
		||||
        // Sort tickets by created_at (newest first)
 | 
			
		||||
        filtered_tickets.sort_by(|a, b| b.created_at.cmp(&a.created_at));
 | 
			
		||||
        
 | 
			
		||||
        // Prepare the template context
 | 
			
		||||
        let mut ctx = tera::Context::new();
 | 
			
		||||
        ctx.insert("active_page", "tickets");
 | 
			
		||||
        ctx.insert("tickets", &filtered_tickets);
 | 
			
		||||
        ctx.insert("user", &user);
 | 
			
		||||
        ctx.insert("tickets", &tickets);
 | 
			
		||||
        // Extract the query parameters for the template
 | 
			
		||||
        ctx.insert("status", &query.status);
 | 
			
		||||
        ctx.insert("priority", &query.priority);
 | 
			
		||||
        ctx.insert("search", &query.search);
 | 
			
		||||
        ctx.insert("filter", &query.into_inner());
 | 
			
		||||
        
 | 
			
		||||
        // Add filter options for the dropdown menus
 | 
			
		||||
        ctx.insert("statuses", &[
 | 
			
		||||
            ("", "All Statuses"),
 | 
			
		||||
            ("open", "Open"),
 | 
			
		||||
            ("in_progress", "In Progress"),
 | 
			
		||||
            ("resolved", "Resolved"),
 | 
			
		||||
            ("closed", "Closed"),
 | 
			
		||||
        ]);
 | 
			
		||||
        
 | 
			
		||||
        ctx.insert("priorities", &[
 | 
			
		||||
            ("", "All Priorities"),
 | 
			
		||||
            ("low", "Low"),
 | 
			
		||||
            ("medium", "Medium"),
 | 
			
		||||
            ("high", "High"),
 | 
			
		||||
            ("urgent", "Urgent"),
 | 
			
		||||
        ]);
 | 
			
		||||
        
 | 
			
		||||
        // Render the template
 | 
			
		||||
        let rendered = tmpl.render("tickets/list.html", &ctx)
 | 
			
		||||
@@ -158,15 +146,8 @@ impl TicketController {
 | 
			
		||||
        tmpl: web::Data<Tera>
 | 
			
		||||
    ) -> Result<impl Responder> {
 | 
			
		||||
        // Get the current user from the session
 | 
			
		||||
        let user = match session.get::<String>("user")? {
 | 
			
		||||
            Some(user_json) => {
 | 
			
		||||
                match serde_json::from_str::<User>(&user_json) {
 | 
			
		||||
                    Ok(user) => Some(user),
 | 
			
		||||
                    Err(_) => None,
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            None => None,
 | 
			
		||||
        };
 | 
			
		||||
        let user_value = Self::get_user_from_session(&session);
 | 
			
		||||
        let user: Option<User> = user_value.and_then(|v| serde_json::from_value(v).ok());
 | 
			
		||||
        
 | 
			
		||||
        // If the user is not logged in, redirect to the login page
 | 
			
		||||
        if user.is_none() {
 | 
			
		||||
@@ -175,19 +156,20 @@ impl TicketController {
 | 
			
		||||
                .finish());
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        let user = user.unwrap();
 | 
			
		||||
        let user: User = user.unwrap();
 | 
			
		||||
        
 | 
			
		||||
        // Prepare the template context
 | 
			
		||||
        let mut ctx = tera::Context::new();
 | 
			
		||||
        ctx.insert("active_page", "tickets");
 | 
			
		||||
        ctx.insert("user", &user);
 | 
			
		||||
        
 | 
			
		||||
        // Add an empty form to the context to avoid template errors
 | 
			
		||||
        ctx.insert("form", &serde_json::json!({
 | 
			
		||||
            "title": "",
 | 
			
		||||
            "priority": "medium",
 | 
			
		||||
            "description": ""
 | 
			
		||||
        }));
 | 
			
		||||
        // Add priority options for the dropdown menu
 | 
			
		||||
        ctx.insert("priorities", &[
 | 
			
		||||
            ("low", "Low"),
 | 
			
		||||
            ("medium", "Medium"),
 | 
			
		||||
            ("high", "High"),
 | 
			
		||||
            ("urgent", "Urgent"),
 | 
			
		||||
        ]);
 | 
			
		||||
        
 | 
			
		||||
        // Render the template
 | 
			
		||||
        let rendered = tmpl.render("tickets/new.html", &ctx)
 | 
			
		||||
@@ -206,15 +188,8 @@ impl TicketController {
 | 
			
		||||
        _tmpl: web::Data<Tera>
 | 
			
		||||
    ) -> Result<impl Responder> {
 | 
			
		||||
        // Get the current user from the session
 | 
			
		||||
        let user = match session.get::<String>("user")? {
 | 
			
		||||
            Some(user_json) => {
 | 
			
		||||
                match serde_json::from_str::<User>(&user_json) {
 | 
			
		||||
                    Ok(user) => Some(user),
 | 
			
		||||
                    Err(_) => None,
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            None => None,
 | 
			
		||||
        };
 | 
			
		||||
        let user_value = Self::get_user_from_session(&session);
 | 
			
		||||
        let user: Option<User> = user_value.and_then(|v| serde_json::from_value(v).ok());
 | 
			
		||||
        
 | 
			
		||||
        // If the user is not logged in, redirect to the login page
 | 
			
		||||
        if user.is_none() {
 | 
			
		||||
@@ -223,20 +198,18 @@ impl TicketController {
 | 
			
		||||
                .finish());
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        let user = user.unwrap();
 | 
			
		||||
        let user: User = user.unwrap();
 | 
			
		||||
        
 | 
			
		||||
        // Skip validation and always create the ticket
 | 
			
		||||
        
 | 
			
		||||
        // Parse the priority
 | 
			
		||||
        // Parse the priority from the form
 | 
			
		||||
        let priority = match form.priority.as_str() {
 | 
			
		||||
            "low" => TicketPriority::Low,
 | 
			
		||||
            "medium" => TicketPriority::Medium,
 | 
			
		||||
            "high" => TicketPriority::High,
 | 
			
		||||
            "critical" => TicketPriority::Critical,
 | 
			
		||||
            "urgent" => TicketPriority::Critical,
 | 
			
		||||
            _ => TicketPriority::Medium,
 | 
			
		||||
        };
 | 
			
		||||
        
 | 
			
		||||
        // Create the ticket
 | 
			
		||||
        // Create a new ticket
 | 
			
		||||
        let ticket = Ticket::new(
 | 
			
		||||
            user.id.unwrap_or(0),
 | 
			
		||||
            form.title.clone(),
 | 
			
		||||
@@ -244,15 +217,13 @@ impl TicketController {
 | 
			
		||||
            priority
 | 
			
		||||
        );
 | 
			
		||||
        
 | 
			
		||||
        // Store the ticket
 | 
			
		||||
        {
 | 
			
		||||
            let mut tickets_map = TICKETS.lock().unwrap();
 | 
			
		||||
            tickets_map.insert(ticket.id.clone(), ticket.clone());
 | 
			
		||||
        }
 | 
			
		||||
        // Add the ticket to the in-memory storage
 | 
			
		||||
        let mut tickets = TICKETS.lock().unwrap();
 | 
			
		||||
        tickets.insert(ticket.id.clone(), ticket.clone());
 | 
			
		||||
        
 | 
			
		||||
        // Redirect to the ticket detail page
 | 
			
		||||
        // Redirect to the ticket list page
 | 
			
		||||
        Ok(HttpResponse::Found()
 | 
			
		||||
            .append_header(("Location", format!("/tickets/{}", ticket.id)))
 | 
			
		||||
            .append_header(("Location", "/tickets"))
 | 
			
		||||
            .finish())
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
@@ -263,15 +234,8 @@ impl TicketController {
 | 
			
		||||
        tmpl: web::Data<Tera>
 | 
			
		||||
    ) -> Result<impl Responder> {
 | 
			
		||||
        // Get the current user from the session
 | 
			
		||||
        let user = match session.get::<String>("user")? {
 | 
			
		||||
            Some(user_json) => {
 | 
			
		||||
                match serde_json::from_str::<User>(&user_json) {
 | 
			
		||||
                    Ok(user) => Some(user),
 | 
			
		||||
                    Err(_) => None,
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            None => None,
 | 
			
		||||
        };
 | 
			
		||||
        let user_value = Self::get_user_from_session(&session);
 | 
			
		||||
        let user: Option<User> = user_value.and_then(|v| serde_json::from_value(v).ok());
 | 
			
		||||
        
 | 
			
		||||
        // If the user is not logged in, redirect to the login page
 | 
			
		||||
        if user.is_none() {
 | 
			
		||||
@@ -280,38 +244,45 @@ impl TicketController {
 | 
			
		||||
                .finish());
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        let user = user.unwrap();
 | 
			
		||||
        let ticket_id = path.into_inner();
 | 
			
		||||
        let user: User = user.unwrap();
 | 
			
		||||
        
 | 
			
		||||
        // Get the ticket
 | 
			
		||||
        let ticket = {
 | 
			
		||||
            let tickets_map = TICKETS.lock().unwrap();
 | 
			
		||||
            match tickets_map.get(&ticket_id) {
 | 
			
		||||
                Some(ticket) => ticket.clone(),
 | 
			
		||||
                None => {
 | 
			
		||||
                    // Ticket not found, redirect to the tickets list
 | 
			
		||||
                    return Ok(HttpResponse::Found()
 | 
			
		||||
                        .append_header(("Location", "/tickets"))
 | 
			
		||||
                        .finish());
 | 
			
		||||
                }
 | 
			
		||||
        // Get the ticket ID from the path
 | 
			
		||||
        let id = path.into_inner();
 | 
			
		||||
        
 | 
			
		||||
        // Get the ticket from the in-memory storage
 | 
			
		||||
        let tickets = TICKETS.lock().unwrap();
 | 
			
		||||
        let ticket = match tickets.get(&id) {
 | 
			
		||||
            Some(ticket) => ticket.clone(),
 | 
			
		||||
            None => {
 | 
			
		||||
                return Ok(HttpResponse::NotFound().finish());
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        
 | 
			
		||||
        // Regular users can only see their own tickets
 | 
			
		||||
        if user.role != crate::models::user::UserRole::Admin && ticket.user_id != user.id.unwrap_or(0) {
 | 
			
		||||
            return Ok(HttpResponse::Forbidden().finish());
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Get the comments for this ticket
 | 
			
		||||
        let comments = {
 | 
			
		||||
            let comments_map = COMMENTS.lock().unwrap();
 | 
			
		||||
            match comments_map.get(&ticket_id) {
 | 
			
		||||
                Some(comments) => comments.clone(),
 | 
			
		||||
                None => Vec::new(),
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        let comments = COMMENTS.lock().unwrap();
 | 
			
		||||
        let ticket_comments = comments.get(&ticket.id)
 | 
			
		||||
            .cloned()
 | 
			
		||||
            .unwrap_or_default();
 | 
			
		||||
        
 | 
			
		||||
        // Prepare the template context
 | 
			
		||||
        let mut ctx = tera::Context::new();
 | 
			
		||||
        ctx.insert("active_page", "tickets");
 | 
			
		||||
        ctx.insert("user", &user);
 | 
			
		||||
        ctx.insert("ticket", &ticket);
 | 
			
		||||
        ctx.insert("comments", &comments);
 | 
			
		||||
        ctx.insert("comments", &ticket_comments);
 | 
			
		||||
        ctx.insert("user", &user);
 | 
			
		||||
        
 | 
			
		||||
        // Add status options for the dropdown menu (for admins)
 | 
			
		||||
        ctx.insert("statuses", &[
 | 
			
		||||
            ("open", "Open"),
 | 
			
		||||
            ("in_progress", "In Progress"),
 | 
			
		||||
            ("resolved", "Resolved"),
 | 
			
		||||
            ("closed", "Closed"),
 | 
			
		||||
        ]);
 | 
			
		||||
        
 | 
			
		||||
        // Render the template
 | 
			
		||||
        let rendered = tmpl.render("tickets/show.html", &ctx)
 | 
			
		||||
@@ -330,15 +301,8 @@ impl TicketController {
 | 
			
		||||
        form: web::Form<NewCommentForm>
 | 
			
		||||
    ) -> Result<impl Responder> {
 | 
			
		||||
        // Get the current user from the session
 | 
			
		||||
        let user = match session.get::<String>("user")? {
 | 
			
		||||
            Some(user_json) => {
 | 
			
		||||
                match serde_json::from_str::<User>(&user_json) {
 | 
			
		||||
                    Ok(user) => Some(user),
 | 
			
		||||
                    Err(_) => None,
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            None => None,
 | 
			
		||||
        };
 | 
			
		||||
        let user_value = Self::get_user_from_session(&session);
 | 
			
		||||
        let user: Option<User> = user_value.and_then(|v| serde_json::from_value(v).ok());
 | 
			
		||||
        
 | 
			
		||||
        // If the user is not logged in, redirect to the login page
 | 
			
		||||
        if user.is_none() {
 | 
			
		||||
@@ -347,62 +311,53 @@ impl TicketController {
 | 
			
		||||
                .finish());
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        let user = user.unwrap();
 | 
			
		||||
        let ticket_id = path.into_inner();
 | 
			
		||||
        let user: User = user.unwrap();
 | 
			
		||||
        
 | 
			
		||||
        // Validate the form data
 | 
			
		||||
        if form.content.trim().is_empty() {
 | 
			
		||||
            // Comment is empty, redirect back to the ticket
 | 
			
		||||
            return Ok(HttpResponse::Found()
 | 
			
		||||
                .append_header(("Location", format!("/tickets/{}", ticket_id)))
 | 
			
		||||
                .finish());
 | 
			
		||||
        }
 | 
			
		||||
        // Get the ticket ID from the path
 | 
			
		||||
        let id = path.into_inner();
 | 
			
		||||
        
 | 
			
		||||
        // Check if the ticket exists
 | 
			
		||||
        {
 | 
			
		||||
            let tickets_map = TICKETS.lock().unwrap();
 | 
			
		||||
            if !tickets_map.contains_key(&ticket_id) {
 | 
			
		||||
                // Ticket not found, redirect to the tickets list
 | 
			
		||||
                return Ok(HttpResponse::Found()
 | 
			
		||||
                    .append_header(("Location", "/tickets"))
 | 
			
		||||
                    .finish());
 | 
			
		||||
        // Get the ticket from the in-memory storage
 | 
			
		||||
        let tickets = TICKETS.lock().unwrap();
 | 
			
		||||
        let ticket = match tickets.get(&id) {
 | 
			
		||||
            Some(ticket) => ticket.clone(),
 | 
			
		||||
            None => {
 | 
			
		||||
                return Ok(HttpResponse::NotFound().finish());
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        
 | 
			
		||||
        // Regular users can only comment on their own tickets
 | 
			
		||||
        if user.role != crate::models::user::UserRole::Admin && ticket.user_id != user.id.unwrap_or(0) {
 | 
			
		||||
            return Ok(HttpResponse::Forbidden().finish());
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Create the comment
 | 
			
		||||
        // Create a new comment
 | 
			
		||||
        let comment = TicketComment::new(
 | 
			
		||||
            ticket_id.clone(),
 | 
			
		||||
            ticket.id.clone(),
 | 
			
		||||
            user.id.unwrap_or(0),
 | 
			
		||||
            form.content.clone(),
 | 
			
		||||
            user.is_admin()
 | 
			
		||||
            user.role == crate::models::user::UserRole::Admin
 | 
			
		||||
        );
 | 
			
		||||
        
 | 
			
		||||
        // Store the comment
 | 
			
		||||
        {
 | 
			
		||||
            let mut comments_map = COMMENTS.lock().unwrap();
 | 
			
		||||
            if let Some(comments) = comments_map.get_mut(&ticket_id) {
 | 
			
		||||
                comments.push(comment);
 | 
			
		||||
            } else {
 | 
			
		||||
                comments_map.insert(ticket_id.clone(), vec![comment]);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Update the ticket status if the user is an admin
 | 
			
		||||
        if user.role == crate::models::user::UserRole::Admin {
 | 
			
		||||
            let mut tickets_map = TICKETS.lock().unwrap();
 | 
			
		||||
            if let Some(ticket) = tickets_map.get_mut(&ticket_id) {
 | 
			
		||||
                ticket.update_status(TicketStatus::WaitingForCustomer);
 | 
			
		||||
            }
 | 
			
		||||
        // Add the comment to the in-memory storage
 | 
			
		||||
        let mut comments = COMMENTS.lock().unwrap();
 | 
			
		||||
        if let Some(ticket_comments) = comments.get_mut(&ticket.id) {
 | 
			
		||||
            ticket_comments.push(comment);
 | 
			
		||||
        } else {
 | 
			
		||||
            let mut tickets_map = TICKETS.lock().unwrap();
 | 
			
		||||
            if let Some(ticket) = tickets_map.get_mut(&ticket_id) {
 | 
			
		||||
                ticket.update_status(TicketStatus::Open);
 | 
			
		||||
            comments.insert(ticket.id.clone(), vec![comment]);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // If the ticket is closed, reopen it when a comment is added
 | 
			
		||||
        if ticket.status == TicketStatus::Closed {
 | 
			
		||||
            let mut tickets = TICKETS.lock().unwrap();
 | 
			
		||||
            if let Some(ticket) = tickets.get_mut(&id) {
 | 
			
		||||
                ticket.status = TicketStatus::Open;
 | 
			
		||||
                ticket.updated_at = chrono::Utc::now();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Redirect back to the ticket
 | 
			
		||||
        // Redirect back to the ticket page
 | 
			
		||||
        Ok(HttpResponse::Found()
 | 
			
		||||
            .append_header(("Location", format!("/tickets/{}", ticket_id)))
 | 
			
		||||
            .append_header(("Location", format!("/tickets/{}", id)))
 | 
			
		||||
            .finish())
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
@@ -412,15 +367,8 @@ impl TicketController {
 | 
			
		||||
        path: web::Path<(String, String)>,
 | 
			
		||||
    ) -> Result<impl Responder> {
 | 
			
		||||
        // Get the current user from the session
 | 
			
		||||
        let user = match session.get::<String>("user")? {
 | 
			
		||||
            Some(user_json) => {
 | 
			
		||||
                match serde_json::from_str::<User>(&user_json) {
 | 
			
		||||
                    Ok(user) => Some(user),
 | 
			
		||||
                    Err(_) => None,
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            None => None,
 | 
			
		||||
        };
 | 
			
		||||
        let user_value = Self::get_user_from_session(&session);
 | 
			
		||||
        let user: Option<User> = user_value.and_then(|v| serde_json::from_value(v).ok());
 | 
			
		||||
        
 | 
			
		||||
        // If the user is not logged in or not an admin, redirect to the login page
 | 
			
		||||
        if user.is_none() || user.as_ref().unwrap().role != crate::models::user::UserRole::Admin {
 | 
			
		||||
@@ -429,39 +377,30 @@ impl TicketController {
 | 
			
		||||
                .finish());
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        let (ticket_id, status_str) = path.into_inner();
 | 
			
		||||
        // Get the ticket ID and status from the path
 | 
			
		||||
        let (id, status_str) = path.into_inner();
 | 
			
		||||
        
 | 
			
		||||
        // Parse the status
 | 
			
		||||
        let status = match status_str.as_str() {
 | 
			
		||||
            "open" => TicketStatus::Open,
 | 
			
		||||
            "in_progress" => TicketStatus::InProgress,
 | 
			
		||||
            "waiting_for_customer" => TicketStatus::WaitingForCustomer,
 | 
			
		||||
            "resolved" => TicketStatus::Resolved,
 | 
			
		||||
            "closed" => TicketStatus::Closed,
 | 
			
		||||
            _ => {
 | 
			
		||||
                // Invalid status, redirect back to the ticket
 | 
			
		||||
                return Ok(HttpResponse::Found()
 | 
			
		||||
                    .append_header(("Location", format!("/tickets/{}", ticket_id)))
 | 
			
		||||
                    .finish());
 | 
			
		||||
            }
 | 
			
		||||
            _ => TicketStatus::Open,
 | 
			
		||||
        };
 | 
			
		||||
        
 | 
			
		||||
        // Update the ticket status
 | 
			
		||||
        {
 | 
			
		||||
            let mut tickets_map = TICKETS.lock().unwrap();
 | 
			
		||||
            if let Some(ticket) = tickets_map.get_mut(&ticket_id) {
 | 
			
		||||
                ticket.update_status(status);
 | 
			
		||||
            } else {
 | 
			
		||||
                // Ticket not found, redirect to the tickets list
 | 
			
		||||
                return Ok(HttpResponse::Found()
 | 
			
		||||
                    .append_header(("Location", "/tickets"))
 | 
			
		||||
                    .finish());
 | 
			
		||||
            }
 | 
			
		||||
        let mut tickets = TICKETS.lock().unwrap();
 | 
			
		||||
        if let Some(ticket) = tickets.get_mut(&id) {
 | 
			
		||||
            ticket.status = status;
 | 
			
		||||
            ticket.updated_at = chrono::Utc::now();
 | 
			
		||||
        } else {
 | 
			
		||||
            return Ok(HttpResponse::NotFound().finish());
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Redirect back to the ticket
 | 
			
		||||
        // Redirect back to the ticket page
 | 
			
		||||
        Ok(HttpResponse::Found()
 | 
			
		||||
            .append_header(("Location", format!("/tickets/{}", ticket_id)))
 | 
			
		||||
            .append_header(("Location", format!("/tickets/{}", id)))
 | 
			
		||||
            .finish())
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
@@ -471,15 +410,8 @@ impl TicketController {
 | 
			
		||||
        tmpl: web::Data<Tera>
 | 
			
		||||
    ) -> Result<impl Responder> {
 | 
			
		||||
        // Get the current user from the session
 | 
			
		||||
        let user = match session.get::<String>("user")? {
 | 
			
		||||
            Some(user_json) => {
 | 
			
		||||
                match serde_json::from_str::<User>(&user_json) {
 | 
			
		||||
                    Ok(user) => Some(user),
 | 
			
		||||
                    Err(_) => None,
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            None => None,
 | 
			
		||||
        };
 | 
			
		||||
        let user_value = Self::get_user_from_session(&session);
 | 
			
		||||
        let user: Option<User> = user_value.and_then(|v| serde_json::from_value(v).ok());
 | 
			
		||||
        
 | 
			
		||||
        // If the user is not logged in, redirect to the login page
 | 
			
		||||
        if user.is_none() {
 | 
			
		||||
@@ -488,25 +420,30 @@ impl TicketController {
 | 
			
		||||
                .finish());
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        let user = user.unwrap();
 | 
			
		||||
        let user: User = user.unwrap();
 | 
			
		||||
        
 | 
			
		||||
        // Get the user's tickets
 | 
			
		||||
        let tickets = {
 | 
			
		||||
            let tickets_map = TICKETS.lock().unwrap();
 | 
			
		||||
            tickets_map.values()
 | 
			
		||||
                .filter(|ticket| ticket.user_id == user.id.unwrap_or(0))
 | 
			
		||||
                .cloned()
 | 
			
		||||
                .collect::<Vec<_>>()
 | 
			
		||||
        };
 | 
			
		||||
        // Get all tickets from the in-memory storage
 | 
			
		||||
        let tickets = TICKETS.lock().unwrap();
 | 
			
		||||
        
 | 
			
		||||
        // Filter tickets to only show the user's tickets
 | 
			
		||||
        let my_tickets: Vec<Ticket> = tickets.values()
 | 
			
		||||
            .cloned()
 | 
			
		||||
            .filter(|ticket| ticket.user_id == user.id.unwrap_or(0))
 | 
			
		||||
            .collect();
 | 
			
		||||
        
 | 
			
		||||
        // Sort tickets by created_at (newest first)
 | 
			
		||||
        let mut sorted_tickets = my_tickets;
 | 
			
		||||
        sorted_tickets.sort_by(|a, b| b.created_at.cmp(&a.created_at));
 | 
			
		||||
        
 | 
			
		||||
        // Prepare the template context
 | 
			
		||||
        let mut ctx = tera::Context::new();
 | 
			
		||||
        ctx.insert("active_page", "tickets");
 | 
			
		||||
        ctx.insert("tickets", &sorted_tickets);
 | 
			
		||||
        ctx.insert("user", &user);
 | 
			
		||||
        ctx.insert("tickets", &tickets);
 | 
			
		||||
        ctx.insert("my_tickets", &true);
 | 
			
		||||
        
 | 
			
		||||
        // Render the template
 | 
			
		||||
        let rendered = tmpl.render("tickets/my_tickets.html", &ctx)
 | 
			
		||||
        let rendered = tmpl.render("tickets/list.html", &ctx)
 | 
			
		||||
            .map_err(|e| {
 | 
			
		||||
                eprintln!("Template rendering error: {}", e);
 | 
			
		||||
                actix_web::error::ErrorInternalServerError("Template rendering error")
 | 
			
		||||
 
 | 
			
		||||
@@ -3,22 +3,42 @@ use actix_web::{App, HttpServer, web};
 | 
			
		||||
use actix_web::middleware::Logger;
 | 
			
		||||
use tera::Tera;
 | 
			
		||||
use std::io;
 | 
			
		||||
use std::env;
 | 
			
		||||
use lazy_static::lazy_static;
 | 
			
		||||
 | 
			
		||||
mod config;
 | 
			
		||||
mod controllers;
 | 
			
		||||
mod app_middleware;
 | 
			
		||||
mod middleware;
 | 
			
		||||
mod models;
 | 
			
		||||
mod routes;
 | 
			
		||||
mod utils;
 | 
			
		||||
 | 
			
		||||
// Import middleware components
 | 
			
		||||
use app_middleware::{RequestTimer, SecurityHeaders};
 | 
			
		||||
use middleware::{RequestTimer, SecurityHeaders, JwtAuth};
 | 
			
		||||
use utils::redis_service;
 | 
			
		||||
 | 
			
		||||
// Initialize lazy_static for in-memory storage
 | 
			
		||||
#[macro_use]
 | 
			
		||||
extern crate lazy_static;
 | 
			
		||||
 | 
			
		||||
// Create a consistent session key
 | 
			
		||||
lazy_static! {
 | 
			
		||||
    pub static ref SESSION_KEY: actix_web::cookie::Key = {
 | 
			
		||||
        // In production, this should be a proper secret key from environment variables
 | 
			
		||||
        let secret = std::env::var("SESSION_SECRET").unwrap_or_else(|_| {
 | 
			
		||||
            // Create a key that's at least 64 bytes long
 | 
			
		||||
            "my_secret_session_key_that_is_at_least_64_bytes_long_for_security_reasons_1234567890abcdef".to_string()
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        // Ensure the key is at least 64 bytes
 | 
			
		||||
        let mut key_bytes = secret.as_bytes().to_vec();
 | 
			
		||||
        while key_bytes.len() < 64 {
 | 
			
		||||
            key_bytes.extend_from_slice(b"0123456789abcdef");
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        actix_web::cookie::Key::from(&key_bytes[0..64])
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[actix_web::main]
 | 
			
		||||
async fn main() -> io::Result<()> {
 | 
			
		||||
    // Initialize environment
 | 
			
		||||
@@ -27,7 +47,21 @@ async fn main() -> io::Result<()> {
 | 
			
		||||
    
 | 
			
		||||
    // Load configuration
 | 
			
		||||
    let config = config::get_config();
 | 
			
		||||
    let bind_address = format!("{}:{}", config.server.host, config.server.port);
 | 
			
		||||
    
 | 
			
		||||
    // Check for port override from command line arguments
 | 
			
		||||
    let args: Vec<String> = env::args().collect();
 | 
			
		||||
    let mut port = config.server.port;
 | 
			
		||||
    
 | 
			
		||||
    for i in 1..args.len() {
 | 
			
		||||
        if args[i] == "--port" && i + 1 < args.len() {
 | 
			
		||||
            if let Ok(p) = args[i + 1].parse::<u16>() {
 | 
			
		||||
                port = p;
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    let bind_address = format!("{}:{}", config.server.host, port);
 | 
			
		||||
    
 | 
			
		||||
    // Initialize Redis client
 | 
			
		||||
    let redis_url = std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
 | 
			
		||||
@@ -60,6 +94,7 @@ async fn main() -> io::Result<()> {
 | 
			
		||||
            // Add custom middleware
 | 
			
		||||
            .wrap(RequestTimer)
 | 
			
		||||
            .wrap(SecurityHeaders)
 | 
			
		||||
            .wrap(JwtAuth)
 | 
			
		||||
            // Configure static files
 | 
			
		||||
            .service(fs::Files::new("/static", "./src/static"))
 | 
			
		||||
            // Add Tera template engine
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,14 @@
 | 
			
		||||
use actix_web::{
 | 
			
		||||
    dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
 | 
			
		||||
    Error,
 | 
			
		||||
    Error, HttpMessage,
 | 
			
		||||
};
 | 
			
		||||
use futures::future::{ready, LocalBoxFuture, Ready};
 | 
			
		||||
use std::{
 | 
			
		||||
    future::Future,
 | 
			
		||||
    pin::Pin,
 | 
			
		||||
    task::{Context, Poll},
 | 
			
		||||
    time::Instant,
 | 
			
		||||
};
 | 
			
		||||
use actix_session::SessionExt;
 | 
			
		||||
 | 
			
		||||
/// Middleware for logging request duration
 | 
			
		||||
pub struct RequestTimer;
 | 
			
		||||
@@ -127,4 +127,137 @@ where
 | 
			
		||||
            Ok(res)
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Middleware for JWT authentication
 | 
			
		||||
pub struct JwtAuth;
 | 
			
		||||
 | 
			
		||||
impl<S, B> Transform<S, ServiceRequest> for JwtAuth
 | 
			
		||||
where
 | 
			
		||||
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
 | 
			
		||||
    S::Future: 'static,
 | 
			
		||||
    B: 'static,
 | 
			
		||||
{
 | 
			
		||||
    type Response = ServiceResponse<B>;
 | 
			
		||||
    type Error = Error;
 | 
			
		||||
    type Transform = JwtAuthMiddleware<S>;
 | 
			
		||||
    type InitError = ();
 | 
			
		||||
    type Future = Ready<Result<Self::Transform, Self::InitError>>;
 | 
			
		||||
 | 
			
		||||
    fn new_transform(&self, service: S) -> Self::Future {
 | 
			
		||||
        ready(Ok(JwtAuthMiddleware { service }))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct JwtAuthMiddleware<S> {
 | 
			
		||||
    service: S,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<S, B> Service<ServiceRequest> for JwtAuthMiddleware<S>
 | 
			
		||||
where
 | 
			
		||||
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
 | 
			
		||||
    S::Future: 'static,
 | 
			
		||||
    B: 'static,
 | 
			
		||||
{
 | 
			
		||||
    type Response = ServiceResponse<B>;
 | 
			
		||||
    type Error = Error;
 | 
			
		||||
    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
 | 
			
		||||
 | 
			
		||||
    forward_ready!(service);
 | 
			
		||||
 | 
			
		||||
    fn call(&self, req: ServiceRequest) -> Self::Future {
 | 
			
		||||
        // Define public routes that don't require authentication
 | 
			
		||||
        let path = req.path().to_string();
 | 
			
		||||
        let public_routes = vec![
 | 
			
		||||
            "/login", 
 | 
			
		||||
            "/register", 
 | 
			
		||||
            "/static", 
 | 
			
		||||
            "/favicon.ico",
 | 
			
		||||
            "/",
 | 
			
		||||
            "/about",
 | 
			
		||||
            "/contact"
 | 
			
		||||
        ];
 | 
			
		||||
        
 | 
			
		||||
        // Check if the current path is a public route
 | 
			
		||||
        let is_public_route = public_routes.iter().any(|route| path.starts_with(route));
 | 
			
		||||
        
 | 
			
		||||
        if is_public_route {
 | 
			
		||||
            // For public routes, just pass through without authentication check
 | 
			
		||||
            let fut = self.service.call(req);
 | 
			
		||||
            return Box::pin(async move {
 | 
			
		||||
                fut.await
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // First try to get token from cookie
 | 
			
		||||
        let cookie_token = crate::controllers::auth::AuthController::extract_token_from_cookie(req.request());
 | 
			
		||||
        
 | 
			
		||||
        // If no cookie token, try to get from session
 | 
			
		||||
        let session = req.get_session();
 | 
			
		||||
        let session_token = crate::controllers::auth::AuthController::extract_token_from_session(&session);
 | 
			
		||||
        
 | 
			
		||||
        // Use cookie token if available, otherwise use session token
 | 
			
		||||
        let token = cookie_token.or(session_token);
 | 
			
		||||
        
 | 
			
		||||
        // Process based on token availability
 | 
			
		||||
        if let Some(token_str) = token {
 | 
			
		||||
            // Validate the token
 | 
			
		||||
            let validation_result = crate::controllers::auth::AuthController::validate_token(&token_str);
 | 
			
		||||
            
 | 
			
		||||
            match validation_result {
 | 
			
		||||
                Ok(claims) => {
 | 
			
		||||
                    // Token is valid, store claims in request extensions
 | 
			
		||||
                    req.extensions_mut().insert(claims.clone());
 | 
			
		||||
                    
 | 
			
		||||
                    // Create a user from claims and store in session
 | 
			
		||||
                    let mut user = crate::models::User::new(
 | 
			
		||||
                        claims.sub.clone(),
 | 
			
		||||
                        claims.sub.clone()
 | 
			
		||||
                    );
 | 
			
		||||
                    
 | 
			
		||||
                    // Set the user ID and role
 | 
			
		||||
                    user.id = Some(1);
 | 
			
		||||
                    user.role = if claims.role == "admin" {
 | 
			
		||||
                        crate::models::user::UserRole::Admin
 | 
			
		||||
                    } else {
 | 
			
		||||
                        crate::models::user::UserRole::User
 | 
			
		||||
                    };
 | 
			
		||||
                    
 | 
			
		||||
                    // Store user data in session
 | 
			
		||||
                    if let Ok(user_json) = serde_json::to_string(&user) {
 | 
			
		||||
                        let _ = session.insert("user", &user_json);
 | 
			
		||||
                        let _ = session.insert("auth_token", &token_str);
 | 
			
		||||
                    }
 | 
			
		||||
                    
 | 
			
		||||
                    let fut = self.service.call(req);
 | 
			
		||||
                    Box::pin(async move {
 | 
			
		||||
                        fut.await
 | 
			
		||||
                    })
 | 
			
		||||
                }
 | 
			
		||||
                Err(_) => {
 | 
			
		||||
                    // Token is invalid, redirect to login
 | 
			
		||||
                    Box::pin(async move {
 | 
			
		||||
                        // Return an error that will be handled by the error handlers
 | 
			
		||||
                        Err(actix_web::error::InternalError::from_response(
 | 
			
		||||
                            "JWT validation failed",
 | 
			
		||||
                            actix_web::HttpResponse::Found()
 | 
			
		||||
                                .append_header((actix_web::http::header::LOCATION, "/login"))
 | 
			
		||||
                                .finish()
 | 
			
		||||
                        ).into())
 | 
			
		||||
                    })
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            // No token found, redirect to login
 | 
			
		||||
            Box::pin(async move {
 | 
			
		||||
                // Return an error that will be handled by the error handlers
 | 
			
		||||
                Err(actix_web::error::InternalError::from_response(
 | 
			
		||||
                    "No JWT token found",
 | 
			
		||||
                    actix_web::HttpResponse::Found()
 | 
			
		||||
                        .append_header((actix_web::http::header::LOCATION, "/login"))
 | 
			
		||||
                        .finish()
 | 
			
		||||
                ).into())
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,23 +1,23 @@
 | 
			
		||||
use actix_web::web;
 | 
			
		||||
use actix_session::{SessionMiddleware, storage::CookieSessionStore};
 | 
			
		||||
use actix_web::cookie::Key;
 | 
			
		||||
use crate::controllers::home::HomeController;
 | 
			
		||||
use crate::controllers::auth::AuthController;
 | 
			
		||||
use crate::controllers::ticket::TicketController;
 | 
			
		||||
use crate::controllers::calendar::CalendarController;
 | 
			
		||||
use crate::middleware::JwtAuth;
 | 
			
		||||
use crate::SESSION_KEY;
 | 
			
		||||
 | 
			
		||||
/// Configures all application routes
 | 
			
		||||
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
 | 
			
		||||
    // Generate a random key for cookie encryption
 | 
			
		||||
    let key = Key::generate();
 | 
			
		||||
 | 
			
		||||
    // Configure session middleware with cookie store
 | 
			
		||||
    let session_middleware = SessionMiddleware::new(
 | 
			
		||||
    // Configure session middleware with the consistent key
 | 
			
		||||
    let session_middleware = SessionMiddleware::builder(
 | 
			
		||||
        CookieSessionStore::default(),
 | 
			
		||||
        key.clone()
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
        SESSION_KEY.clone()
 | 
			
		||||
    )
 | 
			
		||||
    .cookie_secure(false) // Set to true in production with HTTPS
 | 
			
		||||
    .build();
 | 
			
		||||
 | 
			
		||||
    // Public routes that don't require authentication
 | 
			
		||||
    cfg.service(
 | 
			
		||||
        web::scope("")
 | 
			
		||||
            .wrap(session_middleware)
 | 
			
		||||
@@ -26,7 +26,6 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) {
 | 
			
		||||
            .route("/about", web::get().to(HomeController::about))
 | 
			
		||||
            .route("/contact", web::get().to(HomeController::contact))
 | 
			
		||||
            .route("/contact", web::post().to(HomeController::submit_contact))
 | 
			
		||||
            .route("/editor", web::get().to(HomeController::editor))
 | 
			
		||||
            
 | 
			
		||||
            // Auth routes
 | 
			
		||||
            .route("/login", web::get().to(AuthController::login_page))
 | 
			
		||||
@@ -35,19 +34,29 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) {
 | 
			
		||||
            .route("/register", web::post().to(AuthController::register))
 | 
			
		||||
            .route("/logout", web::get().to(AuthController::logout))
 | 
			
		||||
            
 | 
			
		||||
            // Protected routes that require authentication
 | 
			
		||||
            // These routes will be protected by the JwtAuth middleware in the main.rs file
 | 
			
		||||
            .route("/editor", web::get().to(HomeController::editor))
 | 
			
		||||
            
 | 
			
		||||
            // Ticket routes
 | 
			
		||||
            .route("/tickets", web::get().to(TicketController::list_tickets))
 | 
			
		||||
            .route("/tickets/new", web::get().to(TicketController::new_ticket))
 | 
			
		||||
            .route("/tickets/new", web::post().to(TicketController::create_ticket))
 | 
			
		||||
            .route("/tickets/my", web::get().to(TicketController::my_tickets))
 | 
			
		||||
            .route("/tickets", web::post().to(TicketController::create_ticket))
 | 
			
		||||
            .route("/tickets/{id}", web::get().to(TicketController::show_ticket))
 | 
			
		||||
            .route("/tickets/{id}/comment", web::post().to(TicketController::add_comment))
 | 
			
		||||
            .route("/tickets/{id}/status/{status}", web::get().to(TicketController::update_status))
 | 
			
		||||
            .route("/tickets/{id}/status/{status}", web::post().to(TicketController::update_status))
 | 
			
		||||
            .route("/my-tickets", web::get().to(TicketController::my_tickets))
 | 
			
		||||
            
 | 
			
		||||
            // Calendar routes
 | 
			
		||||
            .route("/calendar", web::get().to(CalendarController::calendar))
 | 
			
		||||
            .route("/calendar/new", web::get().to(CalendarController::new_event))
 | 
			
		||||
            .route("/calendar/new", web::post().to(CalendarController::create_event))
 | 
			
		||||
            .route("/calendar/{id}/delete", web::get().to(CalendarController::delete_event))
 | 
			
		||||
            .route("/calendar/events/new", web::get().to(CalendarController::new_event))
 | 
			
		||||
            .route("/calendar/events", web::post().to(CalendarController::create_event))
 | 
			
		||||
            .route("/calendar/events/{id}/delete", web::post().to(CalendarController::delete_event))
 | 
			
		||||
    );
 | 
			
		||||
    
 | 
			
		||||
    // Keep the /protected scope for any future routes that should be under that path
 | 
			
		||||
    cfg.service(
 | 
			
		||||
        web::scope("/protected")
 | 
			
		||||
            .wrap(JwtAuth)  // Apply JWT authentication middleware
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
use chrono::{DateTime, TimeZone, Utc};
 | 
			
		||||
use chrono::{DateTime, Utc};
 | 
			
		||||
use tera::{self, Function, Result, Value};
 | 
			
		||||
 | 
			
		||||
// Export modules
 | 
			
		||||
 
 | 
			
		||||
@@ -133,10 +133,7 @@ impl RedisCalendarService {
 | 
			
		||||
        // Filter events that fall within the date range
 | 
			
		||||
        let filtered_events = all_events
 | 
			
		||||
            .into_iter()
 | 
			
		||||
            .filter(|event| {
 | 
			
		||||
                // Check if the event overlaps with the date range
 | 
			
		||||
                (event.start_time <= end && event.end_time >= start)
 | 
			
		||||
            })
 | 
			
		||||
            .filter(|event| event.start_time <= end && event.end_time >= start)
 | 
			
		||||
            .collect();
 | 
			
		||||
        
 | 
			
		||||
        Ok(filtered_events)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user