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`.
This commit is contained in:
Mahmoud Emad
2025-05-07 14:03:08 +03:00
parent 84d357f0c5
commit 645a387528
26 changed files with 2646 additions and 1 deletions

284
docs/configuration.md Normal file
View File

@@ -0,0 +1,284 @@
# Configuration Guide
This guide provides detailed information on how to configure the Hostbasket application.
## Table of Contents
1. [Configuration Methods](#configuration-methods)
2. [Server Configuration](#server-configuration)
3. [Templates Configuration](#templates-configuration)
4. [Authentication Configuration](#authentication-configuration)
5. [OAuth Configuration](#oauth-configuration)
6. [Database Configuration](#database-configuration)
7. [Logging Configuration](#logging-configuration)
8. [Environment-Specific Configuration](#environment-specific-configuration)
9. [Advanced Configuration](#advanced-configuration)
## Configuration Methods
Hostbasket supports multiple configuration methods:
1. **Environment Variables**: Set configuration values using environment variables.
2. **Configuration Files**: Use TOML configuration files in the `config` directory.
3. **Command-Line Arguments**: Override specific settings using command-line arguments.
The configuration is loaded in the following order, with later methods overriding earlier ones:
1. Default values
2. Configuration files
3. Environment variables
4. Command-line arguments
## Server Configuration
### Environment Variables
```bash
export APP__SERVER__HOST=127.0.0.1
export APP__SERVER__PORT=9999
export APP__SERVER__WORKERS=4
```
### Configuration File
```toml
[server]
host = "127.0.0.1"
port = 9999
workers = 4
```
### Available Settings
| Setting | Description | Default Value |
|---------|-------------|---------------|
| `host` | The host address to bind to | `127.0.0.1` |
| `port` | The port to listen on | `9999` |
| `workers` | The number of worker threads | Number of CPU cores |
## Templates Configuration
### Environment Variables
```bash
export APP__TEMPLATES__DIR=./src/views
```
### Configuration File
```toml
[templates]
dir = "./src/views"
```
### Available Settings
| Setting | Description | Default Value |
|---------|-------------|---------------|
| `dir` | The directory containing templates | `./src/views` |
## Authentication Configuration
### Environment Variables
```bash
export JWT_SECRET=your_jwt_secret_key
export JWT_EXPIRATION_HOURS=24
```
### Configuration File
```toml
[auth]
jwt_secret = "your_jwt_secret_key"
jwt_expiration_hours = 24
```
### Available Settings
| Setting | Description | Default Value |
|---------|-------------|---------------|
| `jwt_secret` | The secret key used to sign JWT tokens | `your_jwt_secret_key` |
| `jwt_expiration_hours` | The number of hours before a JWT token expires | `24` |
## OAuth Configuration
### Environment Variables
```bash
export GITEA_CLIENT_ID=your_client_id
export GITEA_CLIENT_SECRET=your_client_secret
export GITEA_INSTANCE_URL=https://your-gitea-instance.com
```
### Configuration File
```toml
[oauth.gitea]
client_id = "your_client_id"
client_secret = "your_client_secret"
instance_url = "https://your-gitea-instance.com"
```
### Available Settings
| Setting | Description | Default Value |
|---------|-------------|---------------|
| `client_id` | The OAuth client ID | None |
| `client_secret` | The OAuth client secret | None |
| `instance_url` | The URL of your Gitea instance | None |
## Database Configuration
### Environment Variables
```bash
export APP__DATABASE__URL=postgres://user:password@localhost/hostbasket
export APP__DATABASE__POOL_SIZE=5
```
### Configuration File
```toml
[database]
url = "postgres://user:password@localhost/hostbasket"
pool_size = 5
```
### Available Settings
| Setting | Description | Default Value |
|---------|-------------|---------------|
| `url` | The database connection URL | None |
| `pool_size` | The size of the connection pool | `5` |
## Logging Configuration
### Environment Variables
```bash
export RUST_LOG=info
```
### Configuration File
```toml
[logging]
level = "info"
```
### Available Settings
| Setting | Description | Default Value |
|---------|-------------|---------------|
| `level` | The log level (trace, debug, info, warn, error) | `info` |
## Environment-Specific Configuration
You can create environment-specific configuration files:
- `config/default.toml`: Default configuration for all environments
- `config/development.toml`: Configuration for development environment
- `config/production.toml`: Configuration for production environment
- `config/test.toml`: Configuration for test environment
To specify the environment:
```bash
export APP_ENV=production
```
## Advanced Configuration
### Custom Configuration
You can add custom configuration sections to the configuration file:
```toml
[custom]
setting1 = "value1"
setting2 = "value2"
```
To access these settings in your code:
```rust
let config = config::get_config();
let custom_config = config.get_table("custom").unwrap();
let setting1 = custom_config.get("setting1").unwrap().as_str().unwrap();
```
### Configuration Validation
You can validate the configuration when loading it:
```rust
impl AppConfig {
pub fn validate(&self) -> Result<(), String> {
if self.server.port < 1024 && !cfg!(debug_assertions) {
return Err("Port number should be >= 1024 in production".to_string());
}
Ok(())
}
}
pub fn get_config() -> AppConfig {
let config = AppConfig::new().expect("Failed to load configuration");
if let Err(e) = config.validate() {
panic!("Invalid configuration: {}", e);
}
config
}
```
### Reloading Configuration
You can implement configuration reloading to update the configuration without restarting the application:
```rust
pub async fn reload_config() -> Result<AppConfig, ConfigError> {
let config = AppConfig::new()?;
// Update the global configuration
let mut global_config = GLOBAL_CONFIG.write().await;
*global_config = config.clone();
Ok(config)
}
```
### Secrets Management
For production environments, consider using a secrets management solution like HashiCorp Vault or AWS Secrets Manager instead of storing secrets in configuration files or environment variables.
You can implement a custom secrets provider:
```rust
pub async fn load_secrets() -> Result<(), Error> {
// Load secrets from your secrets manager
let jwt_secret = secrets_manager.get_secret("jwt_secret").await?;
// Set the secrets as environment variables
std::env::set_var("JWT_SECRET", jwt_secret);
Ok(())
}
```
Call this function before loading the configuration:
```rust
#[actix_web::main]
async fn main() -> io::Result<()> {
// Load secrets
load_secrets().await.expect("Failed to load secrets");
// Load configuration
let config = config::get_config();
// ...
}
```

362
docs/gitea-auth.md Normal file
View File

@@ -0,0 +1,362 @@
# Gitea Authentication Guide
This guide provides detailed instructions on how to integrate Gitea authentication into your Hostbasket application.
## Table of Contents
1. [Introduction](#introduction)
2. [Prerequisites](#prerequisites)
3. [Setting Up Gitea OAuth Application](#setting-up-gitea-oauth-application)
4. [Configuring Hostbasket for Gitea Authentication](#configuring-hostbasket-for-gitea-authentication)
5. [Implementing the OAuth Flow](#implementing-the-oauth-flow)
6. [Testing the Integration](#testing-the-integration)
7. [Troubleshooting](#troubleshooting)
## Introduction
Gitea is a self-hosted Git service that provides a GitHub-like interface. By integrating Gitea authentication into your Hostbasket application, you can allow users to log in using their Gitea accounts, simplifying the authentication process and providing a seamless experience.
## Prerequisites
Before you begin, ensure you have:
- A running Gitea instance
- Administrative access to your Gitea instance
- A running Hostbasket application
## Setting Up Gitea OAuth Application
1. Log in to your Gitea instance as an administrator
2. Navigate to **Site Administration** > **Applications**
3. Click **Create a New OAuth2 Application**
4. Fill in the application details:
- **Application Name**: Hostbasket
- **Redirect URI**: `http://localhost:9999/auth/gitea/callback` (adjust the URL to match your Hostbasket instance)
- **Confidential Client**: Check this box
5. Click **Create Application**
6. Note the **Client ID** and **Client Secret** that are generated
## Configuring Hostbasket for Gitea Authentication
1. Add the following environment variables to your Hostbasket configuration:
```bash
export GITEA_CLIENT_ID=your_client_id
export GITEA_CLIENT_SECRET=your_client_secret
export GITEA_INSTANCE_URL=https://your-gitea-instance.com
```
2. Alternatively, add these settings to your configuration file (`config/default.toml`):
```toml
[oauth.gitea]
client_id = "your_client_id"
client_secret = "your_client_secret"
instance_url = "https://your-gitea-instance.com"
```
## Implementing the OAuth Flow
To implement the OAuth flow, you need to add new routes and controllers to your Hostbasket application.
### 1. Add Required Dependencies
Add the following dependencies to your `Cargo.toml`:
```toml
[dependencies]
oauth2 = "4.3"
reqwest = { version = "0.11", features = ["json"] }
```
### 2. Create a Gitea OAuth Configuration Module
Create a new file `src/config/oauth.rs`:
```rust
use oauth2::{
AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl,
basic::BasicClient, AuthorizationCode, CsrfToken, Scope, TokenResponse,
};
use serde::{Deserialize, Serialize};
use std::env;
#[derive(Clone, Debug)]
pub struct GiteaOAuthConfig {
pub client: BasicClient,
pub instance_url: String,
}
impl GiteaOAuthConfig {
pub fn new() -> Self {
// Get configuration from environment variables
let client_id = env::var("GITEA_CLIENT_ID")
.expect("Missing GITEA_CLIENT_ID environment variable");
let client_secret = env::var("GITEA_CLIENT_SECRET")
.expect("Missing GITEA_CLIENT_SECRET environment variable");
let instance_url = env::var("GITEA_INSTANCE_URL")
.expect("Missing GITEA_INSTANCE_URL environment variable");
// Create OAuth client
let auth_url = format!("{}/login/oauth/authorize", instance_url);
let token_url = format!("{}/login/oauth/access_token", instance_url);
let client = BasicClient::new(
ClientId::new(client_id),
Some(ClientSecret::new(client_secret)),
AuthUrl::new(auth_url).unwrap(),
Some(TokenUrl::new(token_url).unwrap()),
)
.set_redirect_uri(
RedirectUrl::new("http://localhost:9999/auth/gitea/callback".to_string()).unwrap(),
);
Self {
client,
instance_url,
}
}
}
// Gitea user information structure
#[derive(Debug, Deserialize, Serialize)]
pub struct GiteaUser {
pub id: i64,
pub login: String,
pub full_name: String,
pub email: String,
pub avatar_url: String,
}
```
### 3. Create a Gitea Authentication Controller
Create a new file `src/controllers/gitea_auth.rs`:
```rust
use actix_web::{web, HttpResponse, Responder, Result, http::header, cookie::Cookie};
use actix_session::Session;
use oauth2::{AuthorizationCode, CsrfToken, Scope, TokenResponse};
use reqwest::Client;
use crate::config::oauth::GiteaOAuthConfig;
use crate::models::user::{User, UserRole};
use crate::controllers::auth::AuthController;
use serde_json::json;
pub struct GiteaAuthController;
impl GiteaAuthController {
// Initiate the OAuth flow
pub async fn login(
oauth_config: web::Data<GiteaOAuthConfig>,
session: Session,
) -> Result<impl Responder> {
// Generate the authorization URL
let (auth_url, csrf_token) = oauth_config
.client
.authorize_url(CsrfToken::new_random)
.add_scope(Scope::new("read:user".to_string()))
.add_scope(Scope::new("user:email".to_string()))
.url();
// Store the CSRF token in the session
session.insert("oauth_csrf_token", csrf_token.secret())?;
// Redirect to the authorization URL
Ok(HttpResponse::Found()
.append_header((header::LOCATION, auth_url.to_string()))
.finish())
}
// Handle the OAuth callback
pub async fn callback(
oauth_config: web::Data<GiteaOAuthConfig>,
session: Session,
query: web::Query<CallbackQuery>,
) -> Result<impl Responder> {
// Verify the CSRF token
let csrf_token = session.get::<String>("oauth_csrf_token")?
.ok_or_else(|| actix_web::error::ErrorBadRequest("Missing CSRF token"))?;
if csrf_token != query.state {
return Err(actix_web::error::ErrorBadRequest("Invalid CSRF token"));
}
// Exchange the authorization code for an access token
let token = oauth_config
.client
.exchange_code(AuthorizationCode::new(query.code.clone()))
.request_async(oauth2::reqwest::async_http_client)
.await
.map_err(|e| actix_web::error::ErrorInternalServerError(format!("Token exchange error: {}", e)))?;
// Get the user information from Gitea
let client = Client::new();
let user_info_url = format!("{}/api/v1/user", oauth_config.instance_url);
let gitea_user = client
.get(&user_info_url)
.bearer_auth(token.access_token().secret())
.send()
.await
.map_err(|e| actix_web::error::ErrorInternalServerError(format!("API request error: {}", e)))?
.json::<crate::config::oauth::GiteaUser>()
.await
.map_err(|e| actix_web::error::ErrorInternalServerError(format!("JSON parsing error: {}", e)))?;
// Create or update the user in your system
let mut user = User::new(
gitea_user.full_name.clone(),
gitea_user.email.clone(),
);
// Set the user ID and role
user.id = Some(gitea_user.id as i32);
user.role = UserRole::User;
// Generate JWT token
let token = AuthController::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())
}
}
// Query parameters for the OAuth callback
#[derive(serde::Deserialize)]
pub struct CallbackQuery {
pub code: String,
pub state: String,
}
```
### 4. Update the Routes Configuration
Update your `src/routes/mod.rs` file to include the new Gitea authentication routes:
```rust
use crate::controllers::gitea_auth::GiteaAuthController;
use crate::config::oauth::GiteaOAuthConfig;
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
// Create the OAuth configuration
let oauth_config = web::Data::new(GiteaOAuthConfig::new());
// Configure session middleware with the consistent key
let session_middleware = SessionMiddleware::builder(
CookieSessionStore::default(),
SESSION_KEY.clone()
)
.cookie_secure(false) // Set to true in production with HTTPS
.build();
// Public routes that don't require authentication
cfg.service(
web::scope("")
.wrap(session_middleware)
.app_data(oauth_config.clone())
// Existing routes...
// Gitea OAuth routes
.route("/auth/gitea", web::get().to(GiteaAuthController::login))
.route("/auth/gitea/callback", web::get().to(GiteaAuthController::callback))
);
}
```
### 5. Update the Login Page
Update your login page template (`src/views/auth/login.html`) to include a "Login with Gitea" button:
```html
{% extends "base.html" %}
{% block title %}Login{% 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>Login</h2>
</div>
<div class="card-body">
<form method="post" action="/login">
<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>
<button type="submit" class="btn btn-primary">Login</button>
</form>
<hr>
<div class="text-center">
<p>Or login with:</p>
<a href="/auth/gitea" class="btn btn-secondary">
<img src="/static/images/gitea-logo.svg" alt="Gitea" width="20" height="20">
Login with Gitea
</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
```
## Testing the Integration
1. Start your Hostbasket application
2. Navigate to the login page
3. Click the "Login with Gitea" button
4. You should be redirected to your Gitea instance's authorization page
5. Authorize the application
6. You should be redirected back to your Hostbasket application and logged in
## Troubleshooting
### Common Issues
1. **Redirect URI Mismatch**:
- Error: `The redirect URI provided is missing or does not match`
- Solution: Ensure that the redirect URI in your Gitea OAuth application settings matches the one in your code.
2. **Invalid Client ID or Secret**:
- Error: `Invalid client_id or client_secret`
- Solution: Double-check your client ID and secret.
3. **CSRF Token Mismatch**:
- Error: `Invalid CSRF token`
- Solution: Ensure that your session is properly configured and that the CSRF token is being stored and retrieved correctly.
4. **API Request Errors**:
- Error: `API request error`
- Solution: Check your Gitea instance URL and ensure that the API is accessible.
5. **JSON Parsing Errors**:
- Error: `JSON parsing error`
- Solution: Ensure that the API response matches the expected structure.

129
docs/installation.md Normal file
View File

@@ -0,0 +1,129 @@
# Installation Guide
This guide provides detailed instructions for installing and setting up the Hostbasket application.
## Prerequisites
Before you begin, ensure you have the following installed:
- **Rust and Cargo**: The latest stable version is recommended. You can install Rust using [rustup](https://rustup.rs/).
- **Git**: For cloning the repository.
- **A text editor or IDE**: Such as Visual Studio Code, IntelliJ IDEA with Rust plugin, or any other editor of your choice.
## Step 1: Clone the Repository
```bash
git clone https://git.ourworld.tf/herocode/rweb_starterkit
cd rweb_starterkit
```
## Step 2: Configure the Application
The application can be configured using environment variables or configuration files.
### Using Environment Variables
You can set the following environment variables:
```bash
export APP__SERVER__HOST=127.0.0.1
export APP__SERVER__PORT=9999
export APP__SERVER__WORKERS=4
export APP__TEMPLATES__DIR=./src/views
export JWT_SECRET=your_jwt_secret_key
```
### Using Configuration Files
Alternatively, you can create a configuration file in the `config` directory:
1. Create a file named `config/default.toml` with the following content:
```toml
[server]
host = "127.0.0.1"
port = 9999
workers = 4
[templates]
dir = "./src/views"
```
2. You can also create environment-specific configuration files like `config/development.toml` or `config/production.toml`.
## Step 3: Build the Project
```bash
cargo build
```
This will download and compile all dependencies and build the project.
## Step 4: Run the Application
### Development Mode
```bash
cargo run
```
### Production Mode
```bash
cargo build --release
./target/release/hostbasket
```
You can also specify a custom port:
```bash
cargo run -- --port 8080
```
## Step 5: Verify the Installation
Open your web browser and navigate to `http://localhost:9999` (or the port you specified). You should see the Hostbasket home page.
## Troubleshooting
### Common Issues
1. **Port already in use**:
```
Error: Address already in use (os error 98)
```
Solution: Change the port using the `--port` flag or the `APP__SERVER__PORT` environment variable.
2. **Missing dependencies**:
```
error: failed to run custom build command for `openssl-sys v0.9.58`
```
Solution: Install OpenSSL development libraries:
```bash
# Ubuntu/Debian
sudo apt-get install libssl-dev pkg-config
# Fedora/CentOS
sudo dnf install openssl-devel pkgconfig
# macOS
brew install openssl pkg-config
```
3. **Template parsing errors**:
```
Parsing error(s): Failed to parse template...
```
Solution: Check your template files for syntax errors.
## Next Steps
After successfully installing the application, you can:
1. Explore the [Usage Guide](usage.md) to learn how to use the application.
2. Check the [Views Guide](views.md) to learn how to create and customize views.
3. Set up [Gitea Authentication](gitea-auth.md) to enable login with Gitea.
## Advanced Configuration
For advanced configuration options, please refer to the [Configuration Guide](configuration.md).

273
docs/mvc.md Normal file
View File

@@ -0,0 +1,273 @@
# 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.

240
docs/views.md Normal file
View File

@@ -0,0 +1,240 @@
# Views Guide
This guide provides detailed instructions on how to create and customize views in the Hostbasket application.
## Table of Contents
1. [Introduction to Tera Templates](#introduction-to-tera-templates)
2. [Template Structure](#template-structure)
3. [Creating a New View](#creating-a-new-view)
4. [Template Inheritance](#template-inheritance)
5. [Using Variables](#using-variables)
6. [Control Structures](#control-structures)
7. [Filters](#filters)
8. [Macros](#macros)
9. [Custom Functions](#custom-functions)
10. [Best Practices](#best-practices)
## Introduction to Tera Templates
Hostbasket uses [Tera](https://tera.netlify.app/) as its template engine. Tera is inspired by Jinja2 and Django templates and provides a powerful way to create dynamic HTML pages.
## Template Structure
Templates are stored in the `src/views` directory. The directory structure typically follows the application's features:
```
src/views/
├── auth/
│ ├── login.html
│ └── register.html
├── home/
│ ├── index.html
│ ├── about.html
│ └── contact.html
├── tickets/
│ ├── index.html
│ ├── new.html
│ └── show.html
├── assets/
│ ├── index.html
│ ├── create.html
│ └── detail.html
└── base.html
```
## Creating a New View
To create a new view:
1. Create a new HTML file in the appropriate directory under `src/views`
2. Use template inheritance to extend the base template
3. Add your content within the appropriate blocks
Example:
```html
{% extends "base.html" %}
{% block title %}My New Page{% endblock %}
{% block content %}
<div class="container">
<h1>Welcome to My New Page</h1>
<p>This is a custom page.</p>
</div>
{% endblock %}
```
## Template Inheritance
Tera supports template inheritance, which allows you to define a base template with common elements and extend it in child templates.
### Base Template
The base template (`base.html`) typically contains the HTML structure, header, footer, and navigation menu:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Hostbasket{% endblock %}</title>
<link rel="stylesheet" href="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/styles.css">
{% block head %}{% endblock %}
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<!-- Navigation content -->
</nav>
<main>
{% block content %}{% endblock %}
</main>
<footer class="footer mt-auto py-3 bg-light">
<!-- Footer content -->
</footer>
<script src="/static/js/bootstrap.bundle.min.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>
```
### Child Template
Child templates extend the base template and override specific blocks:
```html
{% extends "base.html" %}
{% block title %}Home{% endblock %}
{% block content %}
<div class="container">
<h1>Welcome to Hostbasket</h1>
<p>This is the home page.</p>
</div>
{% endblock %}
{% block scripts %}
<script src="/static/js/home.js"></script>
{% endblock %}
```
## Using Variables
You can pass variables from your controller to the template and use them in your HTML:
### In the Controller
```rust
pub async fn index(tmpl: web::Data<Tera>) -> Result<impl Responder> {
let mut ctx = tera::Context::new();
ctx.insert("title", "Welcome to Hostbasket");
ctx.insert("user_name", "John Doe");
render_template(&tmpl, "home/index.html", &ctx)
}
```
### In the Template
```html
<h1>{{ title }}</h1>
<p>Hello, {{ user_name }}!</p>
```
## Control Structures
Tera provides various control structures for conditional rendering and iteration.
### Conditionals
```html
{% if user %}
<p>Welcome, {{ user.name }}!</p>
{% else %}
<p>Please log in.</p>
{% endif %}
```
### Loops
```html
<ul>
{% for item in items %}
<li>{{ item.name }}</li>
{% endfor %}
</ul>
{% if items is empty %}
<p>No items found.</p>
{% endif %}
```
## Filters
Filters transform the values of variables:
```html
<p>{{ user.name | upper }}</p>
<p>{{ user.bio | truncate(length=100) }}</p>
<p>{{ user.created_at | date(format="%Y-%m-%d") }}</p>
```
## Macros
Macros are reusable template fragments:
```html
{% macro input(name, value='', type='text', label='') %}
<div class="mb-3">
{% if label %}
<label for="{{ name }}" class="form-label">{{ label }}</label>
{% endif %}
<input type="{{ type }}" name="{{ name }}" id="{{ name }}" value="{{ value }}" class="form-control">
</div>
{% endmacro %}
<!-- Usage -->
{{ input(name="email", type="email", label="Email Address") }}
```
## Custom Functions
You can register custom functions in Rust and use them in your templates:
### In Rust
```rust
fn register_tera_functions(tera: &mut Tera) {
tera.register_function("format_date", format_date);
}
fn format_date(args: &HashMap<String, Value>) -> Result<Value, tera::Error> {
// Implementation
}
```
### In the Template
```html
<p>{{ format_date(date=user.created_at, format="%B %d, %Y") }}</p>
```
## Best Practices
1. **Use Template Inheritance**: Extend the base template to maintain consistency across pages.
2. **Organize Templates by Feature**: Group related templates in subdirectories.
3. **Keep Templates Simple**: Move complex logic to the controller or custom functions.
4. **Use Meaningful Variable Names**: Choose descriptive names for variables and blocks.
5. **Comment Your Templates**: Add comments to explain complex sections.
6. **Validate User Input**: Always validate and sanitize user input in the controller before passing it to the template.
7. **Use Partials for Reusable Components**: Extract common components into separate files and include them where needed.
8. **Optimize for Performance**: Minimize the use of expensive operations in templates.
9. **Test Your Templates**: Ensure that your templates render correctly with different data.
10. **Follow Accessibility Guidelines**: Make your templates accessible to all users.