add jwt auth, fix session handling, clean up middlewares

This commit is contained in:
Timur Gordon 2025-04-21 13:55:29 +02:00
parent 4b637b7e04
commit 36812e4178
13 changed files with 680 additions and 438 deletions

View File

@ -255,6 +255,7 @@ dependencies = [
"dotenv", "dotenv",
"env_logger", "env_logger",
"futures", "futures",
"jsonwebtoken",
"lazy_static", "lazy_static",
"log", "log",
"num_cpus", "num_cpus",
@ -466,6 +467,12 @@ dependencies = [
"windows-targets", "windows-targets",
] ]
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.20.0" version = "0.20.0"
@ -1508,6 +1515,20 @@ dependencies = [
"serde", "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]] [[package]]
name = "language-tags" name = "language-tags"
version = "0.3.2" version = "0.3.2"
@ -1630,12 +1651,31 @@ dependencies = [
"minimal-lexical", "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]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.1.0" version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 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]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@ -1724,6 +1764,15 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" 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]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.1" version = "2.3.1"
@ -2021,6 +2070,21 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 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]] [[package]]
name = "ron" name = "ron"
version = "0.8.1" version = "0.8.1"
@ -2187,6 +2251,18 @@ dependencies = [
"libc", "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]] [[package]]
name = "siphasher" name = "siphasher"
version = "1.0.1" version = "1.0.1"
@ -2238,6 +2314,12 @@ dependencies = [
"windows-sys 0.52.0", "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]] [[package]]
name = "stable_deref_trait" name = "stable_deref_trait"
version = "1.2.0" version = "1.2.0"
@ -2556,6 +2638,12 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.4" version = "2.5.4"
@ -2690,6 +2778,16 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"

View File

@ -22,3 +22,4 @@ bcrypt = "0.15.0"
uuid = { version = "1.6.1", features = ["v4", "serde"] } uuid = { version = "1.6.1", features = ["v4", "serde"] }
lazy_static = "1.4.0" lazy_static = "1.4.0"
redis = { version = "0.23.0", features = ["tokio-comp"] } redis = { version = "0.23.0", features = ["tokio-comp"] }
jsonwebtoken = "8.3.0"

View File

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

View File

@ -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 actix_session::Session;
use tera::Tera; 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 /// Controller for handling authentication-related routes
pub struct AuthController; pub struct AuthController;
impl 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 /// Renders the login page
pub async fn login_page(tmpl: web::Data<Tera>) -> Result<impl Responder> { pub async fn login_page(tmpl: web::Data<Tera>) -> Result<impl Responder> {
let mut ctx = tera::Context::new(); let mut ctx = tera::Context::new();
@ -27,10 +106,7 @@ impl AuthController {
session: Session, session: Session,
_tmpl: web::Data<Tera> _tmpl: web::Data<Tera>
) -> Result<impl Responder> { ) -> Result<impl Responder> {
// In a real application, you would validate the credentials against a database // For simplicity, always log in the user without checking credentials
// For this example, we'll use a hardcoded user
// Skip authentication check and always log in the user
// Create a user object with admin role // Create a user object with admin role
let mut test_user = User::new( let mut test_user = User::new(
"Admin User".to_string(), "Admin User".to_string(),
@ -39,17 +115,29 @@ impl AuthController {
// Set the ID and admin role // Set the ID and admin role
test_user.id = Some(1); 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 // Store user data in session
let user_json = serde_json::to_string(&test_user).unwrap(); let user_json = serde_json::to_string(&test_user).unwrap();
if let Err(e) = session.insert("user", &user_json) { session.insert("user", &user_json)?;
eprintln!("Session error: {}", e); 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() Ok(HttpResponse::Found()
.append_header(("Location", "/")) .cookie(cookie)
.append_header((header::LOCATION, "/"))
.finish()) .finish())
} }
@ -81,15 +169,29 @@ impl AuthController {
// Set the ID and admin role // Set the ID and admin role
user.id = Some(1); 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 // Store user data in session
let user_json = serde_json::to_string(&user).unwrap(); 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() Ok(HttpResponse::Found()
.append_header(("Location", "/")) .cookie(cookie)
.append_header((header::LOCATION, "/"))
.finish()) .finish())
} }
@ -98,9 +200,17 @@ impl AuthController {
// Clear the session // Clear the session
session.purge(); 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() Ok(HttpResponse::Found()
.append_header(("Location", "/")) .cookie(cookie)
.append_header((header::LOCATION, "/"))
.finish()) .finish())
} }
} }

View File

@ -1,7 +1,9 @@
use actix_web::{web, HttpResponse, Responder, Result}; use actix_web::{web, HttpResponse, Responder, Result};
use actix_session::Session;
use chrono::{DateTime, Datelike, NaiveDate, TimeZone, Utc}; use chrono::{DateTime, Datelike, NaiveDate, TimeZone, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tera::Tera; use tera::Tera;
use serde_json::Value;
use crate::models::{CalendarEvent, CalendarViewMode}; use crate::models::{CalendarEvent, CalendarViewMode};
use crate::utils::RedisCalendarService; use crate::utils::RedisCalendarService;
@ -10,10 +12,18 @@ use crate::utils::RedisCalendarService;
pub struct CalendarController; pub struct CalendarController;
impl 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 /// Handles the calendar page route
pub async fn calendar( pub async fn calendar(
tmpl: web::Data<Tera>, tmpl: web::Data<Tera>,
query: web::Query<CalendarQuery>, query: web::Query<CalendarQuery>,
_session: Session,
) -> Result<impl Responder> { ) -> Result<impl Responder> {
let mut ctx = tera::Context::new(); let mut ctx = tera::Context::new();
ctx.insert("active_page", "calendar"); ctx.insert("active_page", "calendar");
@ -25,7 +35,7 @@ impl CalendarController {
// Parse the date from the query parameters or use the current date // Parse the date from the query parameters or use the current date
let date = if let Some(date_str) = &query.date { let date = if let Some(date_str) = &query.date {
match NaiveDate::parse_from_str(date_str, "%Y-%m-%d") { 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(), Err(_) => Utc::now(),
} }
} else { } else {
@ -37,6 +47,11 @@ impl CalendarController {
ctx.insert("current_month", &date.month()); ctx.insert("current_month", &date.month());
ctx.insert("current_day", &date.day()); 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 // Get events for the current view
let (start_date, end_date) = match view_mode { let (start_date, end_date) = match view_mode {
CalendarViewMode::Year => { CalendarViewMode::Year => {
@ -52,9 +67,9 @@ impl CalendarController {
}, },
CalendarViewMode::Week => { CalendarViewMode::Week => {
// Calculate the start of the week (Sunday) // 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_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); let end = start + chrono::Duration::days(7);
(start, end) (start, end)
}, },
@ -210,10 +225,15 @@ impl CalendarController {
} }
/// Handles the new event page route /// 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(); let mut ctx = tera::Context::new();
ctx.insert("active_page", "calendar"); 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) let rendered = tmpl.render("calendar/new_event.html", &ctx)
.map_err(|e| { .map_err(|e| {
eprintln!("Template rendering error: {}", e); eprintln!("Template rendering error: {}", e);
@ -227,6 +247,7 @@ impl CalendarController {
pub async fn create_event( pub async fn create_event(
form: web::Form<EventForm>, form: web::Form<EventForm>,
tmpl: web::Data<Tera>, tmpl: web::Data<Tera>,
_session: Session,
) -> Result<impl Responder> { ) -> Result<impl Responder> {
// Parse the start and end times // Parse the start and end times
let start_time = match DateTime::parse_from_rfc3339(&form.start_time) { let start_time = match DateTime::parse_from_rfc3339(&form.start_time) {
@ -272,6 +293,11 @@ impl CalendarController {
ctx.insert("active_page", "calendar"); ctx.insert("active_page", "calendar");
ctx.insert("error", "Failed to save event"); 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) let rendered = tmpl.render("calendar/new_event.html", &ctx)
.map_err(|e| { .map_err(|e| {
eprintln!("Template rendering error: {}", e); eprintln!("Template rendering error: {}", e);
@ -286,6 +312,7 @@ impl CalendarController {
/// Handles the delete event route /// Handles the delete event route
pub async fn delete_event( pub async fn delete_event(
path: web::Path<String>, path: web::Path<String>,
_session: Session,
) -> Result<impl Responder> { ) -> Result<impl Responder> {
let id = path.into_inner(); let id = path.into_inner();

View File

@ -1,16 +1,29 @@
use actix_web::{web, HttpResponse, Responder, Result}; use actix_web::{web, HttpResponse, Responder, Result};
use actix_session::Session;
use tera::Tera; use tera::Tera;
use crate::models::User; use serde_json::Value;
/// Controller for handling home-related routes /// Controller for handling home-related routes
pub struct HomeController; pub struct HomeController;
impl 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 /// 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(); let mut ctx = tera::Context::new();
ctx.insert("active_page", "editor"); 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) let rendered = tmpl.render("editor.html", &ctx)
.map_err(|e| { .map_err(|e| {
eprintln!("Template rendering error: {}", e); eprintln!("Template rendering error: {}", e);
@ -21,13 +34,14 @@ impl HomeController {
} }
/// Handles the home page route /// 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(); let mut ctx = tera::Context::new();
ctx.insert("active_page", "home"); ctx.insert("active_page", "home");
// Example of using models in controllers // Add user to context if available
let example_user = User::new("John Doe".to_string(), "john@example.com".to_string()); if let Some(user) = Self::get_user_from_session(&session) {
ctx.insert("user", &example_user); ctx.insert("user", &user);
}
let rendered = tmpl.render("index.html", &ctx) let rendered = tmpl.render("index.html", &ctx)
.map_err(|e| { .map_err(|e| {
@ -39,10 +53,15 @@ impl HomeController {
} }
/// Handles the about page route /// 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(); let mut ctx = tera::Context::new();
ctx.insert("active_page", "about"); 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) let rendered = tmpl.render("about.html", &ctx)
.map_err(|e| { .map_err(|e| {
eprintln!("Template rendering error: {}", e); eprintln!("Template rendering error: {}", e);
@ -53,10 +72,15 @@ impl HomeController {
} }
/// Handles the contact page route /// 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(); let mut ctx = tera::Context::new();
ctx.insert("active_page", "contact"); 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) let rendered = tmpl.render("contact.html", &ctx)
.map_err(|e| { .map_err(|e| {
eprintln!("Template rendering error: {}", e); eprintln!("Template rendering error: {}", e);
@ -69,7 +93,8 @@ impl HomeController {
/// Handles form submissions from the contact page /// Handles form submissions from the contact page
pub async fn submit_contact( pub async fn submit_contact(
form: web::Form<ContactForm>, form: web::Form<ContactForm>,
tmpl: web::Data<Tera> tmpl: web::Data<Tera>,
session: Session
) -> Result<impl Responder> { ) -> Result<impl Responder> {
// In a real application, you would process the form data here // In a real application, you would process the form data here
// For example, save it to a database or send an email // For example, save it to a database or send an email
@ -82,6 +107,11 @@ impl HomeController {
ctx.insert("active_page", "contact"); ctx.insert("active_page", "contact");
ctx.insert("success_message", "Your message has been sent successfully!"); 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) let rendered = tmpl.render("contact.html", &ctx)
.map_err(|e| { .map_err(|e| {
eprintln!("Template rendering error: {}", e); eprintln!("Template rendering error: {}", e);

View File

@ -2,10 +2,4 @@
pub mod home; pub mod home;
pub mod auth; pub mod auth;
pub mod ticket; pub mod ticket;
pub mod calendar; 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;

View File

@ -2,7 +2,8 @@ use actix_web::{web, HttpResponse, Responder, Result};
use actix_session::Session; use actix_session::Session;
use tera::Tera; use tera::Tera;
use serde::{Deserialize, Serialize}; 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::collections::HashMap;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
@ -35,6 +36,13 @@ pub struct TicketFilterForm {
} }
impl TicketController { 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 /// Lists all tickets with optional filtering
pub async fn list_tickets( pub async fn list_tickets(
session: Session, session: Session,
@ -42,15 +50,8 @@ impl TicketController {
tmpl: web::Data<Tera> tmpl: web::Data<Tera>
) -> Result<impl Responder> { ) -> Result<impl Responder> {
// Get the current user from the session // Get the current user from the session
let user = match session.get::<String>("user")? { let user_value = Self::get_user_from_session(&session);
Some(user_json) => { let user: Option<User> = user_value.and_then(|v| serde_json::from_value(v).ok());
match serde_json::from_str::<User>(&user_json) {
Ok(user) => Some(user),
Err(_) => None,
}
},
None => None,
};
// If the user is not logged in, redirect to the login page // If the user is not logged in, redirect to the login page
if user.is_none() { if user.is_none() {
@ -59,88 +60,75 @@ impl TicketController {
.finish()); .finish());
} }
let user = user.unwrap(); let user: User = user.unwrap();
// Create a filter based on the query parameters // Get all tickets from the in-memory storage
let mut filter = TicketFilter::default(); 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 { if let Some(status_str) = &query.status {
filter.status = match status_str.as_str() { if !status_str.is_empty() {
"open" => Some(TicketStatus::Open), let status = match status_str.as_str() {
"in_progress" => Some(TicketStatus::InProgress), "open" => TicketStatus::Open,
"waiting_for_customer" => Some(TicketStatus::WaitingForCustomer), "in_progress" => TicketStatus::InProgress,
"resolved" => Some(TicketStatus::Resolved), "resolved" => TicketStatus::Resolved,
"closed" => Some(TicketStatus::Closed), "closed" => TicketStatus::Closed,
_ => None, _ => TicketStatus::Open,
}; };
filtered_tickets.retain(|ticket| ticket.status == status);
}
} }
// Apply priority filter if provided
if let Some(priority_str) = &query.priority { if let Some(priority_str) = &query.priority {
filter.priority = match priority_str.as_str() { if !priority_str.is_empty() {
"low" => Some(TicketPriority::Low), let priority = match priority_str.as_str() {
"medium" => Some(TicketPriority::Medium), "low" => TicketPriority::Low,
"high" => Some(TicketPriority::High), "medium" => TicketPriority::Medium,
"critical" => Some(TicketPriority::Critical), "high" => TicketPriority::High,
_ => None, "urgent" => TicketPriority::Critical,
}; _ => TicketPriority::Medium,
};
filtered_tickets.retain(|ticket| ticket.priority == priority);
}
} }
filter.search_term = query.search.clone(); // Regular users can only see their own tickets
// If the user is not an admin, only show their tickets
if user.role != crate::models::user::UserRole::Admin { 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 // Sort tickets by created_at (newest first)
let tickets = { filtered_tickets.sort_by(|a, b| b.created_at.cmp(&a.created_at));
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<_>>()
};
// Prepare the template context // Prepare the template context
let mut ctx = tera::Context::new(); let mut ctx = tera::Context::new();
ctx.insert("active_page", "tickets"); ctx.insert("active_page", "tickets");
ctx.insert("tickets", &filtered_tickets);
ctx.insert("user", &user); ctx.insert("user", &user);
ctx.insert("tickets", &tickets); ctx.insert("filter", &query.into_inner());
// Extract the query parameters for the template
ctx.insert("status", &query.status); // Add filter options for the dropdown menus
ctx.insert("priority", &query.priority); ctx.insert("statuses", &[
ctx.insert("search", &query.search); ("", "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 // Render the template
let rendered = tmpl.render("tickets/list.html", &ctx) let rendered = tmpl.render("tickets/list.html", &ctx)
@ -158,15 +146,8 @@ impl TicketController {
tmpl: web::Data<Tera> tmpl: web::Data<Tera>
) -> Result<impl Responder> { ) -> Result<impl Responder> {
// Get the current user from the session // Get the current user from the session
let user = match session.get::<String>("user")? { let user_value = Self::get_user_from_session(&session);
Some(user_json) => { let user: Option<User> = user_value.and_then(|v| serde_json::from_value(v).ok());
match serde_json::from_str::<User>(&user_json) {
Ok(user) => Some(user),
Err(_) => None,
}
},
None => None,
};
// If the user is not logged in, redirect to the login page // If the user is not logged in, redirect to the login page
if user.is_none() { if user.is_none() {
@ -175,19 +156,20 @@ impl TicketController {
.finish()); .finish());
} }
let user = user.unwrap(); let user: User = user.unwrap();
// Prepare the template context // Prepare the template context
let mut ctx = tera::Context::new(); let mut ctx = tera::Context::new();
ctx.insert("active_page", "tickets"); ctx.insert("active_page", "tickets");
ctx.insert("user", &user); ctx.insert("user", &user);
// Add an empty form to the context to avoid template errors // Add priority options for the dropdown menu
ctx.insert("form", &serde_json::json!({ ctx.insert("priorities", &[
"title": "", ("low", "Low"),
"priority": "medium", ("medium", "Medium"),
"description": "" ("high", "High"),
})); ("urgent", "Urgent"),
]);
// Render the template // Render the template
let rendered = tmpl.render("tickets/new.html", &ctx) let rendered = tmpl.render("tickets/new.html", &ctx)
@ -206,15 +188,8 @@ impl TicketController {
_tmpl: web::Data<Tera> _tmpl: web::Data<Tera>
) -> Result<impl Responder> { ) -> Result<impl Responder> {
// Get the current user from the session // Get the current user from the session
let user = match session.get::<String>("user")? { let user_value = Self::get_user_from_session(&session);
Some(user_json) => { let user: Option<User> = user_value.and_then(|v| serde_json::from_value(v).ok());
match serde_json::from_str::<User>(&user_json) {
Ok(user) => Some(user),
Err(_) => None,
}
},
None => None,
};
// If the user is not logged in, redirect to the login page // If the user is not logged in, redirect to the login page
if user.is_none() { if user.is_none() {
@ -223,20 +198,18 @@ impl TicketController {
.finish()); .finish());
} }
let user = user.unwrap(); let user: User = user.unwrap();
// Skip validation and always create the ticket // Parse the priority from the form
// Parse the priority
let priority = match form.priority.as_str() { let priority = match form.priority.as_str() {
"low" => TicketPriority::Low, "low" => TicketPriority::Low,
"medium" => TicketPriority::Medium, "medium" => TicketPriority::Medium,
"high" => TicketPriority::High, "high" => TicketPriority::High,
"critical" => TicketPriority::Critical, "urgent" => TicketPriority::Critical,
_ => TicketPriority::Medium, _ => TicketPriority::Medium,
}; };
// Create the ticket // Create a new ticket
let ticket = Ticket::new( let ticket = Ticket::new(
user.id.unwrap_or(0), user.id.unwrap_or(0),
form.title.clone(), form.title.clone(),
@ -244,15 +217,13 @@ impl TicketController {
priority priority
); );
// Store the ticket // Add the ticket to the in-memory storage
{ let mut tickets = TICKETS.lock().unwrap();
let mut tickets_map = TICKETS.lock().unwrap(); tickets.insert(ticket.id.clone(), ticket.clone());
tickets_map.insert(ticket.id.clone(), ticket.clone());
}
// Redirect to the ticket detail page // Redirect to the ticket list page
Ok(HttpResponse::Found() Ok(HttpResponse::Found()
.append_header(("Location", format!("/tickets/{}", ticket.id))) .append_header(("Location", "/tickets"))
.finish()) .finish())
} }
@ -263,15 +234,8 @@ impl TicketController {
tmpl: web::Data<Tera> tmpl: web::Data<Tera>
) -> Result<impl Responder> { ) -> Result<impl Responder> {
// Get the current user from the session // Get the current user from the session
let user = match session.get::<String>("user")? { let user_value = Self::get_user_from_session(&session);
Some(user_json) => { let user: Option<User> = user_value.and_then(|v| serde_json::from_value(v).ok());
match serde_json::from_str::<User>(&user_json) {
Ok(user) => Some(user),
Err(_) => None,
}
},
None => None,
};
// If the user is not logged in, redirect to the login page // If the user is not logged in, redirect to the login page
if user.is_none() { if user.is_none() {
@ -280,38 +244,45 @@ impl TicketController {
.finish()); .finish());
} }
let user = user.unwrap(); let user: User = user.unwrap();
let ticket_id = path.into_inner();
// Get the ticket // Get the ticket ID from the path
let ticket = { let id = path.into_inner();
let tickets_map = TICKETS.lock().unwrap();
match tickets_map.get(&ticket_id) { // Get the ticket from the in-memory storage
Some(ticket) => ticket.clone(), let tickets = TICKETS.lock().unwrap();
None => { let ticket = match tickets.get(&id) {
// Ticket not found, redirect to the tickets list Some(ticket) => ticket.clone(),
return Ok(HttpResponse::Found() None => {
.append_header(("Location", "/tickets")) return Ok(HttpResponse::NotFound().finish());
.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 // Get the comments for this ticket
let comments = { let comments = COMMENTS.lock().unwrap();
let comments_map = COMMENTS.lock().unwrap(); let ticket_comments = comments.get(&ticket.id)
match comments_map.get(&ticket_id) { .cloned()
Some(comments) => comments.clone(), .unwrap_or_default();
None => Vec::new(),
}
};
// Prepare the template context // Prepare the template context
let mut ctx = tera::Context::new(); let mut ctx = tera::Context::new();
ctx.insert("active_page", "tickets"); ctx.insert("active_page", "tickets");
ctx.insert("user", &user);
ctx.insert("ticket", &ticket); 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 // Render the template
let rendered = tmpl.render("tickets/show.html", &ctx) let rendered = tmpl.render("tickets/show.html", &ctx)
@ -330,15 +301,8 @@ impl TicketController {
form: web::Form<NewCommentForm> form: web::Form<NewCommentForm>
) -> Result<impl Responder> { ) -> Result<impl Responder> {
// Get the current user from the session // Get the current user from the session
let user = match session.get::<String>("user")? { let user_value = Self::get_user_from_session(&session);
Some(user_json) => { let user: Option<User> = user_value.and_then(|v| serde_json::from_value(v).ok());
match serde_json::from_str::<User>(&user_json) {
Ok(user) => Some(user),
Err(_) => None,
}
},
None => None,
};
// If the user is not logged in, redirect to the login page // If the user is not logged in, redirect to the login page
if user.is_none() { if user.is_none() {
@ -347,62 +311,53 @@ impl TicketController {
.finish()); .finish());
} }
let user = user.unwrap(); let user: User = user.unwrap();
let ticket_id = path.into_inner();
// Validate the form data // Get the ticket ID from the path
if form.content.trim().is_empty() { let id = path.into_inner();
// Comment is empty, redirect back to the ticket
return Ok(HttpResponse::Found()
.append_header(("Location", format!("/tickets/{}", ticket_id)))
.finish());
}
// Check if the ticket exists // Get the ticket from the in-memory storage
{ let tickets = TICKETS.lock().unwrap();
let tickets_map = TICKETS.lock().unwrap(); let ticket = match tickets.get(&id) {
if !tickets_map.contains_key(&ticket_id) { Some(ticket) => ticket.clone(),
// Ticket not found, redirect to the tickets list None => {
return Ok(HttpResponse::Found() return Ok(HttpResponse::NotFound().finish());
.append_header(("Location", "/tickets"))
.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( let comment = TicketComment::new(
ticket_id.clone(), ticket.id.clone(),
user.id.unwrap_or(0), user.id.unwrap_or(0),
form.content.clone(), form.content.clone(),
user.is_admin() user.role == crate::models::user::UserRole::Admin
); );
// Store the comment // Add the comment to the in-memory storage
{ let mut comments = COMMENTS.lock().unwrap();
let mut comments_map = COMMENTS.lock().unwrap(); if let Some(ticket_comments) = comments.get_mut(&ticket.id) {
if let Some(comments) = comments_map.get_mut(&ticket_id) { ticket_comments.push(comment);
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);
}
} else { } else {
let mut tickets_map = TICKETS.lock().unwrap(); comments.insert(ticket.id.clone(), vec![comment]);
if let Some(ticket) = tickets_map.get_mut(&ticket_id) { }
ticket.update_status(TicketStatus::Open);
// 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() Ok(HttpResponse::Found()
.append_header(("Location", format!("/tickets/{}", ticket_id))) .append_header(("Location", format!("/tickets/{}", id)))
.finish()) .finish())
} }
@ -412,15 +367,8 @@ impl TicketController {
path: web::Path<(String, String)>, path: web::Path<(String, String)>,
) -> Result<impl Responder> { ) -> Result<impl Responder> {
// Get the current user from the session // Get the current user from the session
let user = match session.get::<String>("user")? { let user_value = Self::get_user_from_session(&session);
Some(user_json) => { let user: Option<User> = user_value.and_then(|v| serde_json::from_value(v).ok());
match serde_json::from_str::<User>(&user_json) {
Ok(user) => Some(user),
Err(_) => None,
}
},
None => None,
};
// If the user is not logged in or not an admin, redirect to the login page // 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 { if user.is_none() || user.as_ref().unwrap().role != crate::models::user::UserRole::Admin {
@ -429,39 +377,30 @@ impl TicketController {
.finish()); .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 // Parse the status
let status = match status_str.as_str() { let status = match status_str.as_str() {
"open" => TicketStatus::Open, "open" => TicketStatus::Open,
"in_progress" => TicketStatus::InProgress, "in_progress" => TicketStatus::InProgress,
"waiting_for_customer" => TicketStatus::WaitingForCustomer,
"resolved" => TicketStatus::Resolved, "resolved" => TicketStatus::Resolved,
"closed" => TicketStatus::Closed, "closed" => TicketStatus::Closed,
_ => { _ => TicketStatus::Open,
// Invalid status, redirect back to the ticket
return Ok(HttpResponse::Found()
.append_header(("Location", format!("/tickets/{}", ticket_id)))
.finish());
}
}; };
// Update the ticket status // Update the ticket status
{ let mut tickets = TICKETS.lock().unwrap();
let mut tickets_map = TICKETS.lock().unwrap(); if let Some(ticket) = tickets.get_mut(&id) {
if let Some(ticket) = tickets_map.get_mut(&ticket_id) { ticket.status = status;
ticket.update_status(status); ticket.updated_at = chrono::Utc::now();
} else { } else {
// Ticket not found, redirect to the tickets list return Ok(HttpResponse::NotFound().finish());
return Ok(HttpResponse::Found()
.append_header(("Location", "/tickets"))
.finish());
}
} }
// Redirect back to the ticket // Redirect back to the ticket page
Ok(HttpResponse::Found() Ok(HttpResponse::Found()
.append_header(("Location", format!("/tickets/{}", ticket_id))) .append_header(("Location", format!("/tickets/{}", id)))
.finish()) .finish())
} }
@ -471,15 +410,8 @@ impl TicketController {
tmpl: web::Data<Tera> tmpl: web::Data<Tera>
) -> Result<impl Responder> { ) -> Result<impl Responder> {
// Get the current user from the session // Get the current user from the session
let user = match session.get::<String>("user")? { let user_value = Self::get_user_from_session(&session);
Some(user_json) => { let user: Option<User> = user_value.and_then(|v| serde_json::from_value(v).ok());
match serde_json::from_str::<User>(&user_json) {
Ok(user) => Some(user),
Err(_) => None,
}
},
None => None,
};
// If the user is not logged in, redirect to the login page // If the user is not logged in, redirect to the login page
if user.is_none() { if user.is_none() {
@ -488,25 +420,30 @@ impl TicketController {
.finish()); .finish());
} }
let user = user.unwrap(); let user: User = user.unwrap();
// Get the user's tickets // Get all tickets from the in-memory storage
let tickets = { let tickets = TICKETS.lock().unwrap();
let tickets_map = TICKETS.lock().unwrap();
tickets_map.values() // Filter tickets to only show the user's tickets
.filter(|ticket| ticket.user_id == user.id.unwrap_or(0)) let my_tickets: Vec<Ticket> = tickets.values()
.cloned() .cloned()
.collect::<Vec<_>>() .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 // Prepare the template context
let mut ctx = tera::Context::new(); let mut ctx = tera::Context::new();
ctx.insert("active_page", "tickets"); ctx.insert("active_page", "tickets");
ctx.insert("tickets", &sorted_tickets);
ctx.insert("user", &user); ctx.insert("user", &user);
ctx.insert("tickets", &tickets); ctx.insert("my_tickets", &true);
// Render the template // Render the template
let rendered = tmpl.render("tickets/my_tickets.html", &ctx) let rendered = tmpl.render("tickets/list.html", &ctx)
.map_err(|e| { .map_err(|e| {
eprintln!("Template rendering error: {}", e); eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error") actix_web::error::ErrorInternalServerError("Template rendering error")

View File

@ -3,22 +3,42 @@ use actix_web::{App, HttpServer, web};
use actix_web::middleware::Logger; use actix_web::middleware::Logger;
use tera::Tera; use tera::Tera;
use std::io; use std::io;
use std::env;
use lazy_static::lazy_static;
mod config; mod config;
mod controllers; mod controllers;
mod app_middleware; mod middleware;
mod models; mod models;
mod routes; mod routes;
mod utils; mod utils;
// Import middleware components // Import middleware components
use app_middleware::{RequestTimer, SecurityHeaders}; use middleware::{RequestTimer, SecurityHeaders, JwtAuth};
use utils::redis_service; use utils::redis_service;
// Initialize lazy_static for in-memory storage // Initialize lazy_static for in-memory storage
#[macro_use]
extern crate lazy_static; 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] #[actix_web::main]
async fn main() -> io::Result<()> { async fn main() -> io::Result<()> {
// Initialize environment // Initialize environment
@ -27,7 +47,21 @@ async fn main() -> io::Result<()> {
// Load configuration // Load configuration
let config = config::get_config(); 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 // Initialize Redis client
let redis_url = std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string()); 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 // Add custom middleware
.wrap(RequestTimer) .wrap(RequestTimer)
.wrap(SecurityHeaders) .wrap(SecurityHeaders)
.wrap(JwtAuth)
// Configure static files // Configure static files
.service(fs::Files::new("/static", "./src/static")) .service(fs::Files::new("/static", "./src/static"))
// Add Tera template engine // Add Tera template engine

View File

@ -1,14 +1,14 @@
use actix_web::{ use actix_web::{
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
Error, Error, HttpMessage,
}; };
use futures::future::{ready, LocalBoxFuture, Ready}; use futures::future::{ready, LocalBoxFuture, Ready};
use std::{ use std::{
future::Future, future::Future,
pin::Pin, pin::Pin,
task::{Context, Poll},
time::Instant, time::Instant,
}; };
use actix_session::SessionExt;
/// Middleware for logging request duration /// Middleware for logging request duration
pub struct RequestTimer; pub struct RequestTimer;
@ -127,4 +127,137 @@ where
Ok(res) 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())
})
}
}
} }

View File

@ -1,23 +1,23 @@
use actix_web::web; use actix_web::web;
use actix_session::{SessionMiddleware, storage::CookieSessionStore}; use actix_session::{SessionMiddleware, storage::CookieSessionStore};
use actix_web::cookie::Key;
use crate::controllers::home::HomeController; use crate::controllers::home::HomeController;
use crate::controllers::auth::AuthController; use crate::controllers::auth::AuthController;
use crate::controllers::ticket::TicketController; use crate::controllers::ticket::TicketController;
use crate::controllers::calendar::CalendarController; use crate::controllers::calendar::CalendarController;
use crate::middleware::JwtAuth;
use crate::SESSION_KEY;
/// Configures all application routes /// Configures all application routes
pub fn configure_routes(cfg: &mut web::ServiceConfig) { pub fn configure_routes(cfg: &mut web::ServiceConfig) {
// Generate a random key for cookie encryption // Configure session middleware with the consistent key
let key = Key::generate(); let session_middleware = SessionMiddleware::builder(
// Configure session middleware with cookie store
let session_middleware = SessionMiddleware::new(
CookieSessionStore::default(), 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( cfg.service(
web::scope("") web::scope("")
.wrap(session_middleware) .wrap(session_middleware)
@ -26,7 +26,6 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) {
.route("/about", web::get().to(HomeController::about)) .route("/about", web::get().to(HomeController::about))
.route("/contact", web::get().to(HomeController::contact)) .route("/contact", web::get().to(HomeController::contact))
.route("/contact", web::post().to(HomeController::submit_contact)) .route("/contact", web::post().to(HomeController::submit_contact))
.route("/editor", web::get().to(HomeController::editor))
// Auth routes // Auth routes
.route("/login", web::get().to(AuthController::login_page)) .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("/register", web::post().to(AuthController::register))
.route("/logout", web::get().to(AuthController::logout)) .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 // Ticket routes
.route("/tickets", web::get().to(TicketController::list_tickets)) .route("/tickets", web::get().to(TicketController::list_tickets))
.route("/tickets/new", web::get().to(TicketController::new_ticket)) .route("/tickets/new", web::get().to(TicketController::new_ticket))
.route("/tickets/new", web::post().to(TicketController::create_ticket)) .route("/tickets", web::post().to(TicketController::create_ticket))
.route("/tickets/my", web::get().to(TicketController::my_tickets))
.route("/tickets/{id}", web::get().to(TicketController::show_ticket)) .route("/tickets/{id}", web::get().to(TicketController::show_ticket))
.route("/tickets/{id}/comment", web::post().to(TicketController::add_comment)) .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 // Calendar routes
.route("/calendar", web::get().to(CalendarController::calendar)) .route("/calendar", web::get().to(CalendarController::calendar))
.route("/calendar/new", web::get().to(CalendarController::new_event)) .route("/calendar/events/new", web::get().to(CalendarController::new_event))
.route("/calendar/new", web::post().to(CalendarController::create_event)) .route("/calendar/events", web::post().to(CalendarController::create_event))
.route("/calendar/{id}/delete", web::get().to(CalendarController::delete_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
); );
} }

View File

@ -1,4 +1,4 @@
use chrono::{DateTime, TimeZone, Utc}; use chrono::{DateTime, Utc};
use tera::{self, Function, Result, Value}; use tera::{self, Function, Result, Value};
// Export modules // Export modules

View File

@ -133,10 +133,7 @@ impl RedisCalendarService {
// Filter events that fall within the date range // Filter events that fall within the date range
let filtered_events = all_events let filtered_events = all_events
.into_iter() .into_iter()
.filter(|event| { .filter(|event| event.start_time <= end && event.end_time >= start)
// Check if the event overlaps with the date range
(event.start_time <= end && event.end_time >= start)
})
.collect(); .collect();
Ok(filtered_events) Ok(filtered_events)