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:
parent
fd624d2dae
commit
363a15776b
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>
|
||||
|
Loading…
Reference in New Issue
Block a user