...
This commit is contained in:
114
actix_mvc_app/src/controllers/auth.rs
Normal file
114
actix_mvc_app/src/controllers/auth.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
use actix_web::{web, HttpResponse, Responder, Result};
|
||||
use actix_session::Session;
|
||||
use tera::Tera;
|
||||
use crate::models::user::{User, LoginCredentials, RegistrationData};
|
||||
|
||||
/// Controller for handling authentication-related routes
|
||||
pub struct AuthController;
|
||||
|
||||
impl AuthController {
|
||||
/// Renders the login page
|
||||
pub async fn login_page(tmpl: web::Data<Tera>) -> Result<impl Responder> {
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.insert("active_page", "login");
|
||||
|
||||
let rendered = tmpl.render("auth/login.html", &ctx)
|
||||
.map_err(|e| {
|
||||
eprintln!("Template rendering error: {}", e);
|
||||
actix_web::error::ErrorInternalServerError("Template rendering error")
|
||||
})?;
|
||||
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
|
||||
}
|
||||
|
||||
/// Handles user login
|
||||
pub async fn login(
|
||||
form: web::Form<LoginCredentials>,
|
||||
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
|
||||
// Create a user object with admin role
|
||||
let mut test_user = User::new(
|
||||
"Admin User".to_string(),
|
||||
form.email.clone()
|
||||
);
|
||||
|
||||
// Set the ID and admin role
|
||||
test_user.id = Some(1);
|
||||
test_user.role = crate::models::user::UserRole::Admin;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Redirect to the home page
|
||||
Ok(HttpResponse::Found()
|
||||
.append_header(("Location", "/"))
|
||||
.finish())
|
||||
|
||||
let rendered = tmpl.render("auth/login.html", &ctx)
|
||||
.map_err(|e| {
|
||||
eprintln!("Template rendering error: {}", e);
|
||||
actix_web::error::ErrorInternalServerError("Template rendering error")
|
||||
})?;
|
||||
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
|
||||
}
|
||||
|
||||
/// Renders the registration page
|
||||
pub async fn register_page(tmpl: web::Data<Tera>) -> Result<impl Responder> {
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.insert("active_page", "register");
|
||||
|
||||
let rendered = tmpl.render("auth/register.html", &ctx)
|
||||
.map_err(|e| {
|
||||
eprintln!("Template rendering error: {}", e);
|
||||
actix_web::error::ErrorInternalServerError("Template rendering error")
|
||||
})?;
|
||||
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
|
||||
}
|
||||
|
||||
/// Handles user registration
|
||||
pub async fn register(
|
||||
form: web::Form<RegistrationData>,
|
||||
session: Session,
|
||||
tmpl: web::Data<Tera>
|
||||
) -> Result<impl Responder> {
|
||||
// Skip validation and always create an admin user
|
||||
let mut user = User::new(
|
||||
form.name.clone(),
|
||||
form.email.clone()
|
||||
);
|
||||
|
||||
// Set the ID and admin role
|
||||
user.id = Some(1);
|
||||
user.role = crate::models::user::UserRole::Admin;
|
||||
|
||||
// Store user data in session
|
||||
let user_json = serde_json::to_string(&user).unwrap();
|
||||
session.insert("user", &user_json).unwrap();
|
||||
|
||||
// Redirect to the home page
|
||||
Ok(HttpResponse::Found()
|
||||
.append_header(("Location", "/"))
|
||||
.finish())
|
||||
}
|
||||
|
||||
/// Handles user logout
|
||||
pub async fn logout(session: Session) -> Result<impl Responder> {
|
||||
// Clear the session
|
||||
session.purge();
|
||||
|
||||
// Redirect to the home page
|
||||
Ok(HttpResponse::Found()
|
||||
.append_header(("Location", "/"))
|
||||
.finish())
|
||||
}
|
||||
}
|
@@ -1,5 +1,9 @@
|
||||
// Export controllers
|
||||
pub mod home;
|
||||
pub mod auth;
|
||||
pub mod ticket;
|
||||
|
||||
// Re-export controllers for easier imports
|
||||
pub use home::HomeController;
|
||||
pub use home::HomeController;
|
||||
pub use auth::AuthController;
|
||||
pub use ticket::TicketController;
|
517
actix_mvc_app/src/controllers/ticket.rs
Normal file
517
actix_mvc_app/src/controllers/ticket.rs
Normal file
@@ -0,0 +1,517 @@
|
||||
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 std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
// In-memory storage for tickets and comments (in a real app, this would be a database)
|
||||
lazy_static::lazy_static! {
|
||||
static ref TICKETS: Arc<Mutex<HashMap<String, Ticket>>> = Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref COMMENTS: Arc<Mutex<HashMap<String, Vec<TicketComment>>>> = Arc::new(Mutex::new(HashMap::new()));
|
||||
}
|
||||
|
||||
/// Controller for handling ticket-related routes
|
||||
pub struct TicketController;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct NewTicketForm {
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub priority: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct NewCommentForm {
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct TicketFilterForm {
|
||||
pub status: Option<String>,
|
||||
pub priority: Option<String>,
|
||||
pub search: Option<String>,
|
||||
}
|
||||
|
||||
impl TicketController {
|
||||
/// Lists all tickets with optional filtering
|
||||
pub async fn list_tickets(
|
||||
session: Session,
|
||||
query: web::Query<TicketFilterForm>,
|
||||
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,
|
||||
};
|
||||
|
||||
// If the user is not logged in, redirect to the login page
|
||||
if user.is_none() {
|
||||
return Ok(HttpResponse::Found()
|
||||
.append_header(("Location", "/login"))
|
||||
.finish());
|
||||
}
|
||||
|
||||
let user = user.unwrap();
|
||||
|
||||
// Create a filter based on the query parameters
|
||||
let mut filter = TicketFilter::default();
|
||||
|
||||
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 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,
|
||||
};
|
||||
}
|
||||
|
||||
filter.search_term = query.search.clone();
|
||||
|
||||
// If the user is not an admin, only show their tickets
|
||||
if user.role != crate::models::user::UserRole::Admin {
|
||||
filter.user_id = user.id;
|
||||
}
|
||||
|
||||
// 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<_>>()
|
||||
};
|
||||
|
||||
// Prepare the template context
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.insert("active_page", "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);
|
||||
|
||||
// Render the template
|
||||
let rendered = tmpl.render("tickets/list.html", &ctx)
|
||||
.map_err(|e| {
|
||||
eprintln!("Template rendering error: {}", e);
|
||||
actix_web::error::ErrorInternalServerError("Template rendering error")
|
||||
})?;
|
||||
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
|
||||
}
|
||||
|
||||
/// Shows the form for creating a new ticket
|
||||
pub async fn new_ticket(
|
||||
session: Session,
|
||||
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,
|
||||
};
|
||||
|
||||
// If the user is not logged in, redirect to the login page
|
||||
if user.is_none() {
|
||||
return Ok(HttpResponse::Found()
|
||||
.append_header(("Location", "/login"))
|
||||
.finish());
|
||||
}
|
||||
|
||||
let 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": ""
|
||||
}));
|
||||
|
||||
// Render the template
|
||||
let rendered = tmpl.render("tickets/new.html", &ctx)
|
||||
.map_err(|e| {
|
||||
eprintln!("Template rendering error: {}", e);
|
||||
actix_web::error::ErrorInternalServerError("Template rendering error")
|
||||
})?;
|
||||
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
|
||||
}
|
||||
|
||||
/// Creates a new ticket
|
||||
pub async fn create_ticket(
|
||||
session: Session,
|
||||
form: web::Form<NewTicketForm>,
|
||||
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,
|
||||
};
|
||||
|
||||
// If the user is not logged in, redirect to the login page
|
||||
if user.is_none() {
|
||||
return Ok(HttpResponse::Found()
|
||||
.append_header(("Location", "/login"))
|
||||
.finish());
|
||||
}
|
||||
|
||||
let user = user.unwrap();
|
||||
|
||||
// Skip validation and always create the ticket
|
||||
|
||||
// Parse the priority
|
||||
let priority = match form.priority.as_str() {
|
||||
"low" => TicketPriority::Low,
|
||||
"medium" => TicketPriority::Medium,
|
||||
"high" => TicketPriority::High,
|
||||
"critical" => TicketPriority::Critical,
|
||||
_ => TicketPriority::Medium,
|
||||
};
|
||||
|
||||
// Create the ticket
|
||||
let ticket = Ticket::new(
|
||||
user.id.unwrap_or(0),
|
||||
form.title.clone(),
|
||||
form.description.clone(),
|
||||
priority
|
||||
);
|
||||
|
||||
// Store the ticket
|
||||
{
|
||||
let mut tickets_map = TICKETS.lock().unwrap();
|
||||
tickets_map.insert(ticket.id.clone(), ticket.clone());
|
||||
}
|
||||
|
||||
// Redirect to the ticket detail page
|
||||
Ok(HttpResponse::Found()
|
||||
.append_header(("Location", format!("/tickets/{}", ticket.id)))
|
||||
.finish())
|
||||
}
|
||||
|
||||
/// Shows the details of a ticket
|
||||
pub async fn show_ticket(
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
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,
|
||||
};
|
||||
|
||||
// If the user is not logged in, redirect to the login page
|
||||
if user.is_none() {
|
||||
return Ok(HttpResponse::Found()
|
||||
.append_header(("Location", "/login"))
|
||||
.finish());
|
||||
}
|
||||
|
||||
let user = user.unwrap();
|
||||
let ticket_id = path.into_inner();
|
||||
|
||||
// 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 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(),
|
||||
}
|
||||
};
|
||||
|
||||
// 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);
|
||||
|
||||
// Render the template
|
||||
let rendered = tmpl.render("tickets/show.html", &ctx)
|
||||
.map_err(|e| {
|
||||
eprintln!("Template rendering error: {}", e);
|
||||
actix_web::error::ErrorInternalServerError("Template rendering error")
|
||||
})?;
|
||||
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
|
||||
}
|
||||
|
||||
/// Adds a comment to a ticket
|
||||
pub async fn add_comment(
|
||||
session: Session,
|
||||
path: web::Path<String>,
|
||||
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,
|
||||
};
|
||||
|
||||
// If the user is not logged in, redirect to the login page
|
||||
if user.is_none() {
|
||||
return Ok(HttpResponse::Found()
|
||||
.append_header(("Location", "/login"))
|
||||
.finish());
|
||||
}
|
||||
|
||||
let user = user.unwrap();
|
||||
let ticket_id = path.into_inner();
|
||||
|
||||
// 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());
|
||||
}
|
||||
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
|
||||
// Create the comment
|
||||
let comment = TicketComment::new(
|
||||
ticket_id.clone(),
|
||||
user.id.unwrap_or(0),
|
||||
form.content.clone(),
|
||||
user.is_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);
|
||||
}
|
||||
} else {
|
||||
let mut tickets_map = TICKETS.lock().unwrap();
|
||||
if let Some(ticket) = tickets_map.get_mut(&ticket_id) {
|
||||
ticket.update_status(TicketStatus::Open);
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect back to the ticket
|
||||
Ok(HttpResponse::Found()
|
||||
.append_header(("Location", format!("/tickets/{}", ticket_id)))
|
||||
.finish())
|
||||
}
|
||||
|
||||
/// Updates the status of a ticket
|
||||
pub async fn update_status(
|
||||
session: Session,
|
||||
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,
|
||||
};
|
||||
|
||||
// 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 {
|
||||
return Ok(HttpResponse::Found()
|
||||
.append_header(("Location", "/login"))
|
||||
.finish());
|
||||
}
|
||||
|
||||
let (ticket_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());
|
||||
}
|
||||
};
|
||||
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect back to the ticket
|
||||
Ok(HttpResponse::Found()
|
||||
.append_header(("Location", format!("/tickets/{}", ticket_id)))
|
||||
.finish())
|
||||
}
|
||||
|
||||
/// Shows the user's tickets
|
||||
pub async fn my_tickets(
|
||||
session: Session,
|
||||
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,
|
||||
};
|
||||
|
||||
// If the user is not logged in, redirect to the login page
|
||||
if user.is_none() {
|
||||
return Ok(HttpResponse::Found()
|
||||
.append_header(("Location", "/login"))
|
||||
.finish());
|
||||
}
|
||||
|
||||
let 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<_>>()
|
||||
};
|
||||
|
||||
// Prepare the template context
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.insert("active_page", "tickets");
|
||||
ctx.insert("user", &user);
|
||||
ctx.insert("tickets", &tickets);
|
||||
|
||||
// Render the template
|
||||
let rendered = tmpl.render("tickets/my_tickets.html", &ctx)
|
||||
.map_err(|e| {
|
||||
eprintln!("Template rendering error: {}", e);
|
||||
actix_web::error::ErrorInternalServerError("Template rendering error")
|
||||
})?;
|
||||
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user