add jwt auth, fix session handling, clean up middlewares
This commit is contained in:
@@ -1,12 +1,91 @@
|
||||
use actix_web::{web, HttpResponse, Responder, Result};
|
||||
use actix_web::{web, HttpResponse, Responder, Result, http::header, cookie::Cookie};
|
||||
use actix_session::Session;
|
||||
use tera::Tera;
|
||||
use crate::models::user::{User, LoginCredentials, RegistrationData};
|
||||
use crate::models::user::{User, LoginCredentials, RegistrationData, UserRole};
|
||||
use jsonwebtoken::{encode, decode, Header, Algorithm, Validation, EncodingKey, DecodingKey};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use chrono::{Utc, Duration};
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
// JWT Claims structure
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Claims {
|
||||
pub sub: String, // Subject (email)
|
||||
pub exp: usize, // Expiration time
|
||||
pub iat: usize, // Issued at
|
||||
pub role: String, // User role
|
||||
}
|
||||
|
||||
// JWT Secret key
|
||||
lazy_static! {
|
||||
static ref JWT_SECRET: String = std::env::var("JWT_SECRET").unwrap_or_else(|_| "your_jwt_secret_key".to_string());
|
||||
}
|
||||
|
||||
/// Controller for handling authentication-related routes
|
||||
pub struct AuthController;
|
||||
|
||||
impl AuthController {
|
||||
/// Generate a JWT token for a user
|
||||
fn generate_token(email: &str, role: &UserRole) -> Result<String, jsonwebtoken::errors::Error> {
|
||||
let role_str = match role {
|
||||
UserRole::Admin => "admin",
|
||||
UserRole::User => "user",
|
||||
};
|
||||
|
||||
let expiration = Utc::now()
|
||||
.checked_add_signed(Duration::hours(24))
|
||||
.expect("valid timestamp")
|
||||
.timestamp() as usize;
|
||||
|
||||
let claims = Claims {
|
||||
sub: email.to_owned(),
|
||||
exp: expiration,
|
||||
iat: Utc::now().timestamp() as usize,
|
||||
role: role_str.to_string(),
|
||||
};
|
||||
|
||||
encode(
|
||||
&Header::default(),
|
||||
&claims,
|
||||
&EncodingKey::from_secret(JWT_SECRET.as_bytes())
|
||||
)
|
||||
}
|
||||
|
||||
/// Validate a JWT token
|
||||
pub fn validate_token(token: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
|
||||
let validation = Validation::new(Algorithm::HS256);
|
||||
|
||||
let token_data = decode::<Claims>(
|
||||
token,
|
||||
&DecodingKey::from_secret(JWT_SECRET.as_bytes()),
|
||||
&validation
|
||||
)?;
|
||||
|
||||
Ok(token_data.claims)
|
||||
}
|
||||
|
||||
/// Extract token from session
|
||||
pub fn extract_token_from_session(session: &Session) -> Option<String> {
|
||||
session.get::<String>("auth_token").ok().flatten()
|
||||
}
|
||||
|
||||
/// Extract token from cookie
|
||||
pub fn extract_token_from_cookie(req: &actix_web::HttpRequest) -> Option<String> {
|
||||
req.cookie("auth_token").map(|c| c.value().to_string())
|
||||
}
|
||||
|
||||
/// Check if user is authenticated from session
|
||||
pub async fn is_authenticated(session: &Session) -> Option<Claims> {
|
||||
if let Some(token) = Self::extract_token_from_session(session) {
|
||||
match Self::validate_token(&token) {
|
||||
Ok(claims) => Some(claims),
|
||||
Err(_) => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the login page
|
||||
pub async fn login_page(tmpl: web::Data<Tera>) -> Result<impl Responder> {
|
||||
let mut ctx = tera::Context::new();
|
||||
@@ -27,10 +106,7 @@ impl AuthController {
|
||||
session: Session,
|
||||
_tmpl: web::Data<Tera>
|
||||
) -> Result<impl Responder> {
|
||||
// In a real application, you would validate the credentials against a database
|
||||
// For this example, we'll use a hardcoded user
|
||||
|
||||
// Skip authentication check and always log in the user
|
||||
// For simplicity, always log in the user without checking credentials
|
||||
// Create a user object with admin role
|
||||
let mut test_user = User::new(
|
||||
"Admin User".to_string(),
|
||||
@@ -39,17 +115,29 @@ impl AuthController {
|
||||
|
||||
// Set the ID and admin role
|
||||
test_user.id = Some(1);
|
||||
test_user.role = crate::models::user::UserRole::Admin;
|
||||
test_user.role = UserRole::Admin;
|
||||
|
||||
// Generate JWT token
|
||||
let token = Self::generate_token(&test_user.email, &test_user.role)
|
||||
.map_err(|_| actix_web::error::ErrorInternalServerError("Failed to generate token"))?;
|
||||
|
||||
// Store user data in session
|
||||
let user_json = serde_json::to_string(&test_user).unwrap();
|
||||
if let Err(e) = session.insert("user", &user_json) {
|
||||
eprintln!("Session error: {}", e);
|
||||
}
|
||||
session.insert("user", &user_json)?;
|
||||
session.insert("auth_token", &token)?;
|
||||
|
||||
// Redirect to the home page
|
||||
// Create a cookie with the JWT token
|
||||
let cookie = Cookie::build("auth_token", token)
|
||||
.path("/")
|
||||
.http_only(true)
|
||||
.secure(false) // Set to true in production with HTTPS
|
||||
.max_age(actix_web::cookie::time::Duration::hours(24))
|
||||
.finish();
|
||||
|
||||
// Redirect to the home page with JWT token in cookie
|
||||
Ok(HttpResponse::Found()
|
||||
.append_header(("Location", "/"))
|
||||
.cookie(cookie)
|
||||
.append_header((header::LOCATION, "/"))
|
||||
.finish())
|
||||
}
|
||||
|
||||
@@ -81,15 +169,29 @@ impl AuthController {
|
||||
|
||||
// Set the ID and admin role
|
||||
user.id = Some(1);
|
||||
user.role = crate::models::user::UserRole::Admin;
|
||||
user.role = UserRole::Admin;
|
||||
|
||||
// Generate JWT token
|
||||
let token = Self::generate_token(&user.email, &user.role)
|
||||
.map_err(|_| actix_web::error::ErrorInternalServerError("Failed to generate token"))?;
|
||||
|
||||
// Store user data in session
|
||||
let user_json = serde_json::to_string(&user).unwrap();
|
||||
session.insert("user", &user_json).unwrap();
|
||||
session.insert("user", &user_json)?;
|
||||
session.insert("auth_token", &token)?;
|
||||
|
||||
// Redirect to the home page
|
||||
// Create a cookie with the JWT token
|
||||
let cookie = Cookie::build("auth_token", token)
|
||||
.path("/")
|
||||
.http_only(true)
|
||||
.secure(false) // Set to true in production with HTTPS
|
||||
.max_age(actix_web::cookie::time::Duration::hours(24))
|
||||
.finish();
|
||||
|
||||
// Redirect to the home page with JWT token in cookie
|
||||
Ok(HttpResponse::Found()
|
||||
.append_header(("Location", "/"))
|
||||
.cookie(cookie)
|
||||
.append_header((header::LOCATION, "/"))
|
||||
.finish())
|
||||
}
|
||||
|
||||
@@ -98,9 +200,17 @@ impl AuthController {
|
||||
// Clear the session
|
||||
session.purge();
|
||||
|
||||
// Redirect to the home page
|
||||
// Create an expired cookie to remove the JWT token
|
||||
let cookie = Cookie::build("auth_token", "")
|
||||
.path("/")
|
||||
.http_only(true)
|
||||
.max_age(actix_web::cookie::time::Duration::seconds(0))
|
||||
.finish();
|
||||
|
||||
// Redirect to the home page and clear the auth token cookie
|
||||
Ok(HttpResponse::Found()
|
||||
.append_header(("Location", "/"))
|
||||
.cookie(cookie)
|
||||
.append_header((header::LOCATION, "/"))
|
||||
.finish())
|
||||
}
|
||||
}
|
@@ -1,7 +1,9 @@
|
||||
use actix_web::{web, HttpResponse, Responder, Result};
|
||||
use actix_session::Session;
|
||||
use chrono::{DateTime, Datelike, NaiveDate, TimeZone, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tera::Tera;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::models::{CalendarEvent, CalendarViewMode};
|
||||
use crate::utils::RedisCalendarService;
|
||||
@@ -10,10 +12,18 @@ use crate::utils::RedisCalendarService;
|
||||
pub struct CalendarController;
|
||||
|
||||
impl CalendarController {
|
||||
/// Helper function to get user from session
|
||||
fn get_user_from_session(session: &Session) -> Option<Value> {
|
||||
session.get::<String>("user").ok().flatten().and_then(|user_json| {
|
||||
serde_json::from_str(&user_json).ok()
|
||||
})
|
||||
}
|
||||
|
||||
/// Handles the calendar page route
|
||||
pub async fn calendar(
|
||||
tmpl: web::Data<Tera>,
|
||||
query: web::Query<CalendarQuery>,
|
||||
_session: Session,
|
||||
) -> Result<impl Responder> {
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.insert("active_page", "calendar");
|
||||
@@ -25,7 +35,7 @@ impl CalendarController {
|
||||
// Parse the date from the query parameters or use the current date
|
||||
let date = if let Some(date_str) = &query.date {
|
||||
match NaiveDate::parse_from_str(date_str, "%Y-%m-%d") {
|
||||
Ok(naive_date) => Utc.from_utc_date(&naive_date).and_hms_opt(0, 0, 0).unwrap(),
|
||||
Ok(naive_date) => Utc.from_utc_datetime(&naive_date.and_hms_opt(0, 0, 0).unwrap()).into(),
|
||||
Err(_) => Utc::now(),
|
||||
}
|
||||
} else {
|
||||
@@ -37,6 +47,11 @@ impl CalendarController {
|
||||
ctx.insert("current_month", &date.month());
|
||||
ctx.insert("current_day", &date.day());
|
||||
|
||||
// Add user to context if available
|
||||
if let Some(user) = Self::get_user_from_session(&_session) {
|
||||
ctx.insert("user", &user);
|
||||
}
|
||||
|
||||
// Get events for the current view
|
||||
let (start_date, end_date) = match view_mode {
|
||||
CalendarViewMode::Year => {
|
||||
@@ -52,9 +67,9 @@ impl CalendarController {
|
||||
},
|
||||
CalendarViewMode::Week => {
|
||||
// Calculate the start of the week (Sunday)
|
||||
let weekday = date.weekday().num_days_from_sunday();
|
||||
let _weekday = date.weekday().num_days_from_sunday();
|
||||
let start_date = date.date_naive().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap();
|
||||
let start = Utc.from_utc_date(&start_date).and_hms_opt(0, 0, 0).unwrap();
|
||||
let start = Utc.from_utc_datetime(&start_date.and_hms_opt(0, 0, 0).unwrap());
|
||||
let end = start + chrono::Duration::days(7);
|
||||
(start, end)
|
||||
},
|
||||
@@ -210,10 +225,15 @@ impl CalendarController {
|
||||
}
|
||||
|
||||
/// Handles the new event page route
|
||||
pub async fn new_event(tmpl: web::Data<Tera>) -> Result<impl Responder> {
|
||||
pub async fn new_event(tmpl: web::Data<Tera>, _session: Session) -> Result<impl Responder> {
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.insert("active_page", "calendar");
|
||||
|
||||
// Add user to context if available
|
||||
if let Some(user) = Self::get_user_from_session(&_session) {
|
||||
ctx.insert("user", &user);
|
||||
}
|
||||
|
||||
let rendered = tmpl.render("calendar/new_event.html", &ctx)
|
||||
.map_err(|e| {
|
||||
eprintln!("Template rendering error: {}", e);
|
||||
@@ -227,6 +247,7 @@ impl CalendarController {
|
||||
pub async fn create_event(
|
||||
form: web::Form<EventForm>,
|
||||
tmpl: web::Data<Tera>,
|
||||
_session: Session,
|
||||
) -> Result<impl Responder> {
|
||||
// Parse the start and end times
|
||||
let start_time = match DateTime::parse_from_rfc3339(&form.start_time) {
|
||||
@@ -272,6 +293,11 @@ impl CalendarController {
|
||||
ctx.insert("active_page", "calendar");
|
||||
ctx.insert("error", "Failed to save event");
|
||||
|
||||
// Add user to context if available
|
||||
if let Some(user) = Self::get_user_from_session(&_session) {
|
||||
ctx.insert("user", &user);
|
||||
}
|
||||
|
||||
let rendered = tmpl.render("calendar/new_event.html", &ctx)
|
||||
.map_err(|e| {
|
||||
eprintln!("Template rendering error: {}", e);
|
||||
@@ -286,6 +312,7 @@ impl CalendarController {
|
||||
/// Handles the delete event route
|
||||
pub async fn delete_event(
|
||||
path: web::Path<String>,
|
||||
_session: Session,
|
||||
) -> Result<impl Responder> {
|
||||
let id = path.into_inner();
|
||||
|
||||
|
@@ -1,16 +1,29 @@
|
||||
use actix_web::{web, HttpResponse, Responder, Result};
|
||||
use actix_session::Session;
|
||||
use tera::Tera;
|
||||
use crate::models::User;
|
||||
use serde_json::Value;
|
||||
|
||||
/// Controller for handling home-related routes
|
||||
pub struct HomeController;
|
||||
|
||||
impl HomeController {
|
||||
/// Helper function to get user from session
|
||||
fn get_user_from_session(session: &Session) -> Option<Value> {
|
||||
session.get::<String>("user").ok().flatten().and_then(|user_json| {
|
||||
serde_json::from_str(&user_json).ok()
|
||||
})
|
||||
}
|
||||
|
||||
/// Handles the markdown editor page route
|
||||
pub async fn editor(tmpl: web::Data<Tera>) -> Result<impl Responder> {
|
||||
pub async fn editor(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.insert("active_page", "editor");
|
||||
|
||||
// Add user to context if available
|
||||
if let Some(user) = Self::get_user_from_session(&session) {
|
||||
ctx.insert("user", &user);
|
||||
}
|
||||
|
||||
let rendered = tmpl.render("editor.html", &ctx)
|
||||
.map_err(|e| {
|
||||
eprintln!("Template rendering error: {}", e);
|
||||
@@ -21,13 +34,14 @@ impl HomeController {
|
||||
}
|
||||
|
||||
/// Handles the home page route
|
||||
pub async fn index(tmpl: web::Data<Tera>) -> Result<impl Responder> {
|
||||
pub async fn index(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.insert("active_page", "home");
|
||||
|
||||
// Example of using models in controllers
|
||||
let example_user = User::new("John Doe".to_string(), "john@example.com".to_string());
|
||||
ctx.insert("user", &example_user);
|
||||
// Add user to context if available
|
||||
if let Some(user) = Self::get_user_from_session(&session) {
|
||||
ctx.insert("user", &user);
|
||||
}
|
||||
|
||||
let rendered = tmpl.render("index.html", &ctx)
|
||||
.map_err(|e| {
|
||||
@@ -39,10 +53,15 @@ impl HomeController {
|
||||
}
|
||||
|
||||
/// Handles the about page route
|
||||
pub async fn about(tmpl: web::Data<Tera>) -> Result<impl Responder> {
|
||||
pub async fn about(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.insert("active_page", "about");
|
||||
|
||||
// Add user to context if available
|
||||
if let Some(user) = Self::get_user_from_session(&session) {
|
||||
ctx.insert("user", &user);
|
||||
}
|
||||
|
||||
let rendered = tmpl.render("about.html", &ctx)
|
||||
.map_err(|e| {
|
||||
eprintln!("Template rendering error: {}", e);
|
||||
@@ -53,10 +72,15 @@ impl HomeController {
|
||||
}
|
||||
|
||||
/// Handles the contact page route
|
||||
pub async fn contact(tmpl: web::Data<Tera>) -> Result<impl Responder> {
|
||||
pub async fn contact(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.insert("active_page", "contact");
|
||||
|
||||
// Add user to context if available
|
||||
if let Some(user) = Self::get_user_from_session(&session) {
|
||||
ctx.insert("user", &user);
|
||||
}
|
||||
|
||||
let rendered = tmpl.render("contact.html", &ctx)
|
||||
.map_err(|e| {
|
||||
eprintln!("Template rendering error: {}", e);
|
||||
@@ -69,7 +93,8 @@ impl HomeController {
|
||||
/// Handles form submissions from the contact page
|
||||
pub async fn submit_contact(
|
||||
form: web::Form<ContactForm>,
|
||||
tmpl: web::Data<Tera>
|
||||
tmpl: web::Data<Tera>,
|
||||
session: Session
|
||||
) -> Result<impl Responder> {
|
||||
// In a real application, you would process the form data here
|
||||
// For example, save it to a database or send an email
|
||||
@@ -82,6 +107,11 @@ impl HomeController {
|
||||
ctx.insert("active_page", "contact");
|
||||
ctx.insert("success_message", "Your message has been sent successfully!");
|
||||
|
||||
// Add user to context if available
|
||||
if let Some(user) = Self::get_user_from_session(&session) {
|
||||
ctx.insert("user", &user);
|
||||
}
|
||||
|
||||
let rendered = tmpl.render("contact.html", &ctx)
|
||||
.map_err(|e| {
|
||||
eprintln!("Template rendering error: {}", e);
|
||||
|
@@ -2,10 +2,4 @@
|
||||
pub mod home;
|
||||
pub mod auth;
|
||||
pub mod ticket;
|
||||
pub mod calendar;
|
||||
|
||||
// Re-export controllers for easier imports
|
||||
pub use home::HomeController;
|
||||
pub use auth::AuthController;
|
||||
pub use ticket::TicketController;
|
||||
pub use calendar::CalendarController;
|
||||
pub mod calendar;
|
@@ -2,7 +2,8 @@ use actix_web::{web, HttpResponse, Responder, Result};
|
||||
use actix_session::Session;
|
||||
use tera::Tera;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::models::{User, Ticket, TicketComment, TicketStatus, TicketPriority, TicketFilter};
|
||||
use serde_json::Value;
|
||||
use crate::models::{User, Ticket, TicketComment, TicketStatus, TicketPriority};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
@@ -35,6 +36,13 @@ pub struct TicketFilterForm {
|
||||
}
|
||||
|
||||
impl TicketController {
|
||||
/// Helper function to get user from session
|
||||
fn get_user_from_session(session: &Session) -> Option<Value> {
|
||||
session.get::<String>("user").ok().flatten().and_then(|user_json| {
|
||||
serde_json::from_str(&user_json).ok()
|
||||
})
|
||||
}
|
||||
|
||||
/// Lists all tickets with optional filtering
|
||||
pub async fn list_tickets(
|
||||
session: Session,
|
||||
@@ -42,15 +50,8 @@ impl TicketController {
|
||||
tmpl: web::Data<Tera>
|
||||
) -> Result<impl Responder> {
|
||||
// Get the current user from the session
|
||||
let user = match session.get::<String>("user")? {
|
||||
Some(user_json) => {
|
||||
match serde_json::from_str::<User>(&user_json) {
|
||||
Ok(user) => Some(user),
|
||||
Err(_) => None,
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
let user_value = Self::get_user_from_session(&session);
|
||||
let user: Option<User> = user_value.and_then(|v| serde_json::from_value(v).ok());
|
||||
|
||||
// If the user is not logged in, redirect to the login page
|
||||
if user.is_none() {
|
||||
@@ -59,88 +60,75 @@ impl TicketController {
|
||||
.finish());
|
||||
}
|
||||
|
||||
let user = user.unwrap();
|
||||
let user: User = user.unwrap();
|
||||
|
||||
// Create a filter based on the query parameters
|
||||
let mut filter = TicketFilter::default();
|
||||
// Get all tickets from the in-memory storage
|
||||
let tickets = TICKETS.lock().unwrap();
|
||||
|
||||
// Filter tickets based on the query parameters
|
||||
let mut filtered_tickets: Vec<Ticket> = tickets.values().cloned().collect();
|
||||
|
||||
// Apply status filter if provided
|
||||
if let Some(status_str) = &query.status {
|
||||
filter.status = match status_str.as_str() {
|
||||
"open" => Some(TicketStatus::Open),
|
||||
"in_progress" => Some(TicketStatus::InProgress),
|
||||
"waiting_for_customer" => Some(TicketStatus::WaitingForCustomer),
|
||||
"resolved" => Some(TicketStatus::Resolved),
|
||||
"closed" => Some(TicketStatus::Closed),
|
||||
_ => None,
|
||||
};
|
||||
if !status_str.is_empty() {
|
||||
let status = match status_str.as_str() {
|
||||
"open" => TicketStatus::Open,
|
||||
"in_progress" => TicketStatus::InProgress,
|
||||
"resolved" => TicketStatus::Resolved,
|
||||
"closed" => TicketStatus::Closed,
|
||||
_ => TicketStatus::Open,
|
||||
};
|
||||
|
||||
filtered_tickets.retain(|ticket| ticket.status == status);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply priority filter if provided
|
||||
if let Some(priority_str) = &query.priority {
|
||||
filter.priority = match priority_str.as_str() {
|
||||
"low" => Some(TicketPriority::Low),
|
||||
"medium" => Some(TicketPriority::Medium),
|
||||
"high" => Some(TicketPriority::High),
|
||||
"critical" => Some(TicketPriority::Critical),
|
||||
_ => None,
|
||||
};
|
||||
if !priority_str.is_empty() {
|
||||
let priority = match priority_str.as_str() {
|
||||
"low" => TicketPriority::Low,
|
||||
"medium" => TicketPriority::Medium,
|
||||
"high" => TicketPriority::High,
|
||||
"urgent" => TicketPriority::Critical,
|
||||
_ => TicketPriority::Medium,
|
||||
};
|
||||
|
||||
filtered_tickets.retain(|ticket| ticket.priority == priority);
|
||||
}
|
||||
}
|
||||
|
||||
filter.search_term = query.search.clone();
|
||||
|
||||
// If the user is not an admin, only show their tickets
|
||||
// Regular users can only see their own tickets
|
||||
if user.role != crate::models::user::UserRole::Admin {
|
||||
filter.user_id = user.id;
|
||||
filtered_tickets.retain(|ticket| ticket.user_id == user.id.unwrap_or(0));
|
||||
}
|
||||
|
||||
// Get the tickets that match the filter
|
||||
let tickets = {
|
||||
let tickets_map = TICKETS.lock().unwrap();
|
||||
tickets_map.values()
|
||||
.filter(|ticket| {
|
||||
// Filter by status
|
||||
if let Some(status) = &filter.status {
|
||||
if ticket.status != *status {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by priority
|
||||
if let Some(priority) = &filter.priority {
|
||||
if ticket.priority != *priority {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by user ID
|
||||
if let Some(user_id) = filter.user_id {
|
||||
if ticket.user_id != user_id {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by search term
|
||||
if let Some(term) = &filter.search_term {
|
||||
if !ticket.title.to_lowercase().contains(&term.to_lowercase()) &&
|
||||
!ticket.description.to_lowercase().contains(&term.to_lowercase()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
// Sort tickets by created_at (newest first)
|
||||
filtered_tickets.sort_by(|a, b| b.created_at.cmp(&a.created_at));
|
||||
|
||||
// Prepare the template context
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.insert("active_page", "tickets");
|
||||
ctx.insert("tickets", &filtered_tickets);
|
||||
ctx.insert("user", &user);
|
||||
ctx.insert("tickets", &tickets);
|
||||
// Extract the query parameters for the template
|
||||
ctx.insert("status", &query.status);
|
||||
ctx.insert("priority", &query.priority);
|
||||
ctx.insert("search", &query.search);
|
||||
ctx.insert("filter", &query.into_inner());
|
||||
|
||||
// Add filter options for the dropdown menus
|
||||
ctx.insert("statuses", &[
|
||||
("", "All Statuses"),
|
||||
("open", "Open"),
|
||||
("in_progress", "In Progress"),
|
||||
("resolved", "Resolved"),
|
||||
("closed", "Closed"),
|
||||
]);
|
||||
|
||||
ctx.insert("priorities", &[
|
||||
("", "All Priorities"),
|
||||
("low", "Low"),
|
||||
("medium", "Medium"),
|
||||
("high", "High"),
|
||||
("urgent", "Urgent"),
|
||||
]);
|
||||
|
||||
// Render the template
|
||||
let rendered = tmpl.render("tickets/list.html", &ctx)
|
||||
@@ -158,15 +146,8 @@ impl TicketController {
|
||||
tmpl: web::Data<Tera>
|
||||
) -> Result<impl Responder> {
|
||||
// Get the current user from the session
|
||||
let user = match session.get::<String>("user")? {
|
||||
Some(user_json) => {
|
||||
match serde_json::from_str::<User>(&user_json) {
|
||||
Ok(user) => Some(user),
|
||||
Err(_) => None,
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
let user_value = Self::get_user_from_session(&session);
|
||||
let user: Option<User> = user_value.and_then(|v| serde_json::from_value(v).ok());
|
||||
|
||||
// If the user is not logged in, redirect to the login page
|
||||
if user.is_none() {
|
||||
@@ -175,19 +156,20 @@ impl TicketController {
|
||||
.finish());
|
||||
}
|
||||
|
||||
let user = user.unwrap();
|
||||
let user: User = user.unwrap();
|
||||
|
||||
// Prepare the template context
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.insert("active_page", "tickets");
|
||||
ctx.insert("user", &user);
|
||||
|
||||
// Add an empty form to the context to avoid template errors
|
||||
ctx.insert("form", &serde_json::json!({
|
||||
"title": "",
|
||||
"priority": "medium",
|
||||
"description": ""
|
||||
}));
|
||||
// Add priority options for the dropdown menu
|
||||
ctx.insert("priorities", &[
|
||||
("low", "Low"),
|
||||
("medium", "Medium"),
|
||||
("high", "High"),
|
||||
("urgent", "Urgent"),
|
||||
]);
|
||||
|
||||
// Render the template
|
||||
let rendered = tmpl.render("tickets/new.html", &ctx)
|
||||
@@ -206,15 +188,8 @@ impl TicketController {
|
||||
_tmpl: web::Data<Tera>
|
||||
) -> Result<impl Responder> {
|
||||
// Get the current user from the session
|
||||
let user = match session.get::<String>("user")? {
|
||||
Some(user_json) => {
|
||||
match serde_json::from_str::<User>(&user_json) {
|
||||
Ok(user) => Some(user),
|
||||
Err(_) => None,
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
let user_value = Self::get_user_from_session(&session);
|
||||
let user: Option<User> = user_value.and_then(|v| serde_json::from_value(v).ok());
|
||||
|
||||
// If the user is not logged in, redirect to the login page
|
||||
if user.is_none() {
|
||||
@@ -223,20 +198,18 @@ impl TicketController {
|
||||
.finish());
|
||||
}
|
||||
|
||||
let user = user.unwrap();
|
||||
let user: User = user.unwrap();
|
||||
|
||||
// Skip validation and always create the ticket
|
||||
|
||||
// Parse the priority
|
||||
// Parse the priority from the form
|
||||
let priority = match form.priority.as_str() {
|
||||
"low" => TicketPriority::Low,
|
||||
"medium" => TicketPriority::Medium,
|
||||
"high" => TicketPriority::High,
|
||||
"critical" => TicketPriority::Critical,
|
||||
"urgent" => TicketPriority::Critical,
|
||||
_ => TicketPriority::Medium,
|
||||
};
|
||||
|
||||
// Create the ticket
|
||||
// Create a new ticket
|
||||
let ticket = Ticket::new(
|
||||
user.id.unwrap_or(0),
|
||||
form.title.clone(),
|
||||
@@ -244,15 +217,13 @@ impl TicketController {
|
||||
priority
|
||||
);
|
||||
|
||||
// Store the ticket
|
||||
{
|
||||
let mut tickets_map = TICKETS.lock().unwrap();
|
||||
tickets_map.insert(ticket.id.clone(), ticket.clone());
|
||||
}
|
||||
// Add the ticket to the in-memory storage
|
||||
let mut tickets = TICKETS.lock().unwrap();
|
||||
tickets.insert(ticket.id.clone(), ticket.clone());
|
||||
|
||||
// Redirect to the ticket detail page
|
||||
// Redirect to the ticket list page
|
||||
Ok(HttpResponse::Found()
|
||||
.append_header(("Location", format!("/tickets/{}", ticket.id)))
|
||||
.append_header(("Location", "/tickets"))
|
||||
.finish())
|
||||
}
|
||||
|
||||
@@ -263,15 +234,8 @@ impl TicketController {
|
||||
tmpl: web::Data<Tera>
|
||||
) -> Result<impl Responder> {
|
||||
// Get the current user from the session
|
||||
let user = match session.get::<String>("user")? {
|
||||
Some(user_json) => {
|
||||
match serde_json::from_str::<User>(&user_json) {
|
||||
Ok(user) => Some(user),
|
||||
Err(_) => None,
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
let user_value = Self::get_user_from_session(&session);
|
||||
let user: Option<User> = user_value.and_then(|v| serde_json::from_value(v).ok());
|
||||
|
||||
// If the user is not logged in, redirect to the login page
|
||||
if user.is_none() {
|
||||
@@ -280,38 +244,45 @@ impl TicketController {
|
||||
.finish());
|
||||
}
|
||||
|
||||
let user = user.unwrap();
|
||||
let ticket_id = path.into_inner();
|
||||
let user: User = user.unwrap();
|
||||
|
||||
// Get the ticket
|
||||
let ticket = {
|
||||
let tickets_map = TICKETS.lock().unwrap();
|
||||
match tickets_map.get(&ticket_id) {
|
||||
Some(ticket) => ticket.clone(),
|
||||
None => {
|
||||
// Ticket not found, redirect to the tickets list
|
||||
return Ok(HttpResponse::Found()
|
||||
.append_header(("Location", "/tickets"))
|
||||
.finish());
|
||||
}
|
||||
// Get the ticket ID from the path
|
||||
let id = path.into_inner();
|
||||
|
||||
// Get the ticket from the in-memory storage
|
||||
let tickets = TICKETS.lock().unwrap();
|
||||
let ticket = match tickets.get(&id) {
|
||||
Some(ticket) => ticket.clone(),
|
||||
None => {
|
||||
return Ok(HttpResponse::NotFound().finish());
|
||||
}
|
||||
};
|
||||
|
||||
// Regular users can only see their own tickets
|
||||
if user.role != crate::models::user::UserRole::Admin && ticket.user_id != user.id.unwrap_or(0) {
|
||||
return Ok(HttpResponse::Forbidden().finish());
|
||||
}
|
||||
|
||||
// Get the comments for this ticket
|
||||
let comments = {
|
||||
let comments_map = COMMENTS.lock().unwrap();
|
||||
match comments_map.get(&ticket_id) {
|
||||
Some(comments) => comments.clone(),
|
||||
None => Vec::new(),
|
||||
}
|
||||
};
|
||||
let comments = COMMENTS.lock().unwrap();
|
||||
let ticket_comments = comments.get(&ticket.id)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
// Prepare the template context
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.insert("active_page", "tickets");
|
||||
ctx.insert("user", &user);
|
||||
ctx.insert("ticket", &ticket);
|
||||
ctx.insert("comments", &comments);
|
||||
ctx.insert("comments", &ticket_comments);
|
||||
ctx.insert("user", &user);
|
||||
|
||||
// Add status options for the dropdown menu (for admins)
|
||||
ctx.insert("statuses", &[
|
||||
("open", "Open"),
|
||||
("in_progress", "In Progress"),
|
||||
("resolved", "Resolved"),
|
||||
("closed", "Closed"),
|
||||
]);
|
||||
|
||||
// Render the template
|
||||
let rendered = tmpl.render("tickets/show.html", &ctx)
|
||||
@@ -330,15 +301,8 @@ impl TicketController {
|
||||
form: web::Form<NewCommentForm>
|
||||
) -> Result<impl Responder> {
|
||||
// Get the current user from the session
|
||||
let user = match session.get::<String>("user")? {
|
||||
Some(user_json) => {
|
||||
match serde_json::from_str::<User>(&user_json) {
|
||||
Ok(user) => Some(user),
|
||||
Err(_) => None,
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
let user_value = Self::get_user_from_session(&session);
|
||||
let user: Option<User> = user_value.and_then(|v| serde_json::from_value(v).ok());
|
||||
|
||||
// If the user is not logged in, redirect to the login page
|
||||
if user.is_none() {
|
||||
@@ -347,62 +311,53 @@ impl TicketController {
|
||||
.finish());
|
||||
}
|
||||
|
||||
let user = user.unwrap();
|
||||
let ticket_id = path.into_inner();
|
||||
let user: User = user.unwrap();
|
||||
|
||||
// Validate the form data
|
||||
if form.content.trim().is_empty() {
|
||||
// Comment is empty, redirect back to the ticket
|
||||
return Ok(HttpResponse::Found()
|
||||
.append_header(("Location", format!("/tickets/{}", ticket_id)))
|
||||
.finish());
|
||||
}
|
||||
// Get the ticket ID from the path
|
||||
let id = path.into_inner();
|
||||
|
||||
// Check if the ticket exists
|
||||
{
|
||||
let tickets_map = TICKETS.lock().unwrap();
|
||||
if !tickets_map.contains_key(&ticket_id) {
|
||||
// Ticket not found, redirect to the tickets list
|
||||
return Ok(HttpResponse::Found()
|
||||
.append_header(("Location", "/tickets"))
|
||||
.finish());
|
||||
// Get the ticket from the in-memory storage
|
||||
let tickets = TICKETS.lock().unwrap();
|
||||
let ticket = match tickets.get(&id) {
|
||||
Some(ticket) => ticket.clone(),
|
||||
None => {
|
||||
return Ok(HttpResponse::NotFound().finish());
|
||||
}
|
||||
};
|
||||
|
||||
// Regular users can only comment on their own tickets
|
||||
if user.role != crate::models::user::UserRole::Admin && ticket.user_id != user.id.unwrap_or(0) {
|
||||
return Ok(HttpResponse::Forbidden().finish());
|
||||
}
|
||||
|
||||
// Create the comment
|
||||
// Create a new comment
|
||||
let comment = TicketComment::new(
|
||||
ticket_id.clone(),
|
||||
ticket.id.clone(),
|
||||
user.id.unwrap_or(0),
|
||||
form.content.clone(),
|
||||
user.is_admin()
|
||||
user.role == crate::models::user::UserRole::Admin
|
||||
);
|
||||
|
||||
// Store the comment
|
||||
{
|
||||
let mut comments_map = COMMENTS.lock().unwrap();
|
||||
if let Some(comments) = comments_map.get_mut(&ticket_id) {
|
||||
comments.push(comment);
|
||||
} else {
|
||||
comments_map.insert(ticket_id.clone(), vec![comment]);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the ticket status if the user is an admin
|
||||
if user.role == crate::models::user::UserRole::Admin {
|
||||
let mut tickets_map = TICKETS.lock().unwrap();
|
||||
if let Some(ticket) = tickets_map.get_mut(&ticket_id) {
|
||||
ticket.update_status(TicketStatus::WaitingForCustomer);
|
||||
}
|
||||
// Add the comment to the in-memory storage
|
||||
let mut comments = COMMENTS.lock().unwrap();
|
||||
if let Some(ticket_comments) = comments.get_mut(&ticket.id) {
|
||||
ticket_comments.push(comment);
|
||||
} else {
|
||||
let mut tickets_map = TICKETS.lock().unwrap();
|
||||
if let Some(ticket) = tickets_map.get_mut(&ticket_id) {
|
||||
ticket.update_status(TicketStatus::Open);
|
||||
comments.insert(ticket.id.clone(), vec![comment]);
|
||||
}
|
||||
|
||||
// If the ticket is closed, reopen it when a comment is added
|
||||
if ticket.status == TicketStatus::Closed {
|
||||
let mut tickets = TICKETS.lock().unwrap();
|
||||
if let Some(ticket) = tickets.get_mut(&id) {
|
||||
ticket.status = TicketStatus::Open;
|
||||
ticket.updated_at = chrono::Utc::now();
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect back to the ticket
|
||||
// Redirect back to the ticket page
|
||||
Ok(HttpResponse::Found()
|
||||
.append_header(("Location", format!("/tickets/{}", ticket_id)))
|
||||
.append_header(("Location", format!("/tickets/{}", id)))
|
||||
.finish())
|
||||
}
|
||||
|
||||
@@ -412,15 +367,8 @@ impl TicketController {
|
||||
path: web::Path<(String, String)>,
|
||||
) -> Result<impl Responder> {
|
||||
// Get the current user from the session
|
||||
let user = match session.get::<String>("user")? {
|
||||
Some(user_json) => {
|
||||
match serde_json::from_str::<User>(&user_json) {
|
||||
Ok(user) => Some(user),
|
||||
Err(_) => None,
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
let user_value = Self::get_user_from_session(&session);
|
||||
let user: Option<User> = user_value.and_then(|v| serde_json::from_value(v).ok());
|
||||
|
||||
// If the user is not logged in or not an admin, redirect to the login page
|
||||
if user.is_none() || user.as_ref().unwrap().role != crate::models::user::UserRole::Admin {
|
||||
@@ -429,39 +377,30 @@ impl TicketController {
|
||||
.finish());
|
||||
}
|
||||
|
||||
let (ticket_id, status_str) = path.into_inner();
|
||||
// Get the ticket ID and status from the path
|
||||
let (id, status_str) = path.into_inner();
|
||||
|
||||
// Parse the status
|
||||
let status = match status_str.as_str() {
|
||||
"open" => TicketStatus::Open,
|
||||
"in_progress" => TicketStatus::InProgress,
|
||||
"waiting_for_customer" => TicketStatus::WaitingForCustomer,
|
||||
"resolved" => TicketStatus::Resolved,
|
||||
"closed" => TicketStatus::Closed,
|
||||
_ => {
|
||||
// Invalid status, redirect back to the ticket
|
||||
return Ok(HttpResponse::Found()
|
||||
.append_header(("Location", format!("/tickets/{}", ticket_id)))
|
||||
.finish());
|
||||
}
|
||||
_ => TicketStatus::Open,
|
||||
};
|
||||
|
||||
// Update the ticket status
|
||||
{
|
||||
let mut tickets_map = TICKETS.lock().unwrap();
|
||||
if let Some(ticket) = tickets_map.get_mut(&ticket_id) {
|
||||
ticket.update_status(status);
|
||||
} else {
|
||||
// Ticket not found, redirect to the tickets list
|
||||
return Ok(HttpResponse::Found()
|
||||
.append_header(("Location", "/tickets"))
|
||||
.finish());
|
||||
}
|
||||
let mut tickets = TICKETS.lock().unwrap();
|
||||
if let Some(ticket) = tickets.get_mut(&id) {
|
||||
ticket.status = status;
|
||||
ticket.updated_at = chrono::Utc::now();
|
||||
} else {
|
||||
return Ok(HttpResponse::NotFound().finish());
|
||||
}
|
||||
|
||||
// Redirect back to the ticket
|
||||
// Redirect back to the ticket page
|
||||
Ok(HttpResponse::Found()
|
||||
.append_header(("Location", format!("/tickets/{}", ticket_id)))
|
||||
.append_header(("Location", format!("/tickets/{}", id)))
|
||||
.finish())
|
||||
}
|
||||
|
||||
@@ -471,15 +410,8 @@ impl TicketController {
|
||||
tmpl: web::Data<Tera>
|
||||
) -> Result<impl Responder> {
|
||||
// Get the current user from the session
|
||||
let user = match session.get::<String>("user")? {
|
||||
Some(user_json) => {
|
||||
match serde_json::from_str::<User>(&user_json) {
|
||||
Ok(user) => Some(user),
|
||||
Err(_) => None,
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
let user_value = Self::get_user_from_session(&session);
|
||||
let user: Option<User> = user_value.and_then(|v| serde_json::from_value(v).ok());
|
||||
|
||||
// If the user is not logged in, redirect to the login page
|
||||
if user.is_none() {
|
||||
@@ -488,25 +420,30 @@ impl TicketController {
|
||||
.finish());
|
||||
}
|
||||
|
||||
let user = user.unwrap();
|
||||
let user: User = user.unwrap();
|
||||
|
||||
// Get the user's tickets
|
||||
let tickets = {
|
||||
let tickets_map = TICKETS.lock().unwrap();
|
||||
tickets_map.values()
|
||||
.filter(|ticket| ticket.user_id == user.id.unwrap_or(0))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
// Get all tickets from the in-memory storage
|
||||
let tickets = TICKETS.lock().unwrap();
|
||||
|
||||
// Filter tickets to only show the user's tickets
|
||||
let my_tickets: Vec<Ticket> = tickets.values()
|
||||
.cloned()
|
||||
.filter(|ticket| ticket.user_id == user.id.unwrap_or(0))
|
||||
.collect();
|
||||
|
||||
// Sort tickets by created_at (newest first)
|
||||
let mut sorted_tickets = my_tickets;
|
||||
sorted_tickets.sort_by(|a, b| b.created_at.cmp(&a.created_at));
|
||||
|
||||
// Prepare the template context
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.insert("active_page", "tickets");
|
||||
ctx.insert("tickets", &sorted_tickets);
|
||||
ctx.insert("user", &user);
|
||||
ctx.insert("tickets", &tickets);
|
||||
ctx.insert("my_tickets", &true);
|
||||
|
||||
// Render the template
|
||||
let rendered = tmpl.render("tickets/my_tickets.html", &ctx)
|
||||
let rendered = tmpl.render("tickets/list.html", &ctx)
|
||||
.map_err(|e| {
|
||||
eprintln!("Template rendering error: {}", e);
|
||||
actix_web::error::ErrorInternalServerError("Template rendering error")
|
||||
|
Reference in New Issue
Block a user