rweb_starterkit/docs/mvc.md
Mahmoud Emad 645a387528 feat: Add basic project structure and configuration
- Add `.env.template` file for environment variable configuration.
- Add `.gitignore` file to ignore generated files and IDE artifacts.
- Add `Cargo.toml` file specifying project dependencies.
- Add basic project documentation in `README.md` and configuration
  guide in `docs/configuration.md`.
- Add Gitea authentication guide in `docs/gitea-auth.md`.
- Add installation guide in `docs/installation.md`.
- Add MVC architecture guide in `docs/mvc.md`.
- Add views guide in `docs/views.md`.
2025-05-07 14:03:08 +03:00

8.8 KiB

MVC Architecture in Hostbasket

This document explains the Model-View-Controller (MVC) architecture used in the Hostbasket application.

Table of Contents

  1. Introduction to MVC
  2. MVC Components in Hostbasket
  3. Data Flow in MVC
  4. Benefits of MVC
  5. Best Practices
  6. Examples

Introduction to MVC

Model-View-Controller (MVC) is a software architectural pattern that separates an application into three main logical components:

  • Model: Represents the data and business logic of the application
  • View: Represents the user interface
  • Controller: Acts as an intermediary between Model and View

This separation helps in organizing code, making it more maintainable, testable, and scalable.

MVC Components in Hostbasket

Model

In Hostbasket, models are defined in the src/models directory. They represent the data structures and business logic of the application.

// src/models/user.rs
pub struct User {
    pub id: Option<i32>,
    pub name: String,
    pub email: String,
    pub password_hash: Option<String>,
    pub role: UserRole,
    pub created_at: Option<DateTime<Utc>>,
    pub updated_at: Option<DateTime<Utc>>,
}

Models are responsible for:

  • Defining data structures
  • Implementing business logic
  • Validating data
  • Interacting with the database (in a full implementation)

View

Views in Hostbasket are implemented using Tera templates, located in the src/views directory. They are responsible for rendering the user interface.

<!-- src/views/home/index.html -->
{% extends "base.html" %}

{% block title %}Home - Hostbasket{% endblock %}

{% block content %}
<div class="container">
    <h1>Welcome to Hostbasket!</h1>
    <p>A web application framework built with Actix Web and Rust.</p>
</div>
{% endblock %}

Views are responsible for:

  • Presenting data to the user
  • Capturing user input
  • Providing a user interface
  • Implementing client-side logic (with JavaScript)

Controller

Controllers in Hostbasket are defined in the src/controllers directory. They handle HTTP requests, process user input, interact with models, and render views.

// src/controllers/home.rs
pub struct HomeController;

impl HomeController {
    pub async fn index(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
        let mut ctx = tera::Context::new();
        ctx.insert("active_page", "home");
        
        // Add user to context if available
        if let Ok(Some(user)) = session.get::<String>("user") {
            ctx.insert("user_json", &user);
        }
        
        render_template(&tmpl, "home/index.html", &ctx)
    }
}

Controllers are responsible for:

  • Handling HTTP requests
  • Processing user input
  • Interacting with models
  • Preparing data for views
  • Rendering views

Data Flow in MVC

The typical data flow in an MVC application like Hostbasket is as follows:

  1. The user interacts with the view (e.g., submits a form)
  2. The controller receives the request
  3. The controller processes the request, often interacting with models
  4. The controller prepares data for the view
  5. The controller renders the view with the prepared data
  6. The view is displayed to the user

This flow ensures a clear separation of concerns and makes the application easier to maintain and extend.

Benefits of MVC

Using the MVC architecture in Hostbasket provides several benefits:

  1. Separation of Concerns: Each component has a specific responsibility, making the code more organized and maintainable.
  2. Code Reusability: Models and controllers can be reused across different views.
  3. Parallel Development: Different team members can work on models, views, and controllers simultaneously.
  4. Testability: Components can be tested in isolation, making unit testing easier.
  5. Flexibility: Changes to one component have minimal impact on others.
  6. Scalability: The application can grow without becoming unwieldy.

Best Practices

When working with the MVC architecture in Hostbasket, follow these best practices:

Models

  • Keep models focused on data and business logic
  • Implement validation in models
  • Use traits to define common behavior
  • Keep database interactions separate from business logic

Views

  • Keep views simple and focused on presentation
  • Use template inheritance to avoid duplication
  • Minimize logic in templates
  • Use partials for reusable components

Controllers

  • Keep controllers thin
  • Focus on request handling and coordination
  • Delegate business logic to models
  • Use dependency injection for services

Examples

Complete MVC Example

Here's a complete example of how the MVC pattern works in Hostbasket for a user registration flow:

Model (src/models/user.rs)

impl User {
    pub fn new_with_password(name: String, email: String, password: &str) -> Result<Self, bcrypt::BcryptError> {
        let password_hash = hash(password, DEFAULT_COST)?;
        
        Ok(Self {
            id: None,
            name,
            email,
            password_hash: Some(password_hash),
            role: UserRole::User,
            created_at: Some(Utc::now()),
            updated_at: Some(Utc::now()),
        })
    }
    
    pub fn verify_password(&self, password: &str) -> Result<bool, bcrypt::BcryptError> {
        match &self.password_hash {
            Some(hash) => verify(password, hash),
            None => Ok(false),
        }
    }
}

View (src/views/auth/register.html)

{% extends "base.html" %}

{% block title %}Register - Hostbasket{% endblock %}

{% block content %}
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-6">
            <div class="card mt-5">
                <div class="card-header">
                    <h2>Register</h2>
                </div>
                <div class="card-body">
                    <form method="post" action="/register">
                        <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="password" class="form-label">Password</label>
                            <input type="password" class="form-control" id="password" name="password" required>
                        </div>
                        <div class="mb-3">
                            <label for="password_confirmation" class="form-label">Confirm Password</label>
                            <input type="password" class="form-control" id="password_confirmation" name="password_confirmation" required>
                        </div>
                        <button type="submit" class="btn btn-primary">Register</button>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
{% endblock %}

Controller (src/controllers/auth.rs)

pub async fn register(
    form: web::Form<RegistrationData>,
    session: Session,
    _tmpl: web::Data<Tera>
) -> Result<impl Responder> {
    // Create a new user with the form data
    let user = match User::new_with_password(
        form.name.clone(),
        form.email.clone(),
        &form.password
    ) {
        Ok(user) => user,
        Err(_) => return Err(actix_web::error::ErrorInternalServerError("Failed to create user")),
    };
    
    // In a real application, you would save the user to a database here
    
    // Generate JWT token
    let token = Self::generate_token(&user.email, &user.role)
        .map_err(|_| actix_web::error::ErrorInternalServerError("Failed to generate token"))?;
    
    // Store user data in session
    let user_json = serde_json::to_string(&user).unwrap();
    session.insert("user", &user_json)?;
    session.insert("auth_token", &token)?;
    
    // Create a cookie with the JWT token
    let cookie = Cookie::build("auth_token", token)
        .path("/")
        .http_only(true)
        .secure(false) // Set to true in production with HTTPS
        .max_age(actix_web::cookie::time::Duration::hours(24))
        .finish();
    
    // Redirect to the home page with JWT token in cookie
    Ok(HttpResponse::Found()
        .cookie(cookie)
        .append_header((header::LOCATION, "/"))
        .finish())
}

This example demonstrates how the MVC pattern separates concerns while allowing the components to work together to handle user registration.