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:
Mahmoud Emad 2025-05-07 16:26:58 +03:00
parent fd624d2dae
commit 363a15776b
13 changed files with 628 additions and 44 deletions

30
.env.sample Normal file
View 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
View 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`

View File

@ -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
View 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))
}
}

View 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,
}

View File

@ -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)

View File

@ -1,3 +1,5 @@
// Export controllers
pub mod home;
pub mod auth;
pub mod debug;
pub mod gitea_auth;
pub mod home;

View File

@ -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
View 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>

View 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

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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>