From 36812e417869834b5f0b8ba1c0eb15d15707ec01 Mon Sep 17 00:00:00 2001 From: Timur Gordon <31495328+timurgordon@users.noreply.github.com> Date: Mon, 21 Apr 2025 13:55:29 +0200 Subject: [PATCH] add jwt auth, fix session handling, clean up middlewares --- actix_mvc_app/Cargo.lock | 98 +++++ actix_mvc_app/Cargo.toml | 1 + actix_mvc_app/src/app_middleware/mod.rs | 129 ------- actix_mvc_app/src/controllers/auth.rs | 146 +++++++- actix_mvc_app/src/controllers/calendar.rs | 35 +- actix_mvc_app/src/controllers/home.rs | 48 ++- actix_mvc_app/src/controllers/mod.rs | 8 +- actix_mvc_app/src/controllers/ticket.rs | 425 +++++++++------------- actix_mvc_app/src/main.rs | 43 ++- actix_mvc_app/src/middleware/mod.rs | 137 ++++++- actix_mvc_app/src/routes/mod.rs | 41 ++- actix_mvc_app/src/utils/mod.rs | 2 +- actix_mvc_app/src/utils/redis_service.rs | 5 +- 13 files changed, 680 insertions(+), 438 deletions(-) delete mode 100644 actix_mvc_app/src/app_middleware/mod.rs diff --git a/actix_mvc_app/Cargo.lock b/actix_mvc_app/Cargo.lock index 18b2ba5..d3cbc8b 100644 --- a/actix_mvc_app/Cargo.lock +++ b/actix_mvc_app/Cargo.lock @@ -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" diff --git a/actix_mvc_app/Cargo.toml b/actix_mvc_app/Cargo.toml index 9e2cce4..47f2a0b 100644 --- a/actix_mvc_app/Cargo.toml +++ b/actix_mvc_app/Cargo.toml @@ -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" diff --git a/actix_mvc_app/src/app_middleware/mod.rs b/actix_mvc_app/src/app_middleware/mod.rs deleted file mode 100644 index fcbc5b0..0000000 --- a/actix_mvc_app/src/app_middleware/mod.rs +++ /dev/null @@ -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 Transform for RequestTimer -where - S: Service, Error = Error>, - S::Future: 'static, - B: 'static, -{ - type Response = ServiceResponse; - type Error = Error; - type Transform = RequestTimerMiddleware; - type InitError = (); - type Future = Ready>; - - fn new_transform(&self, service: S) -> Self::Future { - ready(Ok(RequestTimerMiddleware { service })) - } -} - -pub struct RequestTimerMiddleware { - service: S, -} - -impl Service for RequestTimerMiddleware -where - S: Service, Error = Error>, - S::Future: 'static, - B: 'static, -{ - type Response = ServiceResponse; - type Error = Error; - type Future = LocalBoxFuture<'static, Result>; - - 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 Transform for SecurityHeaders -where - S: Service, Error = Error>, - S::Future: 'static, - B: 'static, -{ - type Response = ServiceResponse; - type Error = Error; - type Transform = SecurityHeadersMiddleware; - type InitError = (); - type Future = Ready>; - - fn new_transform(&self, service: S) -> Self::Future { - ready(Ok(SecurityHeadersMiddleware { service })) - } -} - -pub struct SecurityHeadersMiddleware { - service: S, -} - -impl Service for SecurityHeadersMiddleware -where - S: Service, Error = Error>, - S::Future: 'static, - B: 'static, -{ - type Response = ServiceResponse; - type Error = Error; - type Future = Pin, 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) - }) - } -} \ No newline at end of file diff --git a/actix_mvc_app/src/controllers/auth.rs b/actix_mvc_app/src/controllers/auth.rs index 4711133..eb072f4 100644 --- a/actix_mvc_app/src/controllers/auth.rs +++ b/actix_mvc_app/src/controllers/auth.rs @@ -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 { + 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 { + let validation = Validation::new(Algorithm::HS256); + + let token_data = decode::( + 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 { + session.get::("auth_token").ok().flatten() + } + + /// Extract token from cookie + pub fn extract_token_from_cookie(req: &actix_web::HttpRequest) -> Option { + 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 { + 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) -> Result { let mut ctx = tera::Context::new(); @@ -27,10 +106,7 @@ impl AuthController { session: Session, _tmpl: web::Data ) -> Result { - // 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()) } } \ No newline at end of file diff --git a/actix_mvc_app/src/controllers/calendar.rs b/actix_mvc_app/src/controllers/calendar.rs index e59e09e..646644a 100644 --- a/actix_mvc_app/src/controllers/calendar.rs +++ b/actix_mvc_app/src/controllers/calendar.rs @@ -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 { + session.get::("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, query: web::Query, + _session: Session, ) -> Result { 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) -> Result { + pub async fn new_event(tmpl: web::Data, _session: Session) -> Result { 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, tmpl: web::Data, + _session: Session, ) -> Result { // 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, + _session: Session, ) -> Result { let id = path.into_inner(); diff --git a/actix_mvc_app/src/controllers/home.rs b/actix_mvc_app/src/controllers/home.rs index fc08b31..114be0d 100644 --- a/actix_mvc_app/src/controllers/home.rs +++ b/actix_mvc_app/src/controllers/home.rs @@ -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 { + session.get::("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) -> Result { + pub async fn editor(tmpl: web::Data, session: Session) -> Result { 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) -> Result { + pub async fn index(tmpl: web::Data, session: Session) -> Result { 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) -> Result { + pub async fn about(tmpl: web::Data, session: Session) -> Result { 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) -> Result { + pub async fn contact(tmpl: web::Data, session: Session) -> Result { 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, - tmpl: web::Data + tmpl: web::Data, + session: Session ) -> Result { // 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); diff --git a/actix_mvc_app/src/controllers/mod.rs b/actix_mvc_app/src/controllers/mod.rs index 9152455..509119f 100644 --- a/actix_mvc_app/src/controllers/mod.rs +++ b/actix_mvc_app/src/controllers/mod.rs @@ -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; \ No newline at end of file +pub mod calendar; \ No newline at end of file diff --git a/actix_mvc_app/src/controllers/ticket.rs b/actix_mvc_app/src/controllers/ticket.rs index 5b39092..ef95769 100644 --- a/actix_mvc_app/src/controllers/ticket.rs +++ b/actix_mvc_app/src/controllers/ticket.rs @@ -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 { + session.get::("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 ) -> Result { // Get the current user from the session - let user = match session.get::("user")? { - Some(user_json) => { - match serde_json::from_str::(&user_json) { - Ok(user) => Some(user), - Err(_) => None, - } - }, - None => None, - }; + let user_value = Self::get_user_from_session(&session); + let user: Option = 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 = 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::>() - }; + // 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 ) -> Result { // Get the current user from the session - let user = match session.get::("user")? { - Some(user_json) => { - match serde_json::from_str::(&user_json) { - Ok(user) => Some(user), - Err(_) => None, - } - }, - None => None, - }; + let user_value = Self::get_user_from_session(&session); + let user: Option = 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 ) -> Result { // Get the current user from the session - let user = match session.get::("user")? { - Some(user_json) => { - match serde_json::from_str::(&user_json) { - Ok(user) => Some(user), - Err(_) => None, - } - }, - None => None, - }; + let user_value = Self::get_user_from_session(&session); + let user: Option = 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 ) -> Result { // Get the current user from the session - let user = match session.get::("user")? { - Some(user_json) => { - match serde_json::from_str::(&user_json) { - Ok(user) => Some(user), - Err(_) => None, - } - }, - None => None, - }; + let user_value = Self::get_user_from_session(&session); + let user: Option = 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 ) -> Result { // Get the current user from the session - let user = match session.get::("user")? { - Some(user_json) => { - match serde_json::from_str::(&user_json) { - Ok(user) => Some(user), - Err(_) => None, - } - }, - None => None, - }; + let user_value = Self::get_user_from_session(&session); + let user: Option = 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 { // Get the current user from the session - let user = match session.get::("user")? { - Some(user_json) => { - match serde_json::from_str::(&user_json) { - Ok(user) => Some(user), - Err(_) => None, - } - }, - None => None, - }; + let user_value = Self::get_user_from_session(&session); + let user: Option = 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 ) -> Result { // Get the current user from the session - let user = match session.get::("user")? { - Some(user_json) => { - match serde_json::from_str::(&user_json) { - Ok(user) => Some(user), - Err(_) => None, - } - }, - None => None, - }; + let user_value = Self::get_user_from_session(&session); + let user: Option = 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::>() - }; + // 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 = 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") diff --git a/actix_mvc_app/src/main.rs b/actix_mvc_app/src/main.rs index 375f4c5..a8bc923 100644 --- a/actix_mvc_app/src/main.rs +++ b/actix_mvc_app/src/main.rs @@ -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 = 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::() { + 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 diff --git a/actix_mvc_app/src/middleware/mod.rs b/actix_mvc_app/src/middleware/mod.rs index 7643f1a..88c0d82 100644 --- a/actix_mvc_app/src/middleware/mod.rs +++ b/actix_mvc_app/src/middleware/mod.rs @@ -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 Transform for JwtAuth +where + S: Service, Error = Error> + 'static, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse; + type Error = Error; + type Transform = JwtAuthMiddleware; + type InitError = (); + type Future = Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + ready(Ok(JwtAuthMiddleware { service })) + } +} + +pub struct JwtAuthMiddleware { + service: S, +} + +impl Service for JwtAuthMiddleware +where + S: Service, Error = Error> + 'static, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse; + type Error = Error; + type Future = LocalBoxFuture<'static, Result>; + + 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()) + }) + } + } } \ No newline at end of file diff --git a/actix_mvc_app/src/routes/mod.rs b/actix_mvc_app/src/routes/mod.rs index 7bef8da..e1f1a68 100644 --- a/actix_mvc_app/src/routes/mod.rs +++ b/actix_mvc_app/src/routes/mod.rs @@ -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 ); } \ No newline at end of file diff --git a/actix_mvc_app/src/utils/mod.rs b/actix_mvc_app/src/utils/mod.rs index 40e0b4c..7661b1b 100644 --- a/actix_mvc_app/src/utils/mod.rs +++ b/actix_mvc_app/src/utils/mod.rs @@ -1,4 +1,4 @@ -use chrono::{DateTime, TimeZone, Utc}; +use chrono::{DateTime, Utc}; use tera::{self, Function, Result, Value}; // Export modules diff --git a/actix_mvc_app/src/utils/redis_service.rs b/actix_mvc_app/src/utils/redis_service.rs index 011517c..f7244a4 100644 --- a/actix_mvc_app/src/utils/redis_service.rs +++ b/actix_mvc_app/src/utils/redis_service.rs @@ -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)