- 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`.
274 lines
8.8 KiB
Markdown
274 lines
8.8 KiB
Markdown
# 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](#introduction-to-mvc)
|
|
2. [MVC Components in Hostbasket](#mvc-components-in-hostbasket)
|
|
3. [Data Flow in MVC](#data-flow-in-mvc)
|
|
4. [Benefits of MVC](#benefits-of-mvc)
|
|
5. [Best Practices](#best-practices)
|
|
6. [Examples](#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.
|
|
|
|
```rust
|
|
// 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.
|
|
|
|
```html
|
|
<!-- 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.
|
|
|
|
```rust
|
|
// 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)
|
|
|
|
```rust
|
|
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)
|
|
|
|
```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)
|
|
|
|
```rust
|
|
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.
|