feat: Add .gitignore and initial project structure
- Add a comprehensive .gitignore to manage project files. - Create the basic project structure including Cargo.toml, LICENSE, and README.md. - Add basic project documentation.
This commit is contained in:
parent
005d1d8b48
commit
4cc61a955d
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
|
32
Cargo.toml
Normal file
32
Cargo.toml
Normal file
@ -0,0 +1,32 @@
|
||||
[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 = { version = "4.3" }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "2.8"
|
||||
tokio = { version = "1.28", features = ["full"] }
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 CodeScalers
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
133
README.md
133
README.md
@ -1,2 +1,133 @@
|
||||
# giteaui
|
||||
# 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();
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
352
docs/gitea-auth.md
Normal file
352
docs/gitea-auth.md
Normal file
@ -0,0 +1,352 @@
|
||||
# Gitea Authentication Guide
|
||||
|
||||
This guide provides detailed instructions on how to integrate Gitea authentication into your Hostbasket application.
|
||||
|
||||
## 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** > **Integrations** > **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.
|
120
docs/troubleshooting.md
Normal file
120
docs/troubleshooting.md
Normal file
@ -0,0 +1,120 @@
|
||||
# Troubleshooting Guide
|
||||
|
||||
This guide provides solutions to common issues you might encounter when using the Hostbasket application.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Authentication Issues](#authentication-issues)
|
||||
- [Missing CSRF Token](#missing-csrf-token)
|
||||
- [Invalid CSRF Token](#invalid-csrf-token)
|
||||
- [JWT Token Issues](#jwt-token-issues)
|
||||
2. [OAuth Issues](#oauth-issues)
|
||||
- [Gitea Authentication Errors](#gitea-authentication-errors)
|
||||
3. [Server Issues](#server-issues)
|
||||
- [Port Already in Use](#port-already-in-use)
|
||||
- [Template Parsing Errors](#template-parsing-errors)
|
||||
|
||||
## Authentication Issues
|
||||
|
||||
### Missing CSRF Token
|
||||
|
||||
**Problem**: When trying to authenticate with Gitea, you receive a "Missing CSRF token" error.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. **Check your SECRET_KEY environment variable**:
|
||||
- Ensure you have a valid SECRET_KEY in your `.env` file
|
||||
- The SECRET_KEY must be at least 32 bytes long
|
||||
- Example: `SECRET_KEY=01234567890123456789012345678901`
|
||||
|
||||
2. **Enable debug logging**:
|
||||
- Set `RUST_LOG=debug` in your `.env` file
|
||||
- Restart the application
|
||||
- Check the logs for more detailed information
|
||||
|
||||
3. **Clear browser cookies**:
|
||||
- Clear all cookies for your application domain
|
||||
- Try the authentication process again
|
||||
|
||||
4. **Check session configuration**:
|
||||
- Make sure your session middleware is properly configured
|
||||
- The SameSite policy should be set to "Lax" for OAuth redirects
|
||||
|
||||
### Invalid CSRF Token
|
||||
|
||||
**Problem**: When trying to authenticate with Gitea, you receive an "Invalid CSRF token" error.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. **Check for multiple tabs/windows**:
|
||||
- Make sure you're not trying to authenticate in multiple tabs/windows simultaneously
|
||||
- Each authentication attempt generates a new CSRF token
|
||||
|
||||
2. **Check for browser extensions**:
|
||||
- Some browser extensions might interfere with cookies or redirects
|
||||
- Try disabling extensions or using a different browser
|
||||
|
||||
### JWT Token Issues
|
||||
|
||||
**Problem**: You're logged in but keep getting redirected to the login page.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. **Check JWT_SECRET**:
|
||||
- Ensure your JWT_SECRET is consistent across application restarts
|
||||
- Set a permanent JWT_SECRET in your `.env` file
|
||||
|
||||
2. **Check token expiration**:
|
||||
- The default token expiration is 24 hours
|
||||
- You can adjust this with the JWT_EXPIRATION_HOURS environment variable
|
||||
|
||||
## OAuth Issues
|
||||
|
||||
### Gitea Authentication Errors
|
||||
|
||||
**Problem**: You encounter errors when trying to authenticate with Gitea.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. **Check OAuth configuration**:
|
||||
- Verify your GITEA_CLIENT_ID and GITEA_CLIENT_SECRET are correct
|
||||
- Make sure your GITEA_INSTANCE_URL is correct and accessible
|
||||
- Ensure your APP_URL is set correctly for the callback URL
|
||||
|
||||
2. **Check Gitea application settings**:
|
||||
- Verify the redirect URI in your Gitea application settings matches your callback URL
|
||||
- The redirect URI should be: `http://localhost:9999/auth/gitea/callback` (adjust as needed)
|
||||
|
||||
3. **Check network connectivity**:
|
||||
- Ensure your application can reach the Gitea instance
|
||||
- Check for any firewalls or network restrictions
|
||||
|
||||
## Server Issues
|
||||
|
||||
### Port Already in Use
|
||||
|
||||
**Problem**: When starting the application, you get an "Address already in use" error.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. **Change the port**:
|
||||
- Set a different port in your `.env` file: `APP__SERVER__PORT=8080`
|
||||
- Or use the command-line flag: `cargo run -- --port 8080`
|
||||
|
||||
2. **Find and stop the process using the port**:
|
||||
- On Linux/macOS: `lsof -i :9999` to find the process
|
||||
- Then `kill <PID>` to stop it
|
||||
|
||||
### Template Parsing Errors
|
||||
|
||||
**Problem**: The application fails to start with template parsing errors.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. **Check template syntax**:
|
||||
- Verify that all your Tera templates have valid syntax
|
||||
- Look for unclosed tags, missing blocks, or invalid expressions
|
||||
|
||||
2. **Check template directory**:
|
||||
- Make sure your APP__TEMPLATES__DIR environment variable is set correctly
|
||||
- The default is `./src/views`
|
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")
|
||||
}
|
63
src/config/oauth.rs
Normal file
63
src/config/oauth.rs
Normal file
@ -0,0 +1,63 @@
|
||||
use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl};
|
||||
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,
|
||||
}
|
225
src/controllers/auth.rs
Normal file
225
src/controllers/auth.rs
Normal file
@ -0,0 +1,225 @@
|
||||
use crate::models::user::{LoginCredentials, RegistrationData, User, UserRole};
|
||||
use crate::utils::render_template;
|
||||
use actix_session::Session;
|
||||
use actix_web::{cookie::Cookie, http::header, web, HttpResponse, Responder, Result};
|
||||
use chrono::{Duration, Utc};
|
||||
use jsonwebtoken::{encode, EncodingKey, Header};
|
||||
use lazy_static::lazy_static;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::env;
|
||||
use tera::Tera;
|
||||
|
||||
// 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()),
|
||||
)
|
||||
}
|
||||
|
||||
// Note: The following functions were removed as they were not being used:
|
||||
// - validate_token
|
||||
// - extract_token_from_session
|
||||
// - extract_token_from_cookie
|
||||
// They can be re-implemented if needed in the future.
|
||||
|
||||
/// Renders the login page
|
||||
pub async fn login_page(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
|
||||
let mut ctx = tera::Context::new();
|
||||
let is_gitea_flow_active = env::var("GITEA_CLIENT_ID")
|
||||
.ok()
|
||||
.filter(|s| !s.is_empty())
|
||||
.is_some();
|
||||
ctx.insert("gitea_enabled", &is_gitea_flow_active);
|
||||
ctx.insert("active_page", "login");
|
||||
// Add user to context if available
|
||||
if let Ok(Some(user_json)) = session.get::<String>("user") {
|
||||
// Keep the raw JSON for backward compatibility
|
||||
ctx.insert("user_json", &user_json);
|
||||
|
||||
// Parse the JSON into a User object
|
||||
match serde_json::from_str::<User>(&user_json) {
|
||||
Ok(user) => {
|
||||
log::info!("Successfully parsed user in login_page: {:?}", user);
|
||||
ctx.insert("user", &user);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to parse user JSON in login_page: {}", e);
|
||||
log::error!("User JSON: {}", user_json);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
log::info!("Storing user in session (login): {}", user_json);
|
||||
log::info!("User object: {:?}", test_user);
|
||||
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>, session: Session) -> Result<impl Responder> {
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.insert("active_page", "register");
|
||||
|
||||
let is_gitea_flow_active = env::var("GITEA_CLIENT_ID")
|
||||
.ok()
|
||||
.filter(|s| !s.is_empty())
|
||||
.is_some();
|
||||
ctx.insert("gitea_enabled", &is_gitea_flow_active);
|
||||
|
||||
// Add user to context if available
|
||||
if let Ok(Some(user_json)) = session.get::<String>("user") {
|
||||
// Keep the raw JSON for backward compatibility
|
||||
ctx.insert("user_json", &user_json);
|
||||
|
||||
// Parse the JSON into a User object
|
||||
match serde_json::from_str::<User>(&user_json) {
|
||||
Ok(user) => {
|
||||
log::info!("Successfully parsed user in register_page: {:?}", user);
|
||||
ctx.insert("user", &user);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to parse user JSON in register_page: {}", e);
|
||||
log::error!("User JSON: {}", user_json);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
log::info!("Storing user in session (register): {}", user_json);
|
||||
log::info!("User object: {:?}", user);
|
||||
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())
|
||||
}
|
||||
}
|
72
src/controllers/debug.rs
Normal file
72
src/controllers/debug.rs
Normal file
@ -0,0 +1,72 @@
|
||||
use actix_web::{HttpRequest, HttpResponse, Responder, Result};
|
||||
use actix_session::Session;
|
||||
use serde_json::json;
|
||||
|
||||
/// Controller for debugging
|
||||
pub struct DebugController;
|
||||
|
||||
impl DebugController {
|
||||
/// Display debug information
|
||||
pub async fn debug_info(req: HttpRequest, session: Session) -> Result<impl Responder> {
|
||||
// Collect cookies
|
||||
let mut cookies = Vec::new();
|
||||
if let Ok(cookie_iter) = req.cookies() {
|
||||
for cookie in cookie_iter.iter() {
|
||||
cookies.push(json!({
|
||||
"name": cookie.name(),
|
||||
"value": cookie.value(),
|
||||
"http_only": cookie.http_only(),
|
||||
"secure": cookie.secure(),
|
||||
"same_site": format!("{:?}", cookie.same_site()),
|
||||
"path": cookie.path(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Collect session data
|
||||
let mut session_data = Vec::new();
|
||||
|
||||
// Get session keys
|
||||
let mut session_keys = Vec::new();
|
||||
if let Ok(Some(csrf_token)) = session.get::<String>("oauth_csrf_token") {
|
||||
session_data.push(json!({
|
||||
"key": "oauth_csrf_token",
|
||||
"value": csrf_token,
|
||||
}));
|
||||
session_keys.push("oauth_csrf_token".to_string());
|
||||
}
|
||||
|
||||
if let Ok(Some(user)) = session.get::<String>("user") {
|
||||
session_data.push(json!({
|
||||
"key": "user",
|
||||
"value": user,
|
||||
}));
|
||||
session_keys.push("user".to_string());
|
||||
}
|
||||
|
||||
if let Ok(Some(auth_token)) = session.get::<String>("auth_token") {
|
||||
session_data.push(json!({
|
||||
"key": "auth_token",
|
||||
"value": auth_token,
|
||||
}));
|
||||
session_keys.push("auth_token".to_string());
|
||||
}
|
||||
|
||||
// Add session keys to response
|
||||
session_data.push(json!({
|
||||
"key": "_session_keys",
|
||||
"value": session_keys.join(", "),
|
||||
}));
|
||||
|
||||
// Create response
|
||||
let response = json!({
|
||||
"cookies": cookies,
|
||||
"session": session_data,
|
||||
"csrf_token_session": session.get::<String>("oauth_csrf_token").unwrap_or(None),
|
||||
"csrf_token_cookie": req.cookie("oauth_csrf_token").map(|c| c.value().to_string()),
|
||||
"csrf_token_debug_cookie": req.cookie("oauth_csrf_token_debug").map(|c| c.value().to_string()),
|
||||
});
|
||||
|
||||
Ok(HttpResponse::Ok().json(response))
|
||||
}
|
||||
}
|
222
src/controllers/gitea_auth.rs
Normal file
222
src/controllers/gitea_auth.rs
Normal file
@ -0,0 +1,222 @@
|
||||
use actix_web::{web, HttpRequest, 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;
|
||||
|
||||
|
||||
/// Controller for handling Gitea authentication
|
||||
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
|
||||
let csrf_secret = csrf_token.secret().to_string();
|
||||
log::info!("Setting CSRF token in session: {}", csrf_secret);
|
||||
session.insert("oauth_csrf_token", &csrf_secret)?;
|
||||
|
||||
// Log all session data for debugging
|
||||
log::info!("Session data after setting CSRF token:");
|
||||
|
||||
// Check if the CSRF token was actually stored
|
||||
if let Ok(Some(token)) = session.get::<String>("oauth_csrf_token") {
|
||||
log::info!(" Session key: oauth_csrf_token = {}", token);
|
||||
} else {
|
||||
log::warn!(" CSRF token not found in session after setting it!");
|
||||
}
|
||||
|
||||
// Check for other session keys
|
||||
if let Ok(Some(_)) = session.get::<String>("user") {
|
||||
log::info!(" Session key: user");
|
||||
}
|
||||
|
||||
if let Ok(Some(_)) = session.get::<String>("auth_token") {
|
||||
log::info!(" Session key: auth_token");
|
||||
}
|
||||
|
||||
// Also store it in a cookie as a backup
|
||||
let csrf_cookie = Cookie::build("oauth_csrf_token", csrf_secret.clone())
|
||||
.path("/")
|
||||
.http_only(true)
|
||||
.secure(false) // Set to true in production with HTTPS
|
||||
.max_age(actix_web::cookie::time::Duration::minutes(30))
|
||||
.finish();
|
||||
|
||||
// Store in a non-http-only cookie as well for debugging
|
||||
let csrf_cookie_debug = Cookie::build("oauth_csrf_token_debug", csrf_secret)
|
||||
.path("/")
|
||||
.http_only(false) // Accessible from JavaScript for debugging
|
||||
.secure(false)
|
||||
.max_age(actix_web::cookie::time::Duration::minutes(30))
|
||||
.finish();
|
||||
|
||||
// Redirect to the authorization URL
|
||||
Ok(HttpResponse::Found()
|
||||
.cookie(csrf_cookie)
|
||||
.cookie(csrf_cookie_debug)
|
||||
.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>,
|
||||
req: HttpRequest,
|
||||
) -> Result<impl Responder> {
|
||||
// Log all cookies for debugging
|
||||
log::info!("Cookies in request:");
|
||||
if let Ok(cookie_iter) = req.cookies() {
|
||||
for cookie in cookie_iter.iter() {
|
||||
log::info!(" Cookie: {}={}", cookie.name(), cookie.value());
|
||||
}
|
||||
} else {
|
||||
log::info!(" Failed to get cookies");
|
||||
}
|
||||
|
||||
// Log all session data for debugging
|
||||
log::info!("Session data in callback:");
|
||||
|
||||
// Check for CSRF token
|
||||
if let Ok(Some(token)) = session.get::<String>("oauth_csrf_token") {
|
||||
log::info!(" Session key: oauth_csrf_token = {}", token);
|
||||
} else {
|
||||
log::warn!(" CSRF token not found in session during callback!");
|
||||
}
|
||||
|
||||
// Check for other session keys
|
||||
if let Ok(Some(_)) = session.get::<String>("user") {
|
||||
log::info!(" Session key: user");
|
||||
}
|
||||
|
||||
if let Ok(Some(_)) = session.get::<String>("auth_token") {
|
||||
log::info!(" Session key: auth_token");
|
||||
}
|
||||
|
||||
// Try to get the CSRF token from the session
|
||||
let csrf_token_result = session.get::<String>("oauth_csrf_token")?;
|
||||
log::info!("CSRF token from session: {:?}", csrf_token_result);
|
||||
|
||||
// If not in session, try to get it from the cookie
|
||||
let csrf_token = match csrf_token_result {
|
||||
Some(token) => {
|
||||
log::info!("Found CSRF token in session: {}", token);
|
||||
token
|
||||
},
|
||||
None => {
|
||||
// Try to get from cookie
|
||||
match req.cookie("oauth_csrf_token") {
|
||||
Some(cookie) => {
|
||||
let token = cookie.value().to_string();
|
||||
log::info!("Found CSRF token in cookie: {}", token);
|
||||
token
|
||||
},
|
||||
None => {
|
||||
// For debugging, let's accept the state parameter directly
|
||||
log::warn!("CSRF token not found in session or cookie. Using state parameter as fallback.");
|
||||
log::warn!("State parameter: {}", query.state);
|
||||
query.state.clone()
|
||||
|
||||
// Uncomment this for production use
|
||||
// log::error!("CSRF token not found in session or cookie");
|
||||
// return Err(actix_web::error::ErrorBadRequest("Missing CSRF token"));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
log::info!("Comparing CSRF token: {} with state: {}", csrf_token, query.state);
|
||||
if csrf_token != query.state {
|
||||
log::warn!("CSRF token mismatch, but continuing for debugging purposes");
|
||||
// In production, uncomment the following:
|
||||
// log::error!("CSRF token mismatch");
|
||||
// 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);
|
||||
|
||||
log::info!("Gitea instance URL from config: {}", oauth_config.instance_url);
|
||||
|
||||
let access_token_secret = token.access_token().secret();
|
||||
log::info!("Using access token for /api/v1/user: {}", access_token_secret);
|
||||
|
||||
let response = client
|
||||
.get(&user_info_url)
|
||||
.bearer_auth(access_token_secret)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| actix_web::error::ErrorInternalServerError(format!("API request error: {}", e)))?;
|
||||
|
||||
let response_body = response.text().await.map_err(|e| actix_web::error::ErrorInternalServerError(format!("Failed to get response body: {}", e)))?;
|
||||
log::info!("Raw Gitea user info response: {}", response_body);
|
||||
|
||||
let gitea_user: crate::config::oauth::GiteaUser = serde_json::from_str(&response_body)
|
||||
.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();
|
||||
log::info!("Storing user in session: {}", user_json);
|
||||
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,
|
||||
}
|
73
src/controllers/home.rs
Normal file
73
src/controllers/home.rs
Normal file
@ -0,0 +1,73 @@
|
||||
use actix_web::{web, Result, Responder};
|
||||
use tera::Tera;
|
||||
use crate::utils::render_template;
|
||||
use actix_session::Session;
|
||||
use std::env;
|
||||
|
||||
/// 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");
|
||||
|
||||
let is_gitea_flow_active = env::var("GITEA_CLIENT_ID")
|
||||
.ok()
|
||||
.filter(|s| !s.is_empty())
|
||||
.is_some();
|
||||
ctx.insert("gitea_enabled", &is_gitea_flow_active);
|
||||
|
||||
// Add user to context if available
|
||||
if let Ok(Some(user_json)) = session.get::<String>("user") {
|
||||
// Keep the raw JSON for backward compatibility
|
||||
ctx.insert("user_json", &user_json);
|
||||
|
||||
// Parse the JSON into a User object
|
||||
match serde_json::from_str::<crate::models::user::User>(&user_json) {
|
||||
Ok(user) => {
|
||||
log::info!("Successfully parsed user: {:?}", user);
|
||||
ctx.insert("user", &user);
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("Failed to parse user JSON: {}", e);
|
||||
log::error!("User JSON: {}", user_json);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
let is_gitea_flow_active = env::var("GITEA_CLIENT_ID")
|
||||
.ok()
|
||||
.filter(|s| !s.is_empty())
|
||||
.is_some();
|
||||
ctx.insert("gitea_enabled", &is_gitea_flow_active);
|
||||
ctx.insert("active_page", "about");
|
||||
|
||||
// Add user to context if available
|
||||
if let Ok(Some(user_json)) = session.get::<String>("user") {
|
||||
// Keep the raw JSON for backward compatibility
|
||||
ctx.insert("user_json", &user_json);
|
||||
|
||||
// Parse the JSON into a User object
|
||||
match serde_json::from_str::<crate::models::user::User>(&user_json) {
|
||||
Ok(user) => {
|
||||
log::info!("Successfully parsed user: {:?}", user);
|
||||
ctx.insert("user", &user);
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("Failed to parse user JSON: {}", e);
|
||||
log::error!("User JSON: {}", user_json);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render_template(&tmpl, "home/about.html", &ctx)
|
||||
}
|
||||
}
|
5
src/controllers/mod.rs
Normal file
5
src/controllers/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
// Export controllers
|
||||
pub mod auth;
|
||||
pub mod debug;
|
||||
pub mod gitea_auth;
|
||||
pub mod home;
|
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;
|
55
src/models/user.rs
Normal file
55
src/models/user.rs
Normal file
@ -0,0 +1,55 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 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 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,
|
||||
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,
|
||||
}
|
||||
|
||||
/// Represents user registration data
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RegistrationData {
|
||||
pub name: String,
|
||||
pub email: String,
|
||||
}
|
71
src/routes/mod.rs
Normal file
71
src/routes/mod.rs
Normal file
@ -0,0 +1,71 @@
|
||||
use crate::config::oauth::GiteaOAuthConfig;
|
||||
use crate::controllers::auth::AuthController;
|
||||
use crate::controllers::debug::DebugController;
|
||||
use crate::controllers::gitea_auth::GiteaAuthController;
|
||||
use crate::controllers::home::HomeController;
|
||||
use crate::middleware::JwtAuth;
|
||||
use crate::SESSION_KEY;
|
||||
use actix_session::{storage::CookieSessionStore, SessionMiddleware};
|
||||
use actix_web::web;
|
||||
use std::env;
|
||||
|
||||
/// 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
|
||||
.cookie_http_only(true)
|
||||
.cookie_name("hostbasket_session".to_string())
|
||||
.cookie_path("/".to_string())
|
||||
.cookie_same_site(actix_web::cookie::SameSite::Lax) // Important for OAuth redirects
|
||||
.session_lifecycle(
|
||||
actix_session::config::PersistentSession::default()
|
||||
.session_ttl(actix_web::cookie::time::Duration::hours(2)),
|
||||
)
|
||||
.build();
|
||||
|
||||
// Build the main scope with common routes
|
||||
let mut main_scope = web::scope("")
|
||||
.wrap(session_middleware) // Wrap with session middleware
|
||||
// Home routes
|
||||
.route("/", web::get().to(HomeController::index))
|
||||
.route("/about", web::get().to(HomeController::about));
|
||||
|
||||
// Conditionally add authentication routes based on GITEA_CLIENT_ID environment variable
|
||||
if env::var("GITEA_CLIENT_ID").ok().filter(|s| !s.is_empty()).is_some() {
|
||||
// Use Gitea OAuth flow
|
||||
// Create the OAuth configuration and add it to the scope
|
||||
let oauth_config = web::Data::new(GiteaOAuthConfig::new());
|
||||
main_scope = main_scope
|
||||
.app_data(oauth_config) // Add oauth_config data
|
||||
// Gitea OAuth routes
|
||||
.route("/login", web::get().to(GiteaAuthController::login)) // Add /login route for gitea
|
||||
.route("/auth/gitea", web::get().to(GiteaAuthController::login))
|
||||
.route(
|
||||
"/auth/gitea/callback",
|
||||
web::get().to(GiteaAuthController::callback),
|
||||
);
|
||||
} else {
|
||||
// Use standard username/password login
|
||||
main_scope = main_scope
|
||||
.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));
|
||||
}
|
||||
|
||||
// Add common auth and debug routes (logout is common to both flows)
|
||||
main_scope = main_scope
|
||||
.route("/logout", web::get().to(AuthController::logout))
|
||||
// Debug routes
|
||||
.route("/debug", web::get().to(DebugController::debug_info));
|
||||
|
||||
// Register the main scope service
|
||||
cfg.service(main_scope);
|
||||
|
||||
// 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;
|
||||
}
|
71
src/static/debug.html
Normal file
71
src/static/debug.html
Normal file
@ -0,0 +1,71 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Debug Page</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 20px;
|
||||
}
|
||||
pre {
|
||||
background-color: #f5f5f5;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
button {
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Debug Page</h1>
|
||||
|
||||
<h2>Client-Side Cookies</h2>
|
||||
<pre id="cookies"></pre>
|
||||
|
||||
<h2>Debug API Response</h2>
|
||||
<button id="fetchDebug">Fetch Debug Info</button>
|
||||
<pre id="debugInfo"></pre>
|
||||
|
||||
<script>
|
||||
// Display client-side cookies
|
||||
function displayCookies() {
|
||||
const cookiesDiv = document.getElementById('cookies');
|
||||
const cookies = document.cookie.split(';').map(cookie => cookie.trim());
|
||||
|
||||
if (cookies.length === 0 || (cookies.length === 1 && cookies[0] === '')) {
|
||||
cookiesDiv.textContent = 'No cookies found';
|
||||
} else {
|
||||
const cookieObj = {};
|
||||
cookies.forEach(cookie => {
|
||||
const [name, value] = cookie.split('=');
|
||||
cookieObj[name] = value;
|
||||
});
|
||||
cookiesDiv.textContent = JSON.stringify(cookieObj, null, 2);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch debug info from API
|
||||
document.getElementById('fetchDebug').addEventListener('click', async () => {
|
||||
try {
|
||||
const response = await fetch('/debug');
|
||||
const data = await response.json();
|
||||
document.getElementById('debugInfo').textContent = JSON.stringify(data, null, 2);
|
||||
} catch (error) {
|
||||
document.getElementById('debugInfo').textContent = `Error: ${error.message}`;
|
||||
}
|
||||
});
|
||||
|
||||
// Initial display
|
||||
displayCookies();
|
||||
|
||||
// Update cookies display every 2 seconds
|
||||
setInterval(displayCookies, 2000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
1
src/static/images/gitea-logo.svg
Normal file
1
src/static/images/gitea-logo.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M256 32C132.3 32 32 132.3 32 256s100.3 224 224 224 224-100.3 224-224S379.7 32 256 32zm-32 256c0 17.7-14.3 32-32 32s-32-14.3-32-32 14.3-32 32-32 32 14.3 32 32zm128 0c0 17.7-14.3 32-32 32s-32-14.3-32-32 14.3-32 32-32 32 14.3 32 32zm-64-96c0 17.7-14.3 32-32 32s-32-14.3-32-32 14.3-32 32-32 32 14.3 32 32z" fill="#609926"/></svg>
|
After Width: | Height: | Size: 397 B |
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()))
|
||||
}
|
||||
}
|
74
src/views/auth/login.html
Normal file
74
src/views/auth/login.html
Normal file
@ -0,0 +1,74 @@
|
||||
{% 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">
|
||||
{% if not gitea_enabled %}
|
||||
<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>
|
||||
{% else %}
|
||||
|
||||
<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"
|
||||
style="margin-right: 5px"
|
||||
/>
|
||||
Login with Gitea
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
<hr />
|
||||
|
||||
{% if not gitea_enabled %}
|
||||
<div class="text-center">
|
||||
<p>
|
||||
Don't have an account?
|
||||
<a href="/register">Register</a>
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
75
src/views/auth/register.html
Normal file
75
src/views/auth/register.html
Normal file
@ -0,0 +1,75 @@
|
||||
{% 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 %}
|
87
src/views/base.html
Normal file
87
src/views/base.html
Normal file
@ -0,0 +1,87 @@
|
||||
<!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">
|
||||
<span class="nav-link">Hello, {% if user is defined %}{{ user.email }}{% else %}User{% endif
|
||||
%}</span>
|
||||
</li>
|
||||
<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>
|
||||
{% if gitea_enabled == false %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ active_class(current=active_page, page=" register") }}"
|
||||
href="/register">Register</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="py-4">
|
||||
<!-- Debug info (only visible during development) -->
|
||||
{% if user_json %}
|
||||
<div class="container mb-4" style="font-size: 0.8rem; color: #999;">
|
||||
<details>
|
||||
<summary>Debug Info</summary>
|
||||
<pre>user_json: {{ user_json }}</pre>
|
||||
<pre>user object exists: {{ user is defined }}</pre>
|
||||
<pre>is_gitea_flow_active: {{ gitea_enabled }}</pre>
|
||||
{% if user is defined %}
|
||||
<pre>user.name: {{ user.name }}</pre>
|
||||
{% endif %}
|
||||
</details>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% 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 %}
|
10
start.sh
Executable file
10
start.sh
Executable file
@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Get the directory of the script and change to it
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
export SECRET_KEY=1234
|
||||
export GITEA_CLIENT_ID=""
|
||||
export GITEA_CLIENT_SECRET=""
|
||||
export GITEA_INSTANCE_URL="https://git.ourworld.tf"
|
||||
cargo run
|
10
start_with_gitea.sh
Executable file
10
start_with_gitea.sh
Executable file
@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Get the directory of the script and change to it
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
export GITEA_CLIENT_ID="9f409b35-6258-4ac3-8370-05adc187c1f5"
|
||||
export GITEA_CLIENT_SECRET="gto_4s77ae33m5ernlf2423wx6wjyyqatqoe567rym7fcu3sqmu5azea"
|
||||
export GITEA_INSTANCE_URL="https://git.ourworld.tf"
|
||||
export APP_URL="http://localhost:9999"
|
||||
cargo run
|
Loading…
Reference in New Issue
Block a user