feat: Add environment configuration and Gitea OAuth
This commit introduces environment configuration and Gitea OAuth authentication. - Added a `.env.sample` file for configuring server settings, database connection, authentication, and OAuth. This allows for easier customization and separation of configuration from code. - Implemented Gitea OAuth for user authentication. This provides a secure and convenient way for users to log in using their existing Gitea accounts. - Created a troubleshooting guide to help users resolve common issues, including authentication and server problems. This improves the overall user experience. - Added a debug controller and view to aid in development and troubleshooting. This provides developers with more tools to investigate issues. - Improved the user interface for login and registration. The changes include a cleaner design and clearer instructions. This enhances the user experience.
This commit is contained in:
		
							
								
								
									
										30
									
								
								.env.sample
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								.env.sample
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| # Server Configuration | ||||
| APP__SERVER__HOST=127.0.0.1 | ||||
| APP__SERVER__PORT=9999 | ||||
| APP__SERVER__WORKERS=4 | ||||
|  | ||||
| # Templates Configuration | ||||
| APP__TEMPLATES__DIR=./src/views | ||||
|  | ||||
| # Authentication | ||||
| JWT_SECRET=your_jwt_secret_key_change_this_in_production | ||||
| JWT_EXPIRATION_HOURS=24 | ||||
| # This must be at least 32 bytes long and should be a secure random string | ||||
| SECRET_KEY=01234567890123456789012345678901 | ||||
|  | ||||
| # OAuth Configuration - Gitea | ||||
| GITEA_CLIENT_ID=your_client_id | ||||
| GITEA_CLIENT_SECRET=your_client_secret | ||||
| GITEA_INSTANCE_URL=https://your-gitea-instance.com | ||||
| APP_URL=http://localhost:9999 | ||||
|  | ||||
| # Database Configuration | ||||
| APP__DATABASE__URL=postgres://user:password@localhost/hostbasket | ||||
| APP__DATABASE__POOL_SIZE=5 | ||||
|  | ||||
| # Logging | ||||
| RUST_LOG=debug | ||||
|  | ||||
| # Application Environment | ||||
| APP_ENV=development | ||||
| APP_CONFIG=config/local.toml | ||||
							
								
								
									
										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` | ||||
| @@ -32,57 +32,68 @@ impl AuthController { | ||||
|             UserRole::Admin => "admin", | ||||
|             UserRole::User => "user", | ||||
|         }; | ||||
|          | ||||
|  | ||||
|         let expiration = Utc::now() | ||||
|             .checked_add_signed(Duration::hours(24)) | ||||
|             .expect("valid timestamp") | ||||
|             .timestamp() as usize; | ||||
|              | ||||
|  | ||||
|         let claims = Claims { | ||||
|             sub: email.to_owned(), | ||||
|             exp: expiration, | ||||
|             iat: Utc::now().timestamp() as usize, | ||||
|             role: role_str.to_string(), | ||||
|         }; | ||||
|          | ||||
|  | ||||
|         encode( | ||||
|             &Header::default(), | ||||
|             &claims, | ||||
|             &EncodingKey::from_secret(JWT_SECRET.as_bytes()) | ||||
|         ) | ||||
|     } | ||||
|      | ||||
|  | ||||
|     /// Validate a JWT token | ||||
|     pub fn validate_token(token: &str) -> Result<Claims, jsonwebtoken::errors::Error> { | ||||
|         let validation = Validation::new(Algorithm::HS256); | ||||
|          | ||||
|  | ||||
|         let token_data = decode::<Claims>( | ||||
|             token, | ||||
|             &DecodingKey::from_secret(JWT_SECRET.as_bytes()), | ||||
|             &validation | ||||
|         )?; | ||||
|          | ||||
|  | ||||
|         Ok(token_data.claims) | ||||
|     } | ||||
|      | ||||
|  | ||||
|     /// Extract token from session | ||||
|     pub fn extract_token_from_session(session: &Session) -> Option<String> { | ||||
|         session.get::<String>("auth_token").ok().flatten() | ||||
|     } | ||||
|      | ||||
|  | ||||
|     /// Extract token from cookie | ||||
|     pub fn extract_token_from_cookie(req: &actix_web::HttpRequest) -> Option<String> { | ||||
|         req.cookie("auth_token").map(|c| c.value().to_string()) | ||||
|     } | ||||
|      | ||||
|  | ||||
|     /// Renders the login page | ||||
|     pub async fn login_page(tmpl: web::Data<Tera>) -> Result<impl Responder> { | ||||
|     pub async fn login_page(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> { | ||||
|         let mut ctx = tera::Context::new(); | ||||
|         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 | ||||
|             if let Ok(user) = serde_json::from_str::<User>(&user_json) { | ||||
|                 ctx.insert("user", &user); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         render_template(&tmpl, "auth/login.html", &ctx) | ||||
|     } | ||||
|      | ||||
|  | ||||
|     /// Handles user login | ||||
|     pub async fn login( | ||||
|         form: web::Form<LoginCredentials>, | ||||
| @@ -95,20 +106,20 @@ impl AuthController { | ||||
|             "Admin User".to_string(), | ||||
|             form.email.clone() | ||||
|         ); | ||||
|          | ||||
|  | ||||
|         // Set the ID and admin role | ||||
|         test_user.id = Some(1); | ||||
|         test_user.role = UserRole::Admin; | ||||
|          | ||||
|  | ||||
|         // Generate JWT token | ||||
|         let token = Self::generate_token(&test_user.email, &test_user.role) | ||||
|             .map_err(|_| actix_web::error::ErrorInternalServerError("Failed to generate token"))?; | ||||
|          | ||||
|  | ||||
|         // Store user data in session | ||||
|         let user_json = serde_json::to_string(&test_user).unwrap(); | ||||
|         session.insert("user", &user_json)?; | ||||
|         session.insert("auth_token", &token)?; | ||||
|          | ||||
|  | ||||
|         // Create a cookie with the JWT token | ||||
|         let cookie = Cookie::build("auth_token", token) | ||||
|             .path("/") | ||||
| @@ -116,22 +127,33 @@ impl AuthController { | ||||
|             .secure(false) // Set to true in production with HTTPS | ||||
|             .max_age(actix_web::cookie::time::Duration::hours(24)) | ||||
|             .finish(); | ||||
|          | ||||
|  | ||||
|         // Redirect to the home page with JWT token in cookie | ||||
|         Ok(HttpResponse::Found() | ||||
|             .cookie(cookie) | ||||
|             .append_header((header::LOCATION, "/")) | ||||
|             .finish()) | ||||
|     } | ||||
|      | ||||
|  | ||||
|     /// Renders the registration page | ||||
|     pub async fn register_page(tmpl: web::Data<Tera>) -> Result<impl Responder> { | ||||
|     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"); | ||||
|          | ||||
|  | ||||
|         // 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 | ||||
|             if let Ok(user) = serde_json::from_str::<User>(&user_json) { | ||||
|                 ctx.insert("user", &user); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         render_template(&tmpl, "auth/register.html", &ctx) | ||||
|     } | ||||
|      | ||||
|  | ||||
|     /// Handles user registration | ||||
|     pub async fn register( | ||||
|         form: web::Form<RegistrationData>, | ||||
| @@ -143,20 +165,20 @@ impl AuthController { | ||||
|             form.name.clone(), | ||||
|             form.email.clone() | ||||
|         ); | ||||
|          | ||||
|  | ||||
|         // Set the ID and admin role | ||||
|         user.id = Some(1); | ||||
|         user.role = UserRole::Admin; | ||||
|          | ||||
|  | ||||
|         // Generate JWT token | ||||
|         let token = Self::generate_token(&user.email, &user.role) | ||||
|             .map_err(|_| actix_web::error::ErrorInternalServerError("Failed to generate token"))?; | ||||
|          | ||||
|  | ||||
|         // Store user data in session | ||||
|         let user_json = serde_json::to_string(&user).unwrap(); | ||||
|         session.insert("user", &user_json)?; | ||||
|         session.insert("auth_token", &token)?; | ||||
|          | ||||
|  | ||||
|         // Create a cookie with the JWT token | ||||
|         let cookie = Cookie::build("auth_token", token) | ||||
|             .path("/") | ||||
| @@ -164,26 +186,26 @@ impl AuthController { | ||||
|             .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) | ||||
|   | ||||
							
								
								
									
										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)) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										214
									
								
								src/controllers/gitea_auth.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										214
									
								
								src/controllers/gitea_auth.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,214 @@ | ||||
| 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); | ||||
|  | ||||
|         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(); | ||||
|         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, | ||||
| } | ||||
| @@ -13,8 +13,14 @@ impl HomeController { | ||||
|         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); | ||||
|         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 | ||||
|             if let Ok(user) = serde_json::from_str::<crate::models::user::User>(&user_json) { | ||||
|                 ctx.insert("user", &user); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         render_template(&tmpl, "home/index.html", &ctx) | ||||
| @@ -26,8 +32,14 @@ impl HomeController { | ||||
|         ctx.insert("active_page", "about"); | ||||
|  | ||||
|         // Add user to context if available | ||||
|         if let Ok(Some(user)) = session.get::<String>("user") { | ||||
|             ctx.insert("user_json", &user); | ||||
|         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 | ||||
|             if let Ok(user) = serde_json::from_str::<crate::models::user::User>(&user_json) { | ||||
|                 ctx.insert("user", &user); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         render_template(&tmpl, "home/about.html", &ctx) | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| // Export controllers | ||||
| pub mod home; | ||||
| pub mod auth; | ||||
| pub mod debug; | ||||
| pub mod gitea_auth; | ||||
| pub mod home; | ||||
|   | ||||
| @@ -1,4 +1,7 @@ | ||||
| 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; | ||||
| @@ -7,16 +10,28 @@ use actix_web::web; | ||||
|  | ||||
| /// Configures all application routes | ||||
| 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 | ||||
|             .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(); | ||||
|  | ||||
|     // Public routes that don't require authentication | ||||
|     cfg.service( | ||||
|         web::scope("") | ||||
|             .wrap(session_middleware) | ||||
|             .app_data(oauth_config.clone()) | ||||
|             // Home routes | ||||
|             .route("/", web::get().to(HomeController::index)) | ||||
|             .route("/about", web::get().to(HomeController::about)) | ||||
| @@ -25,7 +40,15 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) { | ||||
|             .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)), | ||||
|             .route("/logout", web::get().to(AuthController::logout)) | ||||
|             // Gitea OAuth routes | ||||
|             .route("/auth/gitea", web::get().to(GiteaAuthController::login)) | ||||
|             .route( | ||||
|                 "/auth/gitea/callback", | ||||
|                 web::get().to(GiteaAuthController::callback), | ||||
|             ) | ||||
|             // Debug routes | ||||
|             .route("/debug", web::get().to(DebugController::debug_info)), | ||||
|     ); | ||||
|  | ||||
|     // Protected routes that require authentication | ||||
|   | ||||
							
								
								
									
										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 | 
| @@ -22,18 +22,20 @@ | ||||
|                         </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" | ||||
|                                 style="margin-right: 5px;"> | ||||
|                             Login with Gitea | ||||
|                         </a> | ||||
|                     </div> | ||||
|                      | ||||
|  | ||||
|                     <hr> | ||||
|                      | ||||
|  | ||||
|                     <div class="text-center"> | ||||
|                         <p>Don't have an account? <a href="/register">Register</a></p> | ||||
|                     </div> | ||||
| @@ -42,4 +44,4 @@ | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| {% endblock %} | ||||
| {% endblock %} | ||||
| @@ -26,13 +26,25 @@ | ||||
|                         </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> | ||||
|                             <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>Or register 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;"> | ||||
|                             Register with Gitea | ||||
|                         </a> | ||||
|                     </div> | ||||
|  | ||||
|                     <hr> | ||||
|  | ||||
|                     <div class="text-center"> | ||||
|                         <p>Already have an account? <a href="/login">Login</a></p> | ||||
|                     </div> | ||||
| @@ -41,4 +53,4 @@ | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| {% endblock %} | ||||
| {% endblock %} | ||||
| @@ -31,6 +31,9 @@ | ||||
|                 </ul> | ||||
|                 <ul class="navbar-nav"> | ||||
|                     {% if user_json %} | ||||
|                     <li class="nav-item"> | ||||
|                         <span class="nav-link">Hello, {{ user.name }}</span> | ||||
|                     </li> | ||||
|                     <li class="nav-item"> | ||||
|                         <a class="nav-link" href="/logout">Logout</a> | ||||
|                     </li> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user