....
This commit is contained in:
parent
8f8016e748
commit
86bb165bf2
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
target/
|
2742
actix_mvc_app/Cargo.lock
generated
Normal file
2742
actix_mvc_app/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
actix_mvc_app/Cargo.toml
Normal file
18
actix_mvc_app/Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "actix_mvc_app"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
actix-web = "4.5.1"
|
||||
actix-files = "0.6.5"
|
||||
tera = "1.19.1"
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
serde_json = "1.0.114"
|
||||
env_logger = "0.11.2"
|
||||
log = "0.4.21"
|
||||
dotenv = "0.15.0"
|
||||
chrono = { version = "0.4.35", features = ["serde"] }
|
||||
config = "0.14.0"
|
||||
num_cpus = "1.16.0"
|
||||
futures = "0.3.30"
|
85
actix_mvc_app/README.md
Normal file
85
actix_mvc_app/README.md
Normal file
@ -0,0 +1,85 @@
|
||||
# Actix MVC App
|
||||
|
||||
A Rust web application built with Actix Web, Tera templates, and Bootstrap 5.3.5, following the MVC (Model-View-Controller) architectural pattern.
|
||||
|
||||
## Features
|
||||
|
||||
- **Actix Web**: A powerful, pragmatic, and extremely fast web framework for Rust
|
||||
- **Tera Templates**: A template engine inspired by Jinja2 and Django templates
|
||||
- **Bootstrap 5.3.5**: A popular CSS framework for responsive web design
|
||||
- **MVC Architecture**: Clean separation of concerns with Models, Views, and Controllers
|
||||
- **Middleware Support**: Custom middleware for request timing and security headers
|
||||
- **Configuration Management**: Flexible configuration system with environment variable support
|
||||
- **Static File Serving**: Serve CSS, JavaScript, and other static assets
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
actix_mvc_app/
|
||||
├── Cargo.toml # Project dependencies
|
||||
├── src/
|
||||
│ ├── config/ # Configuration management
|
||||
│ ├── controllers/ # Request handlers
|
||||
│ ├── middleware/ # Custom middleware components
|
||||
│ ├── models/ # Data models and business logic
|
||||
│ ├── routes/ # Route definitions
|
||||
│ ├── static/ # Static assets (CSS, JS, images)
|
||||
│ │ ├── css/ # CSS files including Bootstrap
|
||||
│ │ ├── js/ # JavaScript files
|
||||
│ │ └── images/ # Image files
|
||||
│ ├── utils/ # Utility functions
|
||||
│ ├── views/ # Tera templates
|
||||
│ └── main.rs # Application entry point
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Rust and Cargo (latest stable version)
|
||||
|
||||
### Installation
|
||||
|
||||
1. Clone the repository:
|
||||
```
|
||||
git clone https://github.com/yourusername/actix_mvc_app.git
|
||||
cd actix_mvc_app
|
||||
```
|
||||
|
||||
2. Build the project:
|
||||
```
|
||||
cargo build
|
||||
```
|
||||
|
||||
3. Run the application:
|
||||
```
|
||||
cargo run
|
||||
```
|
||||
|
||||
4. Open your browser and navigate to `http://localhost:8080`
|
||||
|
||||
## Configuration
|
||||
|
||||
The application can be configured using environment variables or configuration files. The following environment variables are supported:
|
||||
|
||||
- `APP__SERVER__HOST`: The host address to bind to (default: 127.0.0.1)
|
||||
- `APP__SERVER__PORT`: The port to listen on (default: 8080)
|
||||
- `APP__SERVER__WORKERS`: The number of worker threads (default: number of CPU cores)
|
||||
- `APP__TEMPLATES__DIR`: The directory containing templates (default: ./src/views)
|
||||
|
||||
## Development
|
||||
|
||||
### Adding a New Page
|
||||
|
||||
1. Create a new template in the `src/views` directory
|
||||
2. Add a new handler method in the appropriate controller
|
||||
3. Add a new route in the `src/routes/mod.rs` file
|
||||
|
||||
### Adding a New Model
|
||||
|
||||
1. Create a new model file in the `src/models` directory
|
||||
2. Add the model to the `src/models/mod.rs` file
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
129
actix_mvc_app/src/app_middleware/mod.rs
Normal file
129
actix_mvc_app/src/app_middleware/mod.rs
Normal file
@ -0,0 +1,129 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
64
actix_mvc_app/src/config/mod.rs
Normal file
64
actix_mvc_app/src/config/mod.rs
Normal file
@ -0,0 +1,64 @@
|
||||
use std::env;
|
||||
use config::{Config, ConfigError, File};
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Application configuration
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct AppConfig {
|
||||
/// Server configuration
|
||||
pub server: ServerConfig,
|
||||
/// Template configuration
|
||||
pub templates: TemplateConfig,
|
||||
}
|
||||
|
||||
/// Server configuration
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct ServerConfig {
|
||||
/// Host address to bind to
|
||||
pub host: String,
|
||||
/// Port to listen on
|
||||
pub port: u16,
|
||||
/// Workers count
|
||||
pub workers: Option<u32>,
|
||||
}
|
||||
|
||||
/// Template configuration
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct TemplateConfig {
|
||||
/// Directory containing templates
|
||||
pub dir: String,
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
/// Loads configuration from files and environment variables
|
||||
pub fn new() -> Result<Self, ConfigError> {
|
||||
// Set default values
|
||||
let mut config_builder = Config::builder()
|
||||
.set_default("server.host", "127.0.0.1")?
|
||||
.set_default("server.port", 8080)?
|
||||
.set_default("server.workers", None::<u32>)?
|
||||
.set_default("templates.dir", "./src/views")?;
|
||||
|
||||
// Load from config file if it exists
|
||||
if let Ok(config_path) = env::var("APP_CONFIG") {
|
||||
config_builder = config_builder.add_source(File::with_name(&config_path));
|
||||
} else {
|
||||
// Try to load from default locations
|
||||
config_builder = config_builder
|
||||
.add_source(File::with_name("config/default").required(false))
|
||||
.add_source(File::with_name("config/local").required(false));
|
||||
}
|
||||
|
||||
// Override with environment variables (e.g., SERVER__HOST, SERVER__PORT)
|
||||
config_builder = config_builder.add_source(config::Environment::with_prefix("APP").separator("__"));
|
||||
|
||||
// Build and deserialize the config
|
||||
let config = config_builder.build()?;
|
||||
config.try_deserialize()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the application configuration
|
||||
pub fn get_config() -> AppConfig {
|
||||
AppConfig::new().expect("Failed to load configuration")
|
||||
}
|
88
actix_mvc_app/src/controllers/home.rs
Normal file
88
actix_mvc_app/src/controllers/home.rs
Normal file
@ -0,0 +1,88 @@
|
||||
use actix_web::{web, HttpResponse, Responder, Result};
|
||||
use tera::Tera;
|
||||
use crate::models::User;
|
||||
|
||||
/// Controller for handling home-related routes
|
||||
pub struct HomeController;
|
||||
|
||||
impl HomeController {
|
||||
/// Handles the home page route
|
||||
pub async fn index(tmpl: web::Data<Tera>) -> 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);
|
||||
|
||||
let rendered = tmpl.render("index.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 the about page route
|
||||
pub async fn about(tmpl: web::Data<Tera>) -> Result<impl Responder> {
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.insert("active_page", "about");
|
||||
|
||||
let rendered = tmpl.render("about.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 the contact page route
|
||||
pub async fn contact(tmpl: web::Data<Tera>) -> Result<impl Responder> {
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.insert("active_page", "contact");
|
||||
|
||||
let rendered = tmpl.render("contact.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 form submissions from the contact page
|
||||
pub async fn submit_contact(
|
||||
form: web::Form<ContactForm>,
|
||||
tmpl: web::Data<Tera>
|
||||
) -> 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
|
||||
|
||||
println!("Contact form submission: {:?}", form);
|
||||
|
||||
// For this example, we'll just redirect back to the contact page
|
||||
// with a success message
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.insert("active_page", "contact");
|
||||
ctx.insert("success_message", "Your message has been sent successfully!");
|
||||
|
||||
let rendered = tmpl.render("contact.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))
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the data submitted in the contact form
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct ContactForm {
|
||||
pub name: String,
|
||||
pub email: String,
|
||||
pub subject: String,
|
||||
pub message: String,
|
||||
}
|
5
actix_mvc_app/src/controllers/mod.rs
Normal file
5
actix_mvc_app/src/controllers/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
// Export controllers
|
||||
pub mod home;
|
||||
|
||||
// Re-export controllers for easier imports
|
||||
pub use home::HomeController;
|
60
actix_mvc_app/src/main.rs
Normal file
60
actix_mvc_app/src/main.rs
Normal file
@ -0,0 +1,60 @@
|
||||
use actix_files as fs;
|
||||
use actix_web::{App, HttpServer, web};
|
||||
use actix_web::middleware::Logger;
|
||||
use tera::Tera;
|
||||
use std::io;
|
||||
|
||||
mod config;
|
||||
mod controllers;
|
||||
mod app_middleware;
|
||||
mod models;
|
||||
mod routes;
|
||||
mod utils;
|
||||
|
||||
// Import middleware components
|
||||
use app_middleware::{RequestTimer, SecurityHeaders};
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
// Initialize environment
|
||||
dotenv::dotenv().ok();
|
||||
env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));
|
||||
|
||||
// Load configuration
|
||||
let config = config::get_config();
|
||||
let bind_address = format!("{}:{}", config.server.host, config.server.port);
|
||||
|
||||
log::info!("Starting server at http://{}", bind_address);
|
||||
|
||||
// Create and configure the HTTP server
|
||||
HttpServer::new(move || {
|
||||
// Initialize Tera templates
|
||||
let mut tera = match Tera::new(&format!("{}/**/*.html", config.templates.dir)) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
log::error!("Parsing error(s): {}", e);
|
||||
::std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Register custom Tera functions
|
||||
utils::register_tera_functions(&mut tera);
|
||||
|
||||
App::new()
|
||||
// Enable logger middleware
|
||||
.wrap(Logger::default())
|
||||
// Add custom middleware
|
||||
.wrap(RequestTimer)
|
||||
.wrap(SecurityHeaders)
|
||||
// Configure static files
|
||||
.service(fs::Files::new("/static", "./src/static"))
|
||||
// Add Tera template engine
|
||||
.app_data(web::Data::new(tera))
|
||||
// Configure routes
|
||||
.configure(routes::configure_routes)
|
||||
})
|
||||
.bind(bind_address)?
|
||||
.workers(num_cpus::get())
|
||||
.run()
|
||||
.await
|
||||
}
|
130
actix_mvc_app/src/middleware/mod.rs
Normal file
130
actix_mvc_app/src/middleware/mod.rs
Normal file
@ -0,0 +1,130 @@
|
||||
use actix_web::{
|
||||
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
|
||||
Error,
|
||||
};
|
||||
use futures::future::{ready, LocalBoxFuture, Ready};
|
||||
use std::{
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
5
actix_mvc_app/src/models/mod.rs
Normal file
5
actix_mvc_app/src/models/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
// Export models
|
||||
pub mod user;
|
||||
|
||||
// Re-export models for easier imports
|
||||
pub use user::User;
|
105
actix_mvc_app/src/models/user.rs
Normal file
105
actix_mvc_app/src/models/user.rs
Normal file
@ -0,0 +1,105 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
/// Represents a user in the system
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
/// Unique identifier for the user
|
||||
pub id: Option<i32>,
|
||||
/// User's full name
|
||||
pub name: String,
|
||||
/// User's email address
|
||||
pub email: String,
|
||||
/// User's role in the system
|
||||
pub role: UserRole,
|
||||
/// When the user was created
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
/// When the user was last updated
|
||||
pub updated_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
/// Represents the possible roles a user can have
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum UserRole {
|
||||
/// Regular user with limited permissions
|
||||
User,
|
||||
/// Administrator with full permissions
|
||||
Admin,
|
||||
}
|
||||
|
||||
impl User {
|
||||
/// Creates a new user with default values
|
||||
pub fn new(name: String, email: String) -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
name,
|
||||
email,
|
||||
role: UserRole::User,
|
||||
created_at: Some(Utc::now()),
|
||||
updated_at: Some(Utc::now()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new admin user
|
||||
pub fn new_admin(name: String, email: String) -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
name,
|
||||
email,
|
||||
role: UserRole::Admin,
|
||||
created_at: Some(Utc::now()),
|
||||
updated_at: Some(Utc::now()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the user is an admin
|
||||
pub fn is_admin(&self) -> bool {
|
||||
self.role == UserRole::Admin
|
||||
}
|
||||
|
||||
/// Updates the user's information
|
||||
pub fn update(&mut self, name: Option<String>, email: Option<String>) {
|
||||
if let Some(name) = name {
|
||||
self.name = name;
|
||||
}
|
||||
|
||||
if let Some(email) = email {
|
||||
self.email = email;
|
||||
}
|
||||
|
||||
self.updated_at = Some(Utc::now());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_new_user() {
|
||||
let user = User::new("John Doe".to_string(), "john@example.com".to_string());
|
||||
assert_eq!(user.name, "John Doe");
|
||||
assert_eq!(user.email, "john@example.com");
|
||||
assert!(!user.is_admin());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_admin() {
|
||||
let admin = User::new_admin("Admin User".to_string(), "admin@example.com".to_string());
|
||||
assert_eq!(admin.name, "Admin User");
|
||||
assert_eq!(admin.email, "admin@example.com");
|
||||
assert!(admin.is_admin());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_user() {
|
||||
let mut user = User::new("John Doe".to_string(), "john@example.com".to_string());
|
||||
user.update(Some("Jane Doe".to_string()), None);
|
||||
assert_eq!(user.name, "Jane Doe");
|
||||
assert_eq!(user.email, "john@example.com");
|
||||
|
||||
user.update(None, Some("jane@example.com".to_string()));
|
||||
assert_eq!(user.name, "Jane Doe");
|
||||
assert_eq!(user.email, "jane@example.com");
|
||||
}
|
||||
}
|
16
actix_mvc_app/src/routes/mod.rs
Normal file
16
actix_mvc_app/src/routes/mod.rs
Normal file
@ -0,0 +1,16 @@
|
||||
use actix_web::web;
|
||||
use crate::controllers::home::HomeController;
|
||||
|
||||
/// Configures all application routes
|
||||
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::scope("")
|
||||
// Home routes
|
||||
.route("/", web::get().to(HomeController::index))
|
||||
.route("/about", web::get().to(HomeController::about))
|
||||
.route("/contact", web::get().to(HomeController::contact))
|
||||
.route("/contact", web::post().to(HomeController::submit_contact))
|
||||
|
||||
// Add more routes here as needed
|
||||
);
|
||||
}
|
6
actix_mvc_app/src/static/css/bootstrap.min.css
vendored
Normal file
6
actix_mvc_app/src/static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
7
actix_mvc_app/src/static/js/bootstrap.bundle.min.js
vendored
Normal file
7
actix_mvc_app/src/static/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
103
actix_mvc_app/src/utils/mod.rs
Normal file
103
actix_mvc_app/src/utils/mod.rs
Normal file
@ -0,0 +1,103 @@
|
||||
use chrono::{DateTime, TimeZone, Utc};
|
||||
use tera::{self, Function, Result, Value};
|
||||
|
||||
/// Registers custom Tera functions
|
||||
pub fn register_tera_functions(tera: &mut tera::Tera) {
|
||||
tera.register_function("now", NowFunction);
|
||||
tera.register_function("format_date", FormatDateFunction);
|
||||
}
|
||||
|
||||
/// Tera function to get the current date/time
|
||||
#[derive(Clone)]
|
||||
pub struct NowFunction;
|
||||
|
||||
impl Function for NowFunction {
|
||||
fn call(&self, args: &std::collections::HashMap<String, Value>) -> Result<Value> {
|
||||
let format = match args.get("format") {
|
||||
Some(val) => match val.as_str() {
|
||||
Some(s) => s,
|
||||
None => "%Y-%m-%d %H:%M:%S",
|
||||
},
|
||||
None => "%Y-%m-%d %H:%M:%S",
|
||||
};
|
||||
|
||||
let now = Utc::now();
|
||||
|
||||
// Special case for just getting the year
|
||||
if args.get("year").and_then(|v| v.as_bool()).unwrap_or(false) {
|
||||
return Ok(Value::String(now.format("%Y").to_string()));
|
||||
}
|
||||
|
||||
Ok(Value::String(now.format(format).to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Tera function to format a date
|
||||
#[derive(Clone)]
|
||||
pub struct FormatDateFunction;
|
||||
|
||||
impl Function for FormatDateFunction {
|
||||
fn call(&self, args: &std::collections::HashMap<String, Value>) -> Result<Value> {
|
||||
let timestamp = match args.get("timestamp") {
|
||||
Some(val) => match val.as_i64() {
|
||||
Some(ts) => ts,
|
||||
None => {
|
||||
return Err(tera::Error::msg(
|
||||
"The 'timestamp' argument must be a valid timestamp",
|
||||
))
|
||||
}
|
||||
},
|
||||
None => {
|
||||
return Err(tera::Error::msg(
|
||||
"The 'timestamp' argument is required",
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let format = match args.get("format") {
|
||||
Some(val) => match val.as_str() {
|
||||
Some(s) => s,
|
||||
None => "%Y-%m-%d %H:%M:%S",
|
||||
},
|
||||
None => "%Y-%m-%d %H:%M:%S",
|
||||
};
|
||||
|
||||
// Convert timestamp to DateTime using the non-deprecated method
|
||||
let datetime = match DateTime::from_timestamp(timestamp, 0) {
|
||||
Some(dt) => dt,
|
||||
None => {
|
||||
return Err(tera::Error::msg(
|
||||
"Failed to convert timestamp to datetime",
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Value::String(datetime.format(format).to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats a date for display
|
||||
pub fn format_date(date: &DateTime<Utc>, format: &str) -> String {
|
||||
date.format(format).to_string()
|
||||
}
|
||||
|
||||
/// Truncates a string to a maximum length and adds an ellipsis if truncated
|
||||
pub fn truncate_string(s: &str, max_length: usize) -> String {
|
||||
if s.len() <= max_length {
|
||||
s.to_string()
|
||||
} else {
|
||||
format!("{}...", &s[0..max_length])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_truncate_string() {
|
||||
assert_eq!(truncate_string("Hello", 10), "Hello");
|
||||
assert_eq!(truncate_string("Hello, world!", 5), "Hello...");
|
||||
assert_eq!(truncate_string("", 5), "");
|
||||
}
|
||||
}
|
62
actix_mvc_app/src/views/about.html
Normal file
62
actix_mvc_app/src/views/about.html
Normal file
@ -0,0 +1,62 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}About - Actix MVC App{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title">About Actix MVC App</h1>
|
||||
<p class="card-text">This is a sample application demonstrating how to build a web application using Rust with an MVC architecture.</p>
|
||||
|
||||
<h2 class="mt-4">Technology Stack</h2>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">Backend</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<strong>Actix Web</strong> - A powerful, pragmatic, and extremely fast web framework for Rust
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Tera</strong> - A template engine inspired by Jinja2 and Django templates
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Serde</strong> - A framework for serializing and deserializing Rust data structures
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">Frontend</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<strong>Bootstrap 5.3.5</strong> - A popular CSS framework for responsive web design
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>HTML5</strong> - The latest version of the HTML standard
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>CSS3</strong> - The latest evolution of the Cascading Style Sheets language
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-4">MVC Architecture</h2>
|
||||
<p>This application follows the Model-View-Controller (MVC) architectural pattern:</p>
|
||||
<ul>
|
||||
<li><strong>Model:</strong> Represents the data and business logic of the application</li>
|
||||
<li><strong>View:</strong> Represents the UI of the application (Tera templates)</li>
|
||||
<li><strong>Controller:</strong> Handles the user request, works with the Model, and selects a View to render</li>
|
||||
</ul>
|
||||
|
||||
<a href="/contact" class="btn btn-primary mt-3">Contact Us</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
54
actix_mvc_app/src/views/base.html
Normal file
54
actix_mvc_app/src/views/base.html
Normal file
@ -0,0 +1,54 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Actix MVC App{% endblock %}</title>
|
||||
<link rel="stylesheet" href="/static/css/bootstrap.min.css">
|
||||
{% block extra_css %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/">Actix MVC App</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if active_page == 'home' %}active{% endif %}" href="/">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if active_page == 'about' %}active{% endif %}" href="/about">About</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if active_page == 'contact' %}active{% endif %}" href="/contact">Contact</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container py-4">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="bg-dark text-white py-4 mt-5">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h5>Actix MVC App</h5>
|
||||
<p>A Rust web application using Actix Web, Tera templates, and Bootstrap.</p>
|
||||
</div>
|
||||
<div class="col-md-6 text-md-end">
|
||||
<p>© {{ now(year=true) }} Actix MVC App. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="/static/js/bootstrap.bundle.min.js"></script>
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
68
actix_mvc_app/src/views/contact.html
Normal file
68
actix_mvc_app/src/views/contact.html
Normal file
@ -0,0 +1,68 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Contact - Actix MVC App{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-8 offset-md-2">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title">Contact Us</h1>
|
||||
<p class="card-text">Have questions or feedback? Feel free to reach out to us using the form below.</p>
|
||||
|
||||
<form id="contactForm" class="mt-4">
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Name</label>
|
||||
<input type="text" class="form-control" id="name" name="name" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email</label>
|
||||
<input type="email" class="form-control" id="email" name="email" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="subject" class="form-label">Subject</label>
|
||||
<input type="text" class="form-control" id="subject" name="subject" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="message" class="form-label">Message</label>
|
||||
<textarea class="form-control" id="message" name="message" rows="5" required></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-5">
|
||||
<h3>Other Ways to Connect</h3>
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-6">
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Email</h5>
|
||||
<p class="card-text">info@example.com</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">GitHub</h5>
|
||||
<p class="card-text">github.com/example/actix-mvc-app</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.getElementById('contactForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
alert('This is a demo form. In a real application, this would submit data to the server.');
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% endblock %}
|
55
actix_mvc_app/src/views/index.html
Normal file
55
actix_mvc_app/src/views/index.html
Normal file
@ -0,0 +1,55 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Home - Actix MVC App{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title">Welcome to Actix MVC App</h1>
|
||||
<p class="card-text">This is a Rust web application built with:</p>
|
||||
<ul class="list-group list-group-flush mb-4">
|
||||
<li class="list-group-item">Actix Web - A powerful, pragmatic, and extremely fast web framework for Rust</li>
|
||||
<li class="list-group-item">Tera Templates - A template engine inspired by Jinja2 and Django templates</li>
|
||||
<li class="list-group-item">Bootstrap 5.3.5 - A popular CSS framework for responsive web design</li>
|
||||
</ul>
|
||||
<p>This application follows the MVC (Model-View-Controller) architectural pattern:</p>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-primary text-white">
|
||||
Models
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Data structures and business logic</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-success text-white">
|
||||
Views
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Tera templates for rendering HTML</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-info text-white">
|
||||
Controllers
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Request handlers and application logic</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/about" class="btn btn-primary">Learn More</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
Loading…
Reference in New Issue
Block a user