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:
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,
|
||||
}
|
||||
Reference in New Issue
Block a user