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:
284
docs/configuration.md
Normal file
284
docs/configuration.md
Normal 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
362
docs/gitea-auth.md
Normal 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
129
docs/installation.md
Normal 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
273
docs/mvc.md
Normal 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
240
docs/views.md
Normal 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.
|
Reference in New Issue
Block a user