....
This commit is contained in:
		
							
								
								
									
										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 %}
 | 
			
		||||
		Reference in New Issue
	
	Block a user