diff --git a/docs/gitea-auth.md b/docs/gitea-auth.md index 3ac5741..62affbe 100644 --- a/docs/gitea-auth.md +++ b/docs/gitea-auth.md @@ -2,16 +2,6 @@ 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. @@ -27,7 +17,7 @@ Before you begin, ensure you have: ## Setting Up Gitea OAuth Application 1. Log in to your Gitea instance as an administrator -2. Navigate to **Site Administration** > **Applications** +2. Navigate to **Site Administration** > **Integrations** > **Applications** 3. Click **Create a New OAuth2 Application** 4. Fill in the application details: - **Application Name**: Hostbasket @@ -96,11 +86,11 @@ impl GiteaOAuthConfig { .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)), @@ -110,7 +100,7 @@ impl GiteaOAuthConfig { .set_redirect_uri( RedirectUrl::new("http://localhost:9999/auth/gitea/callback".to_string()).unwrap(), ); - + Self { client, instance_url, @@ -158,16 +148,16 @@ impl GiteaAuthController { .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, @@ -177,11 +167,11 @@ impl GiteaAuthController { // Verify the CSRF token let csrf_token = session.get::("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 @@ -189,11 +179,11 @@ impl GiteaAuthController { .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()) @@ -203,26 +193,26 @@ impl GiteaAuthController { .json::() .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("/") @@ -230,7 +220,7 @@ impl GiteaAuthController { .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) @@ -258,7 +248,7 @@ 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(), @@ -273,7 +263,7 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) { .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)) @@ -310,9 +300,9 @@ Update your login page template (`src/views/auth/login.html`) to include a "Logi - +
- +

Or login with:

diff --git a/src/config/oauth.rs b/src/config/oauth.rs index 321c0a5..eb62d91 100644 --- a/src/config/oauth.rs +++ b/src/config/oauth.rs @@ -1,12 +1,8 @@ -#[cfg(feature = "gitea")] +use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl}; use serde::{Deserialize, Serialize}; use std::env; -#[cfg(feature = "gitea")] -use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl}; - /// Gitea OAuth configuration -#[cfg(feature = "gitea")] #[derive(Clone, Debug)] pub struct GiteaOAuthConfig { /// OAuth client @@ -15,7 +11,6 @@ pub struct GiteaOAuthConfig { pub instance_url: String, } -#[cfg(feature = "gitea")] impl GiteaOAuthConfig { /// Creates a new Gitea OAuth configuration pub fn new() -> Self { @@ -53,7 +48,6 @@ impl GiteaOAuthConfig { } /// Gitea user information structure -#[cfg(feature = "gitea")] #[derive(Debug, Deserialize, Serialize)] pub struct GiteaUser { /// User ID diff --git a/src/controllers/auth.rs b/src/controllers/auth.rs index 90eff2e..0bb3454 100644 --- a/src/controllers/auth.rs +++ b/src/controllers/auth.rs @@ -1,12 +1,13 @@ -use actix_web::{web, HttpResponse, Responder, Result, http::header, cookie::Cookie}; -use actix_session::Session; -use tera::Tera; -use crate::models::user::{User, LoginCredentials, RegistrationData, UserRole}; +use crate::models::user::{LoginCredentials, RegistrationData, User, UserRole}; use crate::utils::render_template; -use jsonwebtoken::{encode, Header, EncodingKey}; -use serde::{Deserialize, Serialize}; -use chrono::{Utc, Duration}; +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)] @@ -19,7 +20,8 @@ pub struct Claims { // JWT Secret key lazy_static! { - static ref JWT_SECRET: String = std::env::var("JWT_SECRET").unwrap_or_else(|_| "your_jwt_secret_key".to_string()); + 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 @@ -27,7 +29,10 @@ pub struct AuthController; impl AuthController { /// Generate a JWT token for a user - pub fn generate_token(email: &str, role: &UserRole) -> Result { + pub fn generate_token( + email: &str, + role: &UserRole, + ) -> Result { let role_str = match role { UserRole::Admin => "admin", UserRole::User => "user", @@ -48,7 +53,7 @@ impl AuthController { encode( &Header::default(), &claims, - &EncodingKey::from_secret(JWT_SECRET.as_bytes()) + &EncodingKey::from_secret(JWT_SECRET.as_bytes()), ) } @@ -61,8 +66,12 @@ impl AuthController { /// Renders the login page pub async fn login_page(tmpl: web::Data, session: Session) -> Result { 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::("user") { // Keep the raw JSON for backward compatibility @@ -73,7 +82,7 @@ impl AuthController { 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); @@ -88,14 +97,11 @@ impl AuthController { pub async fn login( form: web::Form, session: Session, - _tmpl: web::Data + _tmpl: web::Data, ) -> Result { // 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() - ); + let mut test_user = User::new("Admin User".to_string(), form.email.clone()); // Set the ID and admin role test_user.id = Some(1); @@ -132,6 +138,12 @@ impl AuthController { 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::("user") { // Keep the raw JSON for backward compatibility @@ -142,7 +154,7 @@ impl AuthController { 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); @@ -157,13 +169,10 @@ impl AuthController { pub async fn register( form: web::Form, session: Session, - _tmpl: web::Data + _tmpl: web::Data, ) -> Result { // Skip validation and always create an admin user - let mut user = User::new( - form.name.clone(), - form.email.clone() - ); + let mut user = User::new(form.name.clone(), form.email.clone()); // Set the ID and admin role user.id = Some(1); diff --git a/src/controllers/gitea_auth.rs b/src/controllers/gitea_auth.rs index 6af9903..f2c89b0 100644 --- a/src/controllers/gitea_auth.rs +++ b/src/controllers/gitea_auth.rs @@ -1,21 +1,15 @@ 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; -#[cfg(feature = "gitea")] -use oauth2::{AuthorizationCode, CsrfToken, Scope, TokenResponse}; -#[cfg(feature = "gitea")] -use reqwest::Client; -#[cfg(feature = "gitea")] -use crate::config::oauth::GiteaOAuthConfig; - /// Controller for handling Gitea authentication -#[cfg(feature = "gitea")] pub struct GiteaAuthController; -#[cfg(feature = "gitea")] impl GiteaAuthController { /// Initiate the OAuth flow pub async fn login( @@ -166,14 +160,22 @@ impl GiteaAuthController { let client = Client::new(); let user_info_url = format!("{}/api/v1/user", oauth_config.instance_url); - let gitea_user = client + 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(token.access_token().secret()) + .bearer_auth(access_token_secret) .send() .await - .map_err(|e| actix_web::error::ErrorInternalServerError(format!("API request error: {}", e)))? - .json::() - .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 @@ -213,7 +215,6 @@ impl GiteaAuthController { } /// Query parameters for the OAuth callback -#[cfg(feature = "gitea")] #[derive(serde::Deserialize)] pub struct CallbackQuery { pub code: String, diff --git a/src/controllers/home.rs b/src/controllers/home.rs index 5eb7796..8ea2be5 100644 --- a/src/controllers/home.rs +++ b/src/controllers/home.rs @@ -2,6 +2,7 @@ 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; @@ -12,6 +13,12 @@ impl HomeController { 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::("user") { // Keep the raw JSON for backward compatibility @@ -36,6 +43,11 @@ impl HomeController { /// Renders the about page pub async fn about(tmpl: web::Data, session: Session) -> Result { 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 diff --git a/src/controllers/mod.rs b/src/controllers/mod.rs index 3d5604b..f7d1d35 100644 --- a/src/controllers/mod.rs +++ b/src/controllers/mod.rs @@ -1,6 +1,5 @@ // Export controllers pub mod auth; pub mod debug; -#[cfg(feature = "gitea")] pub mod gitea_auth; pub mod home; diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 6cf1dfc..29d269a 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,14 +1,13 @@ -#[cfg(feature = "gitea")] use crate::config::oauth::GiteaOAuthConfig; use crate::controllers::auth::AuthController; use crate::controllers::debug::DebugController; -#[cfg(feature = "gitea")] 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) { @@ -26,35 +25,44 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) { ) .build(); - let mut scope = web::scope("") - .wrap(session_middleware) + // 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)) - // Auth routes - .route("/login", web::get().to(AuthController::login_page)) - .route("/login", web::post().to(AuthController::login)) - .route("/register", web::get().to(AuthController::register_page)) - .route("/register", web::post().to(AuthController::register)) - .route("/logout", web::get().to(AuthController::logout)) - // Debug routes - .route("/debug", web::get().to(DebugController::debug_info)); + .route("/about", web::get().to(HomeController::about)); - #[cfg(feature = "gitea")] - { - // Create the OAuth configuration + // 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()); - // Gitea OAuth configuration and routes - scope = scope - .app_data(oauth_config.clone()) + 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)); } - cfg.service(scope); + // 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( diff --git a/src/views/auth/login.html b/src/views/auth/login.html index 6a913e8..4ce348d 100644 --- a/src/views/auth/login.html +++ b/src/views/auth/login.html @@ -1,8 +1,5 @@ -{% extends "base.html" %} - -{% block title %}Login - Hostbasket{% endblock %} - -{% block content %} +{% extends "base.html" %} {% block title %}Login - Hostbasket{% endblock %} {% +block content %}
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/views/auth/register.html b/src/views/auth/register.html index 06e5227..39a52fd 100644 --- a/src/views/auth/register.html +++ b/src/views/auth/register.html @@ -1,7 +1,4 @@ -{% extends "base.html" %} - -{% block title %}Register - Hostbasket{% endblock %} - +{% extends "base.html" %} {% block title %}Register - Hostbasket{% endblock %} {% block content %}
@@ -14,45 +11,65 @@
- +
- +
- - + +
- - + +
- +
-
- - {% if gitea_enabled %} -
-

Or register with:

- - Gitea - Register with Gitea - -
- {% endif %} - -
+
-

Already have an account? Login

+

+ Already have an account? Login +

-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/views/base.html b/src/views/base.html index fb0626b..a176b07 100644 --- a/src/views/base.html +++ b/src/views/base.html @@ -43,11 +43,13 @@ Login + {% if gitea_enabled == false %} {% endif %} + {% endif %} @@ -61,6 +63,7 @@ Debug Info
user_json: {{ user_json }}
user object exists: {{ user is defined }}
+
is_gitea_flow_active: {{ gitea_enabled }}
{% if user is defined %}
user.name: {{ user.name }}
{% endif %} @@ -81,4 +84,4 @@ {% block scripts %}{% endblock %} - \ No newline at end of file + diff --git a/start.sh b/start.sh old mode 100644 new mode 100755 index c44f8c2..0a8de12 --- a/start.sh +++ b/start.sh @@ -4,5 +4,7 @@ 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 diff --git a/start_with_gitea.sh b/start_with_gitea.sh index 0a9073f..efd1f0a 100755 --- a/start_with_gitea.sh +++ b/start_with_gitea.sh @@ -3,8 +3,8 @@ # Get the directory of the script and change to it cd "$(dirname "$0")" -export GITEA_CLIENT_ID="your_client_id" -export GITEA_CLIENT_SECRET="your_client_secret" -export GITEA_INSTANCE_URL="https://gitea.example.com" +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 --features gitea +cargo run