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`
|
@ -76,10 +76,21 @@ impl AuthController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Renders the login page
|
/// 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();
|
let mut ctx = tera::Context::new();
|
||||||
ctx.insert("active_page", "login");
|
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)
|
render_template(&tmpl, "auth/login.html", &ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,10 +136,21 @@ impl AuthController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Renders the registration page
|
/// 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();
|
let mut ctx = tera::Context::new();
|
||||||
ctx.insert("active_page", "register");
|
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)
|
render_template(&tmpl, "auth/register.html", &ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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");
|
ctx.insert("active_page", "home");
|
||||||
|
|
||||||
// Add user to context if available
|
// Add user to context if available
|
||||||
if let Ok(Some(user)) = session.get::<String>("user") {
|
if let Ok(Some(user_json)) = session.get::<String>("user") {
|
||||||
ctx.insert("user_json", &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)
|
render_template(&tmpl, "home/index.html", &ctx)
|
||||||
@ -26,8 +32,14 @@ impl HomeController {
|
|||||||
ctx.insert("active_page", "about");
|
ctx.insert("active_page", "about");
|
||||||
|
|
||||||
// Add user to context if available
|
// Add user to context if available
|
||||||
if let Ok(Some(user)) = session.get::<String>("user") {
|
if let Ok(Some(user_json)) = session.get::<String>("user") {
|
||||||
ctx.insert("user_json", &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)
|
render_template(&tmpl, "home/about.html", &ctx)
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
// Export controllers
|
// Export controllers
|
||||||
pub mod home;
|
|
||||||
pub mod auth;
|
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::auth::AuthController;
|
||||||
|
use crate::controllers::debug::DebugController;
|
||||||
|
use crate::controllers::gitea_auth::GiteaAuthController;
|
||||||
use crate::controllers::home::HomeController;
|
use crate::controllers::home::HomeController;
|
||||||
use crate::middleware::JwtAuth;
|
use crate::middleware::JwtAuth;
|
||||||
use crate::SESSION_KEY;
|
use crate::SESSION_KEY;
|
||||||
@ -7,16 +10,28 @@ use actix_web::web;
|
|||||||
|
|
||||||
/// Configures all application routes
|
/// Configures all application routes
|
||||||
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
|
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
|
// Configure session middleware with the consistent key
|
||||||
let session_middleware =
|
let session_middleware =
|
||||||
SessionMiddleware::builder(CookieSessionStore::default(), SESSION_KEY.clone())
|
SessionMiddleware::builder(CookieSessionStore::default(), SESSION_KEY.clone())
|
||||||
.cookie_secure(false) // Set to true in production with HTTPS
|
.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();
|
.build();
|
||||||
|
|
||||||
// Public routes that don't require authentication
|
// Public routes that don't require authentication
|
||||||
cfg.service(
|
cfg.service(
|
||||||
web::scope("")
|
web::scope("")
|
||||||
.wrap(session_middleware)
|
.wrap(session_middleware)
|
||||||
|
.app_data(oauth_config.clone())
|
||||||
// Home routes
|
// Home routes
|
||||||
.route("/", web::get().to(HomeController::index))
|
.route("/", web::get().to(HomeController::index))
|
||||||
.route("/about", web::get().to(HomeController::about))
|
.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("/login", web::post().to(AuthController::login))
|
||||||
.route("/register", web::get().to(AuthController::register_page))
|
.route("/register", web::get().to(AuthController::register_page))
|
||||||
.route("/register", web::post().to(AuthController::register))
|
.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
|
// 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 |
@ -28,6 +28,8 @@
|
|||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<p>Or login with:</p>
|
<p>Or login with:</p>
|
||||||
<a href="/auth/gitea" class="btn btn-secondary">
|
<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
|
Login with Gitea
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,13 +26,25 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="password_confirmation" class="form-label">Confirm Password</label>
|
<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>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary">Register</button>
|
<button type="submit" class="btn btn-primary">Register</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<hr>
|
<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">
|
<div class="text-center">
|
||||||
<p>Already have an account? <a href="/login">Login</a></p>
|
<p>Already have an account? <a href="/login">Login</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -31,6 +31,9 @@
|
|||||||
</ul>
|
</ul>
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
{% if user_json %}
|
{% if user_json %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<span class="nav-link">Hello, {{ user.name }}</span>
|
||||||
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="/logout">Logout</a>
|
<a class="nav-link" href="/logout">Logout</a>
|
||||||
</li>
|
</li>
|
||||||
|
Loading…
Reference in New Issue
Block a user