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:
		
							
								
								
									
										29
									
								
								.env.template
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								.env.template
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| # Server Configuration | ||||
| APP__SERVER__HOST=127.0.0.1 | ||||
| APP__SERVER__PORT=9999 | ||||
| APP__SERVER__WORKERS=4 | ||||
|  | ||||
| # Templates Configuration | ||||
| APP__TEMPLATES__DIR=./src/views | ||||
|  | ||||
| # Authentication | ||||
| JWT_SECRET=your_jwt_secret_key | ||||
| JWT_EXPIRATION_HOURS=24 | ||||
| SECRET_KEY=your_secret_key_for_session_cookies_at_least_32_bytes_long | ||||
|  | ||||
| # OAuth Configuration - Gitea | ||||
| GITEA_CLIENT_ID=your_client_id | ||||
| GITEA_CLIENT_SECRET=your_client_secret | ||||
| GITEA_INSTANCE_URL=https://your-gitea-instance.com | ||||
| APP_URL=http://localhost:9999 | ||||
|  | ||||
| # Database Configuration | ||||
| APP__DATABASE__URL=postgres://user:password@localhost/hostbasket | ||||
| APP__DATABASE__POOL_SIZE=5 | ||||
|  | ||||
| # Logging | ||||
| RUST_LOG=info | ||||
|  | ||||
| # Application Environment | ||||
| APP_ENV=development | ||||
| APP_CONFIG=config/local.toml | ||||
							
								
								
									
										28
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| # Generated by Cargo | ||||
| target/ | ||||
|  | ||||
| # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries | ||||
| # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html | ||||
| Cargo.lock | ||||
|  | ||||
| # These are backup files generated by rustfmt | ||||
| **/*.rs.bk | ||||
|  | ||||
| # Environment variables | ||||
| .env | ||||
|  | ||||
| # IDE files | ||||
| .idea/ | ||||
| .vscode/ | ||||
| *.iml | ||||
|  | ||||
| # macOS files | ||||
| .DS_Store | ||||
|  | ||||
| # Windows files | ||||
| Thumbs.db | ||||
| ehthumbs.db | ||||
| Desktop.ini | ||||
|  | ||||
| # Log files | ||||
| *.log | ||||
							
								
								
									
										31
									
								
								Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| [package] | ||||
| name = "hostbasket" | ||||
| version = "0.1.0" | ||||
| edition = "2021" | ||||
| authors = ["Your Name <your.email@example.com>"] | ||||
| description = "A web application framework built with Actix Web and Rust" | ||||
|  | ||||
| [dependencies] | ||||
| actix-web = "4.3" | ||||
| actix-files = "0.6" | ||||
| actix-session = { version = "0.7", features = ["cookie-session"] } | ||||
| tera = "1.18" | ||||
| serde = { version = "1.0", features = ["derive"] } | ||||
| serde_json = "1.0" | ||||
| config = "0.13" | ||||
| log = "0.4" | ||||
| env_logger = "0.10" | ||||
| dotenv = "0.15" | ||||
| chrono = { version = "0.4", features = ["serde"] } | ||||
| jsonwebtoken = "8.3" | ||||
| lazy_static = "1.4" | ||||
| futures-util = "0.3" | ||||
| num_cpus = "1.15" | ||||
| bcrypt = "0.14" | ||||
| uuid = { version = "1.3", features = ["v4", "serde"] } | ||||
| oauth2 = "4.3" | ||||
| reqwest = { version = "0.11", features = ["json"] } | ||||
|  | ||||
| [dev-dependencies] | ||||
| actix-rt = "2.8" | ||||
| tokio = { version = "1.28", features = ["full"] } | ||||
							
								
								
									
										133
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										133
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,2 +1,133 @@ | ||||
| # webframework_template | ||||
| # Hostbasket Starterkit | ||||
|  | ||||
| Welcome to the Hostbasket Starterkit! This guide will help you get started with the Hostbasket project, a comprehensive platform built with Actix Web and Rust. | ||||
|  | ||||
| ## Table of Contents | ||||
|  | ||||
| 1. [Introduction](#introduction) | ||||
| 2. [Installation](#installation) | ||||
| 3. [Usage](#usage) | ||||
| 4. [Creating Views](#creating-views) | ||||
| 5. [Authentication with Gitea](#authentication-with-gitea) | ||||
| 6. [Documentation](#documentation) | ||||
|  | ||||
| ## Introduction | ||||
|  | ||||
| Hostbasket is a web application framework built with Actix Web, a powerful, pragmatic, and extremely fast web framework for Rust. It follows the MVC (Model-View-Controller) architecture and uses Tera templates for rendering views. | ||||
|  | ||||
| ### Features | ||||
|  | ||||
| - **Actix Web**: A powerful, pragmatic, and extremely fast web framework for Rust | ||||
| - **Tera Templates**: A template engine inspired by Jinja2 and Django templates | ||||
| - **Bootstrap 5.3.5**: A popular CSS framework for responsive web design | ||||
| - **MVC Architecture**: Clean separation of concerns with Models, Views, and Controllers | ||||
| - **Middleware Support**: Custom middleware for request timing and security headers | ||||
| - **Configuration Management**: Flexible configuration system with environment variable support | ||||
| - **Static File Serving**: Serve CSS, JavaScript, and other static assets | ||||
|  | ||||
| ## Installation | ||||
|  | ||||
| ### Prerequisites | ||||
|  | ||||
| - Rust and Cargo (latest stable version) | ||||
| - Git | ||||
|  | ||||
| ### Setup | ||||
|  | ||||
| 1. Clone the repository: | ||||
|    ```bash | ||||
|    git clone https://github.com/yourusername/hostbasket.git | ||||
|    cd hostbasket | ||||
|    ``` | ||||
|  | ||||
| 2. Build the project: | ||||
|    ```bash | ||||
|    cargo build | ||||
|    ``` | ||||
|  | ||||
| 3. Run the application: | ||||
|    ```bash | ||||
|    cargo run | ||||
|    ``` | ||||
|  | ||||
| 4. Open your browser and navigate to `http://localhost:9999` | ||||
|  | ||||
| For more detailed installation instructions, see [Installation Guide](docs/installation.md). | ||||
|  | ||||
| ## Usage | ||||
|  | ||||
| ### Basic Usage | ||||
|  | ||||
| Once the application is running, you can access it through your web browser at `http://localhost:9999`. The default configuration provides: | ||||
|  | ||||
| - Home page at `/` | ||||
| - About page at `/about` | ||||
| - Contact page at `/contact` | ||||
| - Login page at `/login` | ||||
| - Registration page at `/register` | ||||
|  | ||||
| ### Configuration | ||||
|  | ||||
| The application can be configured using environment variables or configuration files. The following environment variables are supported: | ||||
|  | ||||
| - `APP__SERVER__HOST`: The host address to bind to (default: 127.0.0.1) | ||||
| - `APP__SERVER__PORT`: The port to listen on (default: 9999) | ||||
| - `APP__SERVER__WORKERS`: The number of worker threads (default: number of CPU cores) | ||||
| - `APP__TEMPLATES__DIR`: The directory containing templates (default: ./src/views) | ||||
|  | ||||
| For more detailed usage instructions, see [Usage Guide](docs/usage.md). | ||||
|  | ||||
| ## Creating Views | ||||
|  | ||||
| Hostbasket uses Tera templates for rendering views. Templates are stored in the `src/views` directory. | ||||
|  | ||||
| ### Basic Template Structure | ||||
|  | ||||
| ```html | ||||
| {% extends "base.html" %} | ||||
|  | ||||
| {% block title %}Page Title{% endblock %} | ||||
|  | ||||
| {% block content %} | ||||
| <div class="container"> | ||||
|     <h1>Hello, World!</h1> | ||||
|     <p>This is a basic template.</p> | ||||
| </div> | ||||
| {% endblock %} | ||||
| ``` | ||||
|  | ||||
| ### Adding a New Page | ||||
|  | ||||
| 1. Create a new template in the `src/views` directory | ||||
| 2. Add a new handler method in the appropriate controller | ||||
| 3. Add a new route in the `src/routes/mod.rs` file | ||||
|  | ||||
| For more detailed information on creating views, see [Views Guide](docs/views.md). | ||||
|  | ||||
| ## Authentication with Gitea | ||||
|  | ||||
| Hostbasket supports authentication with Gitea using OAuth. This allows users to log in using their Gitea accounts. | ||||
|  | ||||
| ### Setup | ||||
|  | ||||
| 1. Register a new OAuth application in your Gitea instance | ||||
| 2. Configure the OAuth credentials in your Hostbasket application | ||||
| 3. Implement the OAuth flow in your application | ||||
|  | ||||
| For more detailed information on Gitea authentication, see [Gitea Authentication Guide](docs/gitea-auth.md). | ||||
|  | ||||
| ## Documentation | ||||
|  | ||||
| For more detailed documentation, please refer to the following guides: | ||||
|  | ||||
| - [Installation Guide](docs/installation.md) | ||||
| - [Usage Guide](docs/usage.md) | ||||
| - [Views Guide](docs/views.md) | ||||
| - [MVC Architecture Guide](docs/mvc.md) | ||||
| - [Gitea Authentication Guide](docs/gitea-auth.md) | ||||
| - [API Documentation](docs/api.md) | ||||
| - [Configuration Guide](docs/configuration.md) | ||||
|  | ||||
| ## License | ||||
|  | ||||
| This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. | ||||
|   | ||||
							
								
								
									
										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. | ||||
							
								
								
									
										68
									
								
								src/config/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/config/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| use config::{Config, ConfigError, File}; | ||||
| use serde::Deserialize; | ||||
| use std::env; | ||||
|  | ||||
| // Export OAuth module | ||||
| pub mod oauth; | ||||
|  | ||||
| /// Application configuration | ||||
| #[derive(Debug, Deserialize, Clone)] | ||||
| pub struct AppConfig { | ||||
|     /// Server configuration | ||||
|     pub server: ServerConfig, | ||||
|     /// Template configuration | ||||
|     pub templates: TemplateConfig, | ||||
| } | ||||
|  | ||||
| /// Server configuration | ||||
| #[derive(Debug, Deserialize, Clone)] | ||||
| pub struct ServerConfig { | ||||
|     /// Host address to bind to | ||||
|     pub host: String, | ||||
|     /// Port to listen on | ||||
|     pub port: u16, | ||||
|     /// Workers count | ||||
|     pub workers: Option<u32>, | ||||
| } | ||||
|  | ||||
| /// Template configuration | ||||
| #[derive(Debug, Deserialize, Clone)] | ||||
| pub struct TemplateConfig { | ||||
|     /// Directory containing templates | ||||
|     pub dir: String, | ||||
| } | ||||
|  | ||||
| impl AppConfig { | ||||
|     /// Loads configuration from files and environment variables | ||||
|     pub fn new() -> Result<Self, ConfigError> { | ||||
|         // Set default values | ||||
|         let mut config_builder = Config::builder() | ||||
|             .set_default("server.host", "127.0.0.1")? | ||||
|             .set_default("server.port", 9999)? | ||||
|             .set_default("server.workers", None::<u32>)? | ||||
|             .set_default("templates.dir", "./src/views")?; | ||||
|  | ||||
|         // Load from config file if it exists | ||||
|         if let Ok(config_path) = env::var("APP_CONFIG") { | ||||
|             config_builder = config_builder.add_source(File::with_name(&config_path)); | ||||
|         } else { | ||||
|             // Try to load from default locations | ||||
|             config_builder = config_builder | ||||
|                 .add_source(File::with_name("config/default").required(false)) | ||||
|                 .add_source(File::with_name("config/local").required(false)); | ||||
|         } | ||||
|  | ||||
|         // Override with environment variables (e.g., SERVER__HOST, SERVER__PORT) | ||||
|         config_builder = | ||||
|             config_builder.add_source(config::Environment::with_prefix("APP").separator("__")); | ||||
|  | ||||
|         // Build and deserialize the config | ||||
|         let config = config_builder.build()?; | ||||
|         config.try_deserialize() | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Returns the application configuration | ||||
| pub fn get_config() -> AppConfig { | ||||
|     AppConfig::new().expect("Failed to load configuration") | ||||
| } | ||||
							
								
								
									
										62
									
								
								src/config/oauth.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/config/oauth.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| use oauth2::{ | ||||
|     AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl, | ||||
|     basic::BasicClient, AuthorizationCode, CsrfToken, Scope, TokenResponse, | ||||
| }; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use std::env; | ||||
|  | ||||
| /// Gitea OAuth configuration | ||||
| #[derive(Clone, Debug)] | ||||
| pub struct GiteaOAuthConfig { | ||||
|     /// OAuth client | ||||
|     pub client: BasicClient, | ||||
|     /// Gitea instance URL | ||||
|     pub instance_url: String, | ||||
| } | ||||
|  | ||||
| impl GiteaOAuthConfig { | ||||
|     /// Creates a new Gitea OAuth configuration | ||||
|     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(format!("{}/auth/gitea/callback", env::var("APP_URL").unwrap_or_else(|_| "http://localhost:9999".to_string()))).unwrap(), | ||||
|         ); | ||||
|          | ||||
|         Self { | ||||
|             client, | ||||
|             instance_url, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Gitea user information structure | ||||
| #[derive(Debug, Deserialize, Serialize)] | ||||
| pub struct GiteaUser { | ||||
|     /// User ID | ||||
|     pub id: i64, | ||||
|     /// Username | ||||
|     pub login: String, | ||||
|     /// Full name | ||||
|     pub full_name: String, | ||||
|     /// Email address | ||||
|     pub email: String, | ||||
|     /// Avatar URL | ||||
|     pub avatar_url: String, | ||||
| } | ||||
							
								
								
									
										193
									
								
								src/controllers/auth.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										193
									
								
								src/controllers/auth.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,193 @@ | ||||
| use actix_web::{web, HttpResponse, Responder, Result, http::header, cookie::Cookie}; | ||||
| use actix_session::Session; | ||||
| use tera::Tera; | ||||
| use crate::models::user::{User, LoginCredentials, RegistrationData, UserRole}; | ||||
| use crate::utils::render_template; | ||||
| use jsonwebtoken::{encode, decode, Header, Algorithm, Validation, EncodingKey, DecodingKey}; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use chrono::{Utc, Duration}; | ||||
| use lazy_static::lazy_static; | ||||
|  | ||||
| // JWT Claims structure | ||||
| #[derive(Debug, Serialize, Deserialize, Clone)] | ||||
| pub struct Claims { | ||||
|     pub sub: String,  // Subject (email) | ||||
|     pub exp: usize,   // Expiration time | ||||
|     pub iat: usize,   // Issued at | ||||
|     pub role: String, // User role | ||||
| } | ||||
|  | ||||
| // JWT Secret key | ||||
| lazy_static! { | ||||
|     static ref JWT_SECRET: String = std::env::var("JWT_SECRET").unwrap_or_else(|_| "your_jwt_secret_key".to_string()); | ||||
| } | ||||
|  | ||||
| /// Controller for handling authentication-related routes | ||||
| pub struct AuthController; | ||||
|  | ||||
| impl AuthController { | ||||
|     /// Generate a JWT token for a user | ||||
|     pub fn generate_token(email: &str, role: &UserRole) -> Result<String, jsonwebtoken::errors::Error> { | ||||
|         let role_str = match role { | ||||
|             UserRole::Admin => "admin", | ||||
|             UserRole::User => "user", | ||||
|         }; | ||||
|          | ||||
|         let expiration = Utc::now() | ||||
|             .checked_add_signed(Duration::hours(24)) | ||||
|             .expect("valid timestamp") | ||||
|             .timestamp() as usize; | ||||
|              | ||||
|         let claims = Claims { | ||||
|             sub: email.to_owned(), | ||||
|             exp: expiration, | ||||
|             iat: Utc::now().timestamp() as usize, | ||||
|             role: role_str.to_string(), | ||||
|         }; | ||||
|          | ||||
|         encode( | ||||
|             &Header::default(), | ||||
|             &claims, | ||||
|             &EncodingKey::from_secret(JWT_SECRET.as_bytes()) | ||||
|         ) | ||||
|     } | ||||
|      | ||||
|     /// Validate a JWT token | ||||
|     pub fn validate_token(token: &str) -> Result<Claims, jsonwebtoken::errors::Error> { | ||||
|         let validation = Validation::new(Algorithm::HS256); | ||||
|          | ||||
|         let token_data = decode::<Claims>( | ||||
|             token, | ||||
|             &DecodingKey::from_secret(JWT_SECRET.as_bytes()), | ||||
|             &validation | ||||
|         )?; | ||||
|          | ||||
|         Ok(token_data.claims) | ||||
|     } | ||||
|      | ||||
|     /// Extract token from session | ||||
|     pub fn extract_token_from_session(session: &Session) -> Option<String> { | ||||
|         session.get::<String>("auth_token").ok().flatten() | ||||
|     } | ||||
|      | ||||
|     /// Extract token from cookie | ||||
|     pub fn extract_token_from_cookie(req: &actix_web::HttpRequest) -> Option<String> { | ||||
|         req.cookie("auth_token").map(|c| c.value().to_string()) | ||||
|     } | ||||
|      | ||||
|     /// Renders the login page | ||||
|     pub async fn login_page(tmpl: web::Data<Tera>) -> Result<impl Responder> { | ||||
|         let mut ctx = tera::Context::new(); | ||||
|         ctx.insert("active_page", "login"); | ||||
|          | ||||
|         render_template(&tmpl, "auth/login.html", &ctx) | ||||
|     } | ||||
|      | ||||
|     /// Handles user login | ||||
|     pub async fn login( | ||||
|         form: web::Form<LoginCredentials>, | ||||
|         session: Session, | ||||
|         _tmpl: web::Data<Tera> | ||||
|     ) -> Result<impl Responder> { | ||||
|         // For simplicity, always log in the user without checking credentials | ||||
|         // Create a user object with admin role | ||||
|         let mut test_user = User::new( | ||||
|             "Admin User".to_string(), | ||||
|             form.email.clone() | ||||
|         ); | ||||
|          | ||||
|         // Set the ID and admin role | ||||
|         test_user.id = Some(1); | ||||
|         test_user.role = UserRole::Admin; | ||||
|          | ||||
|         // Generate JWT token | ||||
|         let token = Self::generate_token(&test_user.email, &test_user.role) | ||||
|             .map_err(|_| actix_web::error::ErrorInternalServerError("Failed to generate token"))?; | ||||
|          | ||||
|         // Store user data in session | ||||
|         let user_json = serde_json::to_string(&test_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()) | ||||
|     } | ||||
|      | ||||
|     /// Renders the registration page | ||||
|     pub async fn register_page(tmpl: web::Data<Tera>) -> Result<impl Responder> { | ||||
|         let mut ctx = tera::Context::new(); | ||||
|         ctx.insert("active_page", "register"); | ||||
|          | ||||
|         render_template(&tmpl, "auth/register.html", &ctx) | ||||
|     } | ||||
|      | ||||
|     /// Handles user registration | ||||
|     pub async fn register( | ||||
|         form: web::Form<RegistrationData>, | ||||
|         session: Session, | ||||
|         _tmpl: web::Data<Tera> | ||||
|     ) -> Result<impl Responder> { | ||||
|         // Skip validation and always create an admin user | ||||
|         let mut user = User::new( | ||||
|             form.name.clone(), | ||||
|             form.email.clone() | ||||
|         ); | ||||
|          | ||||
|         // Set the ID and admin role | ||||
|         user.id = Some(1); | ||||
|         user.role = UserRole::Admin; | ||||
|          | ||||
|         // 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()) | ||||
|     } | ||||
|      | ||||
|     /// Handles user logout | ||||
|     pub async fn logout(session: Session) -> Result<impl Responder> { | ||||
|         // Clear the session | ||||
|         session.purge(); | ||||
|          | ||||
|         // Create an expired cookie to remove the JWT token | ||||
|         let cookie = Cookie::build("auth_token", "") | ||||
|             .path("/") | ||||
|             .http_only(true) | ||||
|             .max_age(actix_web::cookie::time::Duration::seconds(0)) | ||||
|             .finish(); | ||||
|          | ||||
|         // Redirect to the home page and clear the auth token cookie | ||||
|         Ok(HttpResponse::Found() | ||||
|             .cookie(cookie) | ||||
|             .append_header((header::LOCATION, "/")) | ||||
|             .finish()) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										35
									
								
								src/controllers/home.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/controllers/home.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| use actix_web::{web, Result, Responder}; | ||||
| use tera::Tera; | ||||
| use crate::utils::render_template; | ||||
| use actix_session::Session; | ||||
|  | ||||
| /// Controller for handling home-related routes | ||||
| pub struct HomeController; | ||||
|  | ||||
| impl HomeController { | ||||
|     /// Renders the home page | ||||
|     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) | ||||
|     } | ||||
|  | ||||
|     /// Renders the about page | ||||
|     pub async fn about(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> { | ||||
|         let mut ctx = tera::Context::new(); | ||||
|         ctx.insert("active_page", "about"); | ||||
|  | ||||
|         // Add user to context if available | ||||
|         if let Ok(Some(user)) = session.get::<String>("user") { | ||||
|             ctx.insert("user_json", &user); | ||||
|         } | ||||
|  | ||||
|         render_template(&tmpl, "home/about.html", &ctx) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										3
									
								
								src/controllers/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/controllers/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| // Export controllers | ||||
| pub mod home; | ||||
| pub mod auth; | ||||
							
								
								
									
										92
									
								
								src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/main.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | ||||
| use actix_web::{web, App, HttpServer, middleware::Logger}; | ||||
| use actix_files as fs; | ||||
| use tera::Tera; | ||||
| use std::{io, env}; | ||||
| use dotenv::dotenv; | ||||
|  | ||||
| mod config; | ||||
| mod controllers; | ||||
| mod middleware; | ||||
| mod models; | ||||
| mod routes; | ||||
| mod utils; | ||||
|  | ||||
| // Session key for cookie store | ||||
| use actix_web::cookie::Key; | ||||
| use lazy_static::lazy_static; | ||||
|  | ||||
| lazy_static! { | ||||
|     static ref SESSION_KEY: Key = { | ||||
|         // Load key from environment variable or generate a random one | ||||
|         match env::var("SECRET_KEY") { | ||||
|             Ok(key) if key.as_bytes().len() >= 32 => { | ||||
|                 log::info!("Using SECRET_KEY from environment"); | ||||
|                 Key::from(key.as_bytes()) | ||||
|             } | ||||
|             _ => { | ||||
|                 log::warn!("No valid SECRET_KEY provided; generating random key (sessions will be invalidated on restart)"); | ||||
|                 Key::generate() // Generates a secure 32-byte key | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
| } | ||||
|  | ||||
| #[actix_web::main] | ||||
| async fn main() -> io::Result<()> { | ||||
|     // Initialize environment | ||||
|     dotenv().ok(); | ||||
|     env_logger::init_from_env(env_logger::Env::default().default_filter_or("info")); | ||||
|  | ||||
|     // Load configuration | ||||
|     let config = config::get_config(); | ||||
|  | ||||
|     // Check for port override from command line arguments | ||||
|     let args: Vec<String> = env::args().collect(); | ||||
|     let mut port = config.server.port; | ||||
|  | ||||
|     for i in 1..args.len() { | ||||
|         if args[i] == "--port" && i + 1 < args.len() { | ||||
|             if let Ok(p) = args[i + 1].parse::<u16>() { | ||||
|                 port = p; | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     let bind_address = format!("{}:{}", config.server.host, port); | ||||
|  | ||||
|     log::info!("Starting server at http://{}", bind_address); | ||||
|  | ||||
|     // Create and configure the HTTP server | ||||
|     HttpServer::new(move || { | ||||
|         // Initialize Tera templates | ||||
|         let mut tera = match Tera::new(&format!("{}/**/*.html", config.templates.dir)) { | ||||
|             Ok(t) => t, | ||||
|             Err(e) => { | ||||
|                 log::error!("Parsing error(s): {}", e); | ||||
|                 ::std::process::exit(1); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         // Register custom Tera functions | ||||
|         utils::register_tera_functions(&mut tera); | ||||
|  | ||||
|         App::new() | ||||
|             // Enable logger middleware | ||||
|             .wrap(Logger::default()) | ||||
|             // Add custom middleware | ||||
|             .wrap(middleware::RequestTimer) | ||||
|             .wrap(middleware::SecurityHeaders) | ||||
|             .wrap(middleware::JwtAuth) | ||||
|             // Configure static files | ||||
|             .service(fs::Files::new("/static", "./src/static")) | ||||
|             // Add Tera template engine | ||||
|             .app_data(web::Data::new(tera)) | ||||
|             // Configure routes | ||||
|             .configure(routes::configure_routes) | ||||
|     }) | ||||
|     .workers(config.server.workers.unwrap_or_else(|| num_cpus::get() as u32) as usize) | ||||
|     .bind(bind_address)? | ||||
|     .run() | ||||
|     .await | ||||
| } | ||||
							
								
								
									
										197
									
								
								src/middleware/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										197
									
								
								src/middleware/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,197 @@ | ||||
| use std::{ | ||||
|     future::{ready, Ready}, | ||||
|     time::Instant, | ||||
| }; | ||||
| use actix_web::{ | ||||
|     dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, | ||||
|     Error, | ||||
| }; | ||||
| use futures_util::future::LocalBoxFuture; | ||||
|  | ||||
| // Request Timer Middleware | ||||
| pub struct RequestTimer; | ||||
|  | ||||
| impl<S, B> Transform<S, ServiceRequest> for RequestTimer | ||||
| where | ||||
|     S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>, | ||||
|     S::Future: 'static, | ||||
|     B: 'static, | ||||
| { | ||||
|     type Response = ServiceResponse<B>; | ||||
|     type Error = Error; | ||||
|     type InitError = (); | ||||
|     type Transform = RequestTimerMiddleware<S>; | ||||
|     type Future = Ready<Result<Self::Transform, Self::InitError>>; | ||||
|  | ||||
|     fn new_transform(&self, service: S) -> Self::Future { | ||||
|         ready(Ok(RequestTimerMiddleware { service })) | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub struct RequestTimerMiddleware<S> { | ||||
|     service: S, | ||||
| } | ||||
|  | ||||
| impl<S, B> Service<ServiceRequest> for RequestTimerMiddleware<S> | ||||
| where | ||||
|     S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>, | ||||
|     S::Future: 'static, | ||||
|     B: 'static, | ||||
| { | ||||
|     type Response = ServiceResponse<B>; | ||||
|     type Error = Error; | ||||
|     type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>; | ||||
|  | ||||
|     forward_ready!(service); | ||||
|  | ||||
|     fn call(&self, req: ServiceRequest) -> Self::Future { | ||||
|         let start = Instant::now(); | ||||
|         let path = req.path().to_string(); | ||||
|  | ||||
|         let fut = self.service.call(req); | ||||
|  | ||||
|         Box::pin(async move { | ||||
|             let res = fut.await?; | ||||
|             let elapsed = start.elapsed(); | ||||
|  | ||||
|             log::info!("Request to {} took {:?}", path, elapsed); | ||||
|  | ||||
|             Ok(res) | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Security Headers Middleware | ||||
| pub struct SecurityHeaders; | ||||
|  | ||||
| impl<S, B> Transform<S, ServiceRequest> for SecurityHeaders | ||||
| where | ||||
|     S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>, | ||||
|     S::Future: 'static, | ||||
|     B: 'static, | ||||
| { | ||||
|     type Response = ServiceResponse<B>; | ||||
|     type Error = Error; | ||||
|     type InitError = (); | ||||
|     type Transform = SecurityHeadersMiddleware<S>; | ||||
|     type Future = Ready<Result<Self::Transform, Self::InitError>>; | ||||
|  | ||||
|     fn new_transform(&self, service: S) -> Self::Future { | ||||
|         ready(Ok(SecurityHeadersMiddleware { service })) | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub struct SecurityHeadersMiddleware<S> { | ||||
|     service: S, | ||||
| } | ||||
|  | ||||
| impl<S, B> Service<ServiceRequest> for SecurityHeadersMiddleware<S> | ||||
| where | ||||
|     S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>, | ||||
|     S::Future: 'static, | ||||
|     B: 'static, | ||||
| { | ||||
|     type Response = ServiceResponse<B>; | ||||
|     type Error = Error; | ||||
|     type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>; | ||||
|  | ||||
|     forward_ready!(service); | ||||
|  | ||||
|     fn call(&self, req: ServiceRequest) -> Self::Future { | ||||
|         let fut = self.service.call(req); | ||||
|  | ||||
|         Box::pin(async move { | ||||
|             let mut res = fut.await?; | ||||
|  | ||||
|             // Add security headers | ||||
|             res.headers_mut().insert( | ||||
|                 actix_web::http::header::X_CONTENT_TYPE_OPTIONS, | ||||
|                 actix_web::http::header::HeaderValue::from_static("nosniff"), | ||||
|             ); | ||||
|  | ||||
|             res.headers_mut().insert( | ||||
|                 actix_web::http::header::X_FRAME_OPTIONS, | ||||
|                 actix_web::http::header::HeaderValue::from_static("DENY"), | ||||
|             ); | ||||
|  | ||||
|             res.headers_mut().insert( | ||||
|                 actix_web::http::header::X_XSS_PROTECTION, | ||||
|                 actix_web::http::header::HeaderValue::from_static("1; mode=block"), | ||||
|             ); | ||||
|  | ||||
|             Ok(res) | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| // JWT Authentication Middleware | ||||
| pub struct JwtAuth; | ||||
|  | ||||
| impl<S, B> Transform<S, ServiceRequest> for JwtAuth | ||||
| where | ||||
|     S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>, | ||||
|     S::Future: 'static, | ||||
|     B: 'static, | ||||
| { | ||||
|     type Response = ServiceResponse<B>; | ||||
|     type Error = Error; | ||||
|     type InitError = (); | ||||
|     type Transform = JwtAuthMiddleware<S>; | ||||
|     type Future = Ready<Result<Self::Transform, Self::InitError>>; | ||||
|  | ||||
|     fn new_transform(&self, service: S) -> Self::Future { | ||||
|         ready(Ok(JwtAuthMiddleware { service })) | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub struct JwtAuthMiddleware<S> { | ||||
|     service: S, | ||||
| } | ||||
|  | ||||
| impl<S, B> Service<ServiceRequest> for JwtAuthMiddleware<S> | ||||
| where | ||||
|     S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>, | ||||
|     S::Future: 'static, | ||||
|     B: 'static, | ||||
| { | ||||
|     type Response = ServiceResponse<B>; | ||||
|     type Error = Error; | ||||
|     type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>; | ||||
|  | ||||
|     forward_ready!(service); | ||||
|  | ||||
|     fn call(&self, req: ServiceRequest) -> Self::Future { | ||||
|         // Define public routes that don't require authentication | ||||
|         let path = req.path().to_string(); | ||||
|         let public_routes = vec![ | ||||
|             "/login", | ||||
|             "/register", | ||||
|             "/static", | ||||
|             "/favicon.ico", | ||||
|             "/", | ||||
|             "/about", | ||||
|             "/auth/gitea", | ||||
|             "/auth/gitea/callback" | ||||
|         ]; | ||||
|  | ||||
|         // Check if the current path is a public route | ||||
|         let is_public_route = public_routes.iter().any(|route| path.starts_with(route)); | ||||
|  | ||||
|         if is_public_route { | ||||
|             // For public routes, just pass through without authentication check | ||||
|             let fut = self.service.call(req); | ||||
|             return Box::pin(async move { | ||||
|                 fut.await | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         // For protected routes, check for authentication | ||||
|         // This is a simplified version - in a real application, you would check for a valid JWT token | ||||
|  | ||||
|         // For now, just pass through all requests | ||||
|         let fut = self.service.call(req); | ||||
|         Box::pin(async move { | ||||
|             fut.await | ||||
|         }) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										2
									
								
								src/models/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/models/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| // Export models | ||||
| pub mod user; | ||||
							
								
								
									
										62
									
								
								src/models/user.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/models/user.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use chrono::{DateTime, Utc}; | ||||
|  | ||||
| /// Represents a user in the system | ||||
| #[derive(Debug, Clone, Serialize, Deserialize)] | ||||
| pub struct User { | ||||
|     /// Unique identifier for the user | ||||
|     pub id: Option<i32>, | ||||
|     /// User's full name | ||||
|     pub name: String, | ||||
|     /// User's email address | ||||
|     pub email: String, | ||||
|     /// User's hashed password | ||||
|     #[serde(skip_serializing)] | ||||
|     pub password_hash: Option<String>, | ||||
|     /// User's role in the system | ||||
|     pub role: UserRole, | ||||
|     /// When the user was created | ||||
|     pub created_at: Option<DateTime<Utc>>, | ||||
|     /// When the user was last updated | ||||
|     pub updated_at: Option<DateTime<Utc>>, | ||||
| } | ||||
|  | ||||
| /// Represents the possible roles a user can have | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
| pub enum UserRole { | ||||
|     /// Regular user with limited permissions | ||||
|     User, | ||||
|     /// Administrator with full permissions | ||||
|     Admin, | ||||
| } | ||||
|  | ||||
| impl User { | ||||
|     /// Creates a new user with default values | ||||
|     pub fn new(name: String, email: String) -> Self { | ||||
|         Self { | ||||
|             id: None, | ||||
|             name, | ||||
|             email, | ||||
|             password_hash: None, | ||||
|             role: UserRole::User, | ||||
|             created_at: Some(Utc::now()), | ||||
|             updated_at: Some(Utc::now()), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Represents user login credentials | ||||
| #[derive(Debug, Deserialize)] | ||||
| pub struct LoginCredentials { | ||||
|     pub email: String, | ||||
|     pub password: String, | ||||
| } | ||||
|  | ||||
| /// Represents user registration data | ||||
| #[derive(Debug, Deserialize)] | ||||
| pub struct RegistrationData { | ||||
|     pub name: String, | ||||
|     pub email: String, | ||||
|     pub password: String, | ||||
|     pub password_confirmation: String, | ||||
| } | ||||
							
								
								
									
										35
									
								
								src/routes/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/routes/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| use crate::controllers::auth::AuthController; | ||||
| use crate::controllers::home::HomeController; | ||||
| use crate::middleware::JwtAuth; | ||||
| use crate::SESSION_KEY; | ||||
| use actix_session::{storage::CookieSessionStore, SessionMiddleware}; | ||||
| use actix_web::web; | ||||
|  | ||||
| /// Configures all application routes | ||||
| pub fn configure_routes(cfg: &mut web::ServiceConfig) { | ||||
|     // 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) | ||||
|             // Home routes | ||||
|             .route("/", web::get().to(HomeController::index)) | ||||
|             .route("/about", web::get().to(HomeController::about)) | ||||
|             // Auth routes | ||||
|             .route("/login", web::get().to(AuthController::login_page)) | ||||
|             .route("/login", web::post().to(AuthController::login)) | ||||
|             .route("/register", web::get().to(AuthController::register_page)) | ||||
|             .route("/register", web::post().to(AuthController::register)) | ||||
|             .route("/logout", web::get().to(AuthController::logout)), | ||||
|     ); | ||||
|  | ||||
|     // Protected routes that require authentication | ||||
|     cfg.service( | ||||
|         web::scope("/protected").wrap(JwtAuth), // Apply JWT authentication middleware | ||||
|     ); | ||||
| } | ||||
							
								
								
									
										49
									
								
								src/static/css/styles.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/static/css/styles.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| /* Custom styles for Hostbasket */ | ||||
|  | ||||
| /* Global styles */ | ||||
| body { | ||||
|     min-height: 100vh; | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
| } | ||||
|  | ||||
| main { | ||||
|     flex: 1; | ||||
| } | ||||
|  | ||||
| /* Navigation */ | ||||
| .navbar-brand { | ||||
|     font-weight: bold; | ||||
| } | ||||
|  | ||||
| /* Cards */ | ||||
| .card { | ||||
|     margin-bottom: 1rem; | ||||
|     box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); | ||||
|     transition: all 0.3s ease; | ||||
| } | ||||
|  | ||||
| .card-main { | ||||
|     height: 120px; | ||||
| } | ||||
|  | ||||
| .card:hover { | ||||
|     box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); | ||||
|     transform: translateY(-0.25rem); | ||||
| } | ||||
|  | ||||
| /* Buttons */ | ||||
| .btn { | ||||
|     border-radius: 0.25rem; | ||||
| } | ||||
|  | ||||
| /* Forms */ | ||||
| .form-control:focus { | ||||
|     border-color: #80bdff; | ||||
|     box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); | ||||
| } | ||||
|  | ||||
| /* Footer */ | ||||
| .footer { | ||||
|     margin-top: auto; | ||||
| } | ||||
							
								
								
									
										86
									
								
								src/utils/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								src/utils/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | ||||
| use actix_web::{web, HttpResponse, Result}; | ||||
| use chrono::{DateTime, Utc}; | ||||
| use serde_json::Value; | ||||
| use std::collections::HashMap; | ||||
| use tera::{Context, Tera}; | ||||
|  | ||||
| /// Renders a template with the given context | ||||
| pub fn render_template( | ||||
|     tmpl: &web::Data<Tera>, | ||||
|     template_name: &str, | ||||
|     context: &Context, | ||||
| ) -> Result<HttpResponse> { | ||||
|     let rendered = tmpl.render(template_name, context).map_err(|e| { | ||||
|         log::error!("Template rendering error: {}", e); | ||||
|         actix_web::error::ErrorInternalServerError("Template rendering error") | ||||
|     })?; | ||||
|  | ||||
|     Ok(HttpResponse::Ok().content_type("text/html").body(rendered)) | ||||
| } | ||||
|  | ||||
| /// Registers custom functions with Tera | ||||
| pub fn register_tera_functions(tera: &mut Tera) { | ||||
|     tera.register_function("format_date", format_date); | ||||
|     tera.register_function("active_class", active_class); | ||||
| } | ||||
|  | ||||
| /// Custom Tera function to format dates | ||||
| fn format_date(args: &HashMap<String, Value>) -> tera::Result<Value> { | ||||
|     let date = match args.get("date") { | ||||
|         Some(val) => val | ||||
|             .as_str() | ||||
|             .ok_or_else(|| tera::Error::msg("Date must be a string"))?, | ||||
|         None => return Err(tera::Error::msg("Date is required")), | ||||
|     }; | ||||
|  | ||||
|     let format = match args.get("format") { | ||||
|         Some(val) => val.as_str().unwrap_or("%Y-%m-%d %H:%M:%S"), | ||||
|         None => "%Y-%m-%d %H:%M:%S", | ||||
|     }; | ||||
|  | ||||
|     // Parse the date string | ||||
|     let datetime = match DateTime::parse_from_rfc3339(date) { | ||||
|         Ok(dt) => dt.with_timezone(&Utc), | ||||
|         Err(_) => { | ||||
|             // Try parsing as a timestamp | ||||
|             match date.parse::<i64>() { | ||||
|                 Ok(ts) => DateTime::from_timestamp(ts, 0) | ||||
|                     .ok_or_else(|| tera::Error::msg("Invalid timestamp"))?, | ||||
|                 Err(_) => return Err(tera::Error::msg("Invalid date format")), | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     // Format the date | ||||
|     let formatted = datetime.format(format).to_string(); | ||||
|  | ||||
|     Ok(Value::String(formatted)) | ||||
| } | ||||
|  | ||||
| /// Custom Tera function to set active class for navigation | ||||
| fn active_class(args: &HashMap<String, Value>) -> tera::Result<Value> { | ||||
|     let current = match args.get("current") { | ||||
|         Some(val) => val | ||||
|             .as_str() | ||||
|             .ok_or_else(|| tera::Error::msg("Current must be a string"))?, | ||||
|         None => return Err(tera::Error::msg("Current is required")), | ||||
|     }; | ||||
|  | ||||
|     let page = match args.get("page") { | ||||
|         Some(val) => val | ||||
|             .as_str() | ||||
|             .ok_or_else(|| tera::Error::msg("Page must be a string"))?, | ||||
|         None => return Err(tera::Error::msg("Page is required")), | ||||
|     }; | ||||
|  | ||||
|     let class = match args.get("class") { | ||||
|         Some(val) => val.as_str().unwrap_or("active"), | ||||
|         None => "active", | ||||
|     }; | ||||
|  | ||||
|     if current == page { | ||||
|         Ok(Value::String(class.to_string())) | ||||
|     } else { | ||||
|         Ok(Value::String("".to_string())) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										45
									
								
								src/views/auth/login.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/views/auth/login.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| {% extends "base.html" %} | ||||
|  | ||||
| {% block title %}Login - 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>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"> | ||||
|                             Login with Gitea | ||||
|                         </a> | ||||
|                     </div> | ||||
|                      | ||||
|                     <hr> | ||||
|                      | ||||
|                     <div class="text-center"> | ||||
|                         <p>Don't have an account? <a href="/register">Register</a></p> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| {% endblock %} | ||||
							
								
								
									
										44
									
								
								src/views/auth/register.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/views/auth/register.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| {% 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> | ||||
|                      | ||||
|                     <hr> | ||||
|                      | ||||
|                     <div class="text-center"> | ||||
|                         <p>Already have an account? <a href="/login">Login</a></p> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| {% endblock %} | ||||
							
								
								
									
										66
									
								
								src/views/base.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/views/base.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| <!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 href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet"> | ||||
|     <link rel="stylesheet" href="/static/css/styles.css"> | ||||
|     {% block head %}{% endblock %} | ||||
| </head> | ||||
|  | ||||
| <body> | ||||
|     <nav class="navbar navbar-expand-lg navbar-dark bg-dark"> | ||||
|         <div class="container"> | ||||
|             <a class="navbar-brand" href="/">Hostbasket</a> | ||||
|             <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" | ||||
|                 aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> | ||||
|                 <span class="navbar-toggler-icon"></span> | ||||
|             </button> | ||||
|             <div class="collapse navbar-collapse" id="navbarNav"> | ||||
|                 <ul class="navbar-nav me-auto"> | ||||
|                     <li class="nav-item"> | ||||
|                         <a class="nav-link {{ active_class(current=active_page, page=" home") }}" href="/">Home</a> | ||||
|                     </li> | ||||
|                     <li class="nav-item"> | ||||
|                         <a class="nav-link {{ active_class(current=active_page, page=" about") }}" | ||||
|                             href="/about">About</a> | ||||
|                     </li> | ||||
|  | ||||
|                 </ul> | ||||
|                 <ul class="navbar-nav"> | ||||
|                     {% if user_json %} | ||||
|                     <li class="nav-item"> | ||||
|                         <a class="nav-link" href="/logout">Logout</a> | ||||
|                     </li> | ||||
|                     {% else %} | ||||
|                     <li class="nav-item"> | ||||
|                         <a class="nav-link {{ active_class(current=active_page, page=" login") }}" | ||||
|                             href="/login">Login</a> | ||||
|                     </li> | ||||
|                     <li class="nav-item"> | ||||
|                         <a class="nav-link {{ active_class(current=active_page, page=" register") }}" | ||||
|                             href="/register">Register</a> | ||||
|                     </li> | ||||
|                     {% endif %} | ||||
|                 </ul> | ||||
|             </div> | ||||
|         </div> | ||||
|     </nav> | ||||
|  | ||||
|     <main class="py-4"> | ||||
|         {% block content %}{% endblock %} | ||||
|     </main> | ||||
|  | ||||
|     <footer class="footer mt-auto py-3 bg-light"> | ||||
|         <div class="container text-center"> | ||||
|             <span class="text-muted">© 2023 Hostbasket Powered byThreefold. All rights reserved.</span> | ||||
|         </div> | ||||
|     </footer> | ||||
|  | ||||
|     <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script> | ||||
|     {% block scripts %}{% endblock %} | ||||
| </body> | ||||
|  | ||||
| </html> | ||||
							
								
								
									
										51
									
								
								src/views/home/about.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/views/home/about.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| {% extends "base.html" %} | ||||
|  | ||||
| {% block title %}About - Hostbasket{% endblock %} | ||||
|  | ||||
| {% block content %} | ||||
| <div class="container"> | ||||
|     <div class="row"> | ||||
|         <div class="col-md-12"> | ||||
|             <h1>About Hostbasket</h1> | ||||
|             <p class="lead">A web application framework built with Actix Web and Rust.</p> | ||||
|             <hr> | ||||
|  | ||||
|             <h2>Features</h2> | ||||
|             <ul> | ||||
|                 <li><strong>Actix Web</strong>: A powerful, pragmatic, and extremely fast web framework for Rust</li> | ||||
|                 <li><strong>Tera Templates</strong>: A template engine inspired by Jinja2 and Django templates</li> | ||||
|                 <li><strong>Bootstrap 5.3.5</strong>: A popular CSS framework for responsive web design</li> | ||||
|                 <li><strong>MVC Architecture</strong>: Clean separation of concerns with Models, Views, and Controllers | ||||
|                 </li> | ||||
|                 <li><strong>Middleware Support</strong>: Custom middleware for request timing and security headers</li> | ||||
|                 <li><strong>Configuration Management</strong>: Flexible configuration system with environment variable | ||||
|                     support</li> | ||||
|                 <li><strong>Static File Serving</strong>: Serve CSS, JavaScript, and other static assets</li> | ||||
|             </ul> | ||||
|  | ||||
|             <h2>Project Structure</h2> | ||||
|             <pre> | ||||
| hostbasket/ | ||||
| ├── Cargo.toml                 # Project dependencies | ||||
| ├── src/ | ||||
| │   ├── config/                # Configuration management | ||||
| │   ├── controllers/           # Request handlers | ||||
| │   ├── middleware/            # Custom middleware components | ||||
| │   ├── models/                # Data models and business logic | ||||
| │   ├── routes/                # Route definitions | ||||
| │   ├── static/                # Static assets (CSS, JS, images) | ||||
| │   │   ├── css/               # CSS files including Bootstrap | ||||
| │   │   ├── js/                # JavaScript files | ||||
| │   │   └── images/            # Image files | ||||
| │   ├── utils/                 # Utility functions | ||||
| │   ├── views/                 # Tera templates | ||||
| │   └── main.rs                # Application entry point | ||||
|             </pre> | ||||
|  | ||||
|             <h2>Getting Started</h2> | ||||
|             <p>To get started with Hostbasket, check out the <a target="_blank" | ||||
|                     href="https://git.ourworld.tf/herocode/rweb_starterkit">GitHub repository</a>.</p> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| {% endblock %} | ||||
							
								
								
									
										48
									
								
								src/views/home/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/views/home/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| {% extends "base.html" %} | ||||
|  | ||||
| {% block title %}Home - Hostbasket{% endblock %} | ||||
|  | ||||
| {% block content %} | ||||
| <div class="container"> | ||||
|     <div class="row"> | ||||
|         <div class="col-md-12"> | ||||
|             <div class="jumbotron"> | ||||
|                 <h1 class="display-4">Welcome to Hostbasket!</h1> | ||||
|                 <p class="lead">A web application framework built with Actix Web and Rust.</p> | ||||
|                 <hr class="my-4"> | ||||
|                 <p>This is a starter template for building web applications with Actix Web and Rust.</p> | ||||
|                 <p class="lead"> | ||||
|                     <a class="btn btn-primary btn-lg" href="/about" role="button">Learn more</a> | ||||
|                 </p> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <div class="row mt-4"> | ||||
|         <div class="col-md-4"> | ||||
|             <div class="card card-main"> | ||||
|                 <div class="card-body"> | ||||
|                     <h5 class="card-title">Fast</h5> | ||||
|                     <p class="card-text">Built with Actix Web, one of the fastest web frameworks available.</p> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|         <div class="col-md-4"> | ||||
|             <div class="card card-main"> | ||||
|                 <div class="card-body"> | ||||
|                     <h5 class="card-title">Secure</h5> | ||||
|                     <p class="card-text">Rust's memory safety guarantees help prevent common security issues.</p> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|         <div class="col-md-4"> | ||||
|             <div class="card card-main"> | ||||
|                 <div class="card-body"> | ||||
|                     <h5 class="card-title">Scalable</h5> | ||||
|                     <p class="card-text">Designed to handle high loads and scale horizontally.</p> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| {% endblock %} | ||||
		Reference in New Issue
	
	Block a user