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