This commit is contained in:
despiegk 2025-05-08 08:05:19 +03:00
parent 4274fdaf93
commit 433606eb25
12 changed files with 207 additions and 147 deletions

View File

@ -2,16 +2,6 @@
This guide provides detailed instructions on how to integrate Gitea authentication into your Hostbasket application. This guide provides detailed instructions on how to integrate Gitea authentication into your Hostbasket application.
## Table of Contents
1. [Introduction](#introduction)
2. [Prerequisites](#prerequisites)
3. [Setting Up Gitea OAuth Application](#setting-up-gitea-oauth-application)
4. [Configuring Hostbasket for Gitea Authentication](#configuring-hostbasket-for-gitea-authentication)
5. [Implementing the OAuth Flow](#implementing-the-oauth-flow)
6. [Testing the Integration](#testing-the-integration)
7. [Troubleshooting](#troubleshooting)
## Introduction ## Introduction
Gitea is a self-hosted Git service that provides a GitHub-like interface. By integrating Gitea authentication into your Hostbasket application, you can allow users to log in using their Gitea accounts, simplifying the authentication process and providing a seamless experience. Gitea is a self-hosted Git service that provides a GitHub-like interface. By integrating Gitea authentication into your Hostbasket application, you can allow users to log in using their Gitea accounts, simplifying the authentication process and providing a seamless experience.
@ -27,7 +17,7 @@ Before you begin, ensure you have:
## Setting Up Gitea OAuth Application ## Setting Up Gitea OAuth Application
1. Log in to your Gitea instance as an administrator 1. Log in to your Gitea instance as an administrator
2. Navigate to **Site Administration** > **Applications** 2. Navigate to **Site Administration** > **Integrations** > **Applications**
3. Click **Create a New OAuth2 Application** 3. Click **Create a New OAuth2 Application**
4. Fill in the application details: 4. Fill in the application details:
- **Application Name**: Hostbasket - **Application Name**: Hostbasket

View File

@ -1,12 +1,8 @@
#[cfg(feature = "gitea")] use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::env; use std::env;
#[cfg(feature = "gitea")]
use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl};
/// Gitea OAuth configuration /// Gitea OAuth configuration
#[cfg(feature = "gitea")]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct GiteaOAuthConfig { pub struct GiteaOAuthConfig {
/// OAuth client /// OAuth client
@ -15,7 +11,6 @@ pub struct GiteaOAuthConfig {
pub instance_url: String, pub instance_url: String,
} }
#[cfg(feature = "gitea")]
impl GiteaOAuthConfig { impl GiteaOAuthConfig {
/// Creates a new Gitea OAuth configuration /// Creates a new Gitea OAuth configuration
pub fn new() -> Self { pub fn new() -> Self {
@ -53,7 +48,6 @@ impl GiteaOAuthConfig {
} }
/// Gitea user information structure /// Gitea user information structure
#[cfg(feature = "gitea")]
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct GiteaUser { pub struct GiteaUser {
/// User ID /// User ID

View File

@ -1,12 +1,13 @@
use actix_web::{web, HttpResponse, Responder, Result, http::header, cookie::Cookie}; use crate::models::user::{LoginCredentials, RegistrationData, User, UserRole};
use actix_session::Session;
use tera::Tera;
use crate::models::user::{User, LoginCredentials, RegistrationData, UserRole};
use crate::utils::render_template; use crate::utils::render_template;
use jsonwebtoken::{encode, Header, EncodingKey}; use actix_session::Session;
use serde::{Deserialize, Serialize}; use actix_web::{cookie::Cookie, http::header, web, HttpResponse, Responder, Result};
use chrono::{Utc, Duration}; use chrono::{Duration, Utc};
use jsonwebtoken::{encode, EncodingKey, Header};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use std::env;
use tera::Tera;
// JWT Claims structure // JWT Claims structure
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
@ -19,7 +20,8 @@ pub struct Claims {
// JWT Secret key // JWT Secret key
lazy_static! { lazy_static! {
static ref JWT_SECRET: String = std::env::var("JWT_SECRET").unwrap_or_else(|_| "your_jwt_secret_key".to_string()); static ref JWT_SECRET: String =
std::env::var("JWT_SECRET").unwrap_or_else(|_| "your_jwt_secret_key".to_string());
} }
/// Controller for handling authentication-related routes /// Controller for handling authentication-related routes
@ -27,7 +29,10 @@ pub struct AuthController;
impl AuthController { impl AuthController {
/// Generate a JWT token for a user /// Generate a JWT token for a user
pub fn generate_token(email: &str, role: &UserRole) -> Result<String, jsonwebtoken::errors::Error> { pub fn generate_token(
email: &str,
role: &UserRole,
) -> Result<String, jsonwebtoken::errors::Error> {
let role_str = match role { let role_str = match role {
UserRole::Admin => "admin", UserRole::Admin => "admin",
UserRole::User => "user", UserRole::User => "user",
@ -48,7 +53,7 @@ impl AuthController {
encode( encode(
&Header::default(), &Header::default(),
&claims, &claims,
&EncodingKey::from_secret(JWT_SECRET.as_bytes()) &EncodingKey::from_secret(JWT_SECRET.as_bytes()),
) )
} }
@ -61,8 +66,12 @@ impl AuthController {
/// Renders the login page /// Renders the login page
pub async fn login_page(tmpl: web::Data<Tera>, session: Session) -> 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();
let is_gitea_flow_active = env::var("GITEA_CLIENT_ID")
.ok()
.filter(|s| !s.is_empty())
.is_some();
ctx.insert("gitea_enabled", &is_gitea_flow_active);
ctx.insert("active_page", "login"); ctx.insert("active_page", "login");
// Add user to context if available // Add user to context if available
if let Ok(Some(user_json)) = session.get::<String>("user") { if let Ok(Some(user_json)) = session.get::<String>("user") {
// Keep the raw JSON for backward compatibility // Keep the raw JSON for backward compatibility
@ -73,7 +82,7 @@ impl AuthController {
Ok(user) => { Ok(user) => {
log::info!("Successfully parsed user in login_page: {:?}", user); log::info!("Successfully parsed user in login_page: {:?}", user);
ctx.insert("user", &user); ctx.insert("user", &user);
}, }
Err(e) => { Err(e) => {
log::error!("Failed to parse user JSON in login_page: {}", e); log::error!("Failed to parse user JSON in login_page: {}", e);
log::error!("User JSON: {}", user_json); log::error!("User JSON: {}", user_json);
@ -88,14 +97,11 @@ impl AuthController {
pub async fn login( pub async fn login(
form: web::Form<LoginCredentials>, form: web::Form<LoginCredentials>,
session: Session, session: Session,
_tmpl: web::Data<Tera> _tmpl: web::Data<Tera>,
) -> Result<impl Responder> { ) -> Result<impl Responder> {
// For simplicity, always log in the user without checking credentials // For simplicity, always log in the user without checking credentials
// Create a user object with admin role // Create a user object with admin role
let mut test_user = User::new( let mut test_user = User::new("Admin User".to_string(), form.email.clone());
"Admin User".to_string(),
form.email.clone()
);
// Set the ID and admin role // Set the ID and admin role
test_user.id = Some(1); test_user.id = Some(1);
@ -132,6 +138,12 @@ impl AuthController {
let mut ctx = tera::Context::new(); let mut ctx = tera::Context::new();
ctx.insert("active_page", "register"); ctx.insert("active_page", "register");
let is_gitea_flow_active = env::var("GITEA_CLIENT_ID")
.ok()
.filter(|s| !s.is_empty())
.is_some();
ctx.insert("gitea_enabled", &is_gitea_flow_active);
// Add user to context if available // Add user to context if available
if let Ok(Some(user_json)) = session.get::<String>("user") { if let Ok(Some(user_json)) = session.get::<String>("user") {
// Keep the raw JSON for backward compatibility // Keep the raw JSON for backward compatibility
@ -142,7 +154,7 @@ impl AuthController {
Ok(user) => { Ok(user) => {
log::info!("Successfully parsed user in register_page: {:?}", user); log::info!("Successfully parsed user in register_page: {:?}", user);
ctx.insert("user", &user); ctx.insert("user", &user);
}, }
Err(e) => { Err(e) => {
log::error!("Failed to parse user JSON in register_page: {}", e); log::error!("Failed to parse user JSON in register_page: {}", e);
log::error!("User JSON: {}", user_json); log::error!("User JSON: {}", user_json);
@ -157,13 +169,10 @@ impl AuthController {
pub async fn register( pub async fn register(
form: web::Form<RegistrationData>, form: web::Form<RegistrationData>,
session: Session, session: Session,
_tmpl: web::Data<Tera> _tmpl: web::Data<Tera>,
) -> Result<impl Responder> { ) -> Result<impl Responder> {
// Skip validation and always create an admin user // Skip validation and always create an admin user
let mut user = User::new( let mut user = User::new(form.name.clone(), form.email.clone());
form.name.clone(),
form.email.clone()
);
// Set the ID and admin role // Set the ID and admin role
user.id = Some(1); user.id = Some(1);

View File

@ -1,21 +1,15 @@
use actix_web::{web, HttpRequest, HttpResponse, Responder, Result, http::header, cookie::Cookie}; use actix_web::{web, HttpRequest, HttpResponse, Responder, Result, http::header, cookie::Cookie};
use actix_session::Session; 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::models::user::{User, UserRole};
use crate::controllers::auth::AuthController; use crate::controllers::auth::AuthController;
#[cfg(feature = "gitea")]
use oauth2::{AuthorizationCode, CsrfToken, Scope, TokenResponse};
#[cfg(feature = "gitea")]
use reqwest::Client;
#[cfg(feature = "gitea")]
use crate::config::oauth::GiteaOAuthConfig;
/// Controller for handling Gitea authentication /// Controller for handling Gitea authentication
#[cfg(feature = "gitea")]
pub struct GiteaAuthController; pub struct GiteaAuthController;
#[cfg(feature = "gitea")]
impl GiteaAuthController { impl GiteaAuthController {
/// Initiate the OAuth flow /// Initiate the OAuth flow
pub async fn login( pub async fn login(
@ -166,14 +160,22 @@ impl GiteaAuthController {
let client = Client::new(); let client = Client::new();
let user_info_url = format!("{}/api/v1/user", oauth_config.instance_url); let user_info_url = format!("{}/api/v1/user", oauth_config.instance_url);
let gitea_user = client log::info!("Gitea instance URL from config: {}", oauth_config.instance_url);
let access_token_secret = token.access_token().secret();
log::info!("Using access token for /api/v1/user: {}", access_token_secret);
let response = client
.get(&user_info_url) .get(&user_info_url)
.bearer_auth(token.access_token().secret()) .bearer_auth(access_token_secret)
.send() .send()
.await .await
.map_err(|e| actix_web::error::ErrorInternalServerError(format!("API request error: {}", e)))? .map_err(|e| actix_web::error::ErrorInternalServerError(format!("API request error: {}", e)))?;
.json::<crate::config::oauth::GiteaUser>()
.await let response_body = response.text().await.map_err(|e| actix_web::error::ErrorInternalServerError(format!("Failed to get response body: {}", e)))?;
log::info!("Raw Gitea user info response: {}", response_body);
let gitea_user: crate::config::oauth::GiteaUser = serde_json::from_str(&response_body)
.map_err(|e| actix_web::error::ErrorInternalServerError(format!("JSON parsing error: {}", e)))?; .map_err(|e| actix_web::error::ErrorInternalServerError(format!("JSON parsing error: {}", e)))?;
// Create or update the user in your system // Create or update the user in your system
@ -213,7 +215,6 @@ impl GiteaAuthController {
} }
/// Query parameters for the OAuth callback /// Query parameters for the OAuth callback
#[cfg(feature = "gitea")]
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
pub struct CallbackQuery { pub struct CallbackQuery {
pub code: String, pub code: String,

View File

@ -2,6 +2,7 @@ use actix_web::{web, Result, Responder};
use tera::Tera; use tera::Tera;
use crate::utils::render_template; use crate::utils::render_template;
use actix_session::Session; use actix_session::Session;
use std::env;
/// Controller for handling home-related routes /// Controller for handling home-related routes
pub struct HomeController; pub struct HomeController;
@ -12,6 +13,12 @@ impl HomeController {
let mut ctx = tera::Context::new(); let mut ctx = tera::Context::new();
ctx.insert("active_page", "home"); ctx.insert("active_page", "home");
let is_gitea_flow_active = env::var("GITEA_CLIENT_ID")
.ok()
.filter(|s| !s.is_empty())
.is_some();
ctx.insert("gitea_enabled", &is_gitea_flow_active);
// Add user to context if available // Add user to context if available
if let Ok(Some(user_json)) = session.get::<String>("user") { if let Ok(Some(user_json)) = session.get::<String>("user") {
// Keep the raw JSON for backward compatibility // Keep the raw JSON for backward compatibility
@ -36,6 +43,11 @@ impl HomeController {
/// Renders the about page /// Renders the about page
pub async fn about(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> { pub async fn about(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let mut ctx = tera::Context::new(); let mut ctx = tera::Context::new();
let is_gitea_flow_active = env::var("GITEA_CLIENT_ID")
.ok()
.filter(|s| !s.is_empty())
.is_some();
ctx.insert("gitea_enabled", &is_gitea_flow_active);
ctx.insert("active_page", "about"); ctx.insert("active_page", "about");
// Add user to context if available // Add user to context if available

View File

@ -1,6 +1,5 @@
// Export controllers // Export controllers
pub mod auth; pub mod auth;
pub mod debug; pub mod debug;
#[cfg(feature = "gitea")]
pub mod gitea_auth; pub mod gitea_auth;
pub mod home; pub mod home;

View File

@ -1,14 +1,13 @@
#[cfg(feature = "gitea")]
use crate::config::oauth::GiteaOAuthConfig; use crate::config::oauth::GiteaOAuthConfig;
use crate::controllers::auth::AuthController; use crate::controllers::auth::AuthController;
use crate::controllers::debug::DebugController; use crate::controllers::debug::DebugController;
#[cfg(feature = "gitea")]
use crate::controllers::gitea_auth::GiteaAuthController; 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;
use actix_session::{storage::CookieSessionStore, SessionMiddleware}; use actix_session::{storage::CookieSessionStore, SessionMiddleware};
use actix_web::web; use actix_web::web;
use std::env;
/// Configures all application routes /// Configures all application routes
pub fn configure_routes(cfg: &mut web::ServiceConfig) { pub fn configure_routes(cfg: &mut web::ServiceConfig) {
@ -26,35 +25,44 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) {
) )
.build(); .build();
let mut scope = web::scope("") // Build the main scope with common routes
.wrap(session_middleware) let mut main_scope = web::scope("")
.wrap(session_middleware) // Wrap with session middleware
// 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));
// Auth routes
.route("/login", web::get().to(AuthController::login_page))
.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))
// Debug routes
.route("/debug", web::get().to(DebugController::debug_info));
#[cfg(feature = "gitea")] // Conditionally add authentication routes based on GITEA_CLIENT_ID environment variable
{ if env::var("GITEA_CLIENT_ID").ok().filter(|s| !s.is_empty()).is_some() {
// Create the OAuth configuration // Use Gitea OAuth flow
// Create the OAuth configuration and add it to the scope
let oauth_config = web::Data::new(GiteaOAuthConfig::new()); let oauth_config = web::Data::new(GiteaOAuthConfig::new());
// Gitea OAuth configuration and routes main_scope = main_scope
scope = scope .app_data(oauth_config) // Add oauth_config data
.app_data(oauth_config.clone()) // Gitea OAuth routes
.route("/login", web::get().to(GiteaAuthController::login)) // Add /login route for gitea
.route("/auth/gitea", web::get().to(GiteaAuthController::login)) .route("/auth/gitea", web::get().to(GiteaAuthController::login))
.route( .route(
"/auth/gitea/callback", "/auth/gitea/callback",
web::get().to(GiteaAuthController::callback), web::get().to(GiteaAuthController::callback),
); );
} else {
// Use standard username/password login
main_scope = main_scope
.route("/login", web::get().to(AuthController::login_page))
.route("/login", web::post().to(AuthController::login))
.route("/register", web::get().to(AuthController::register_page))
.route("/register", web::post().to(AuthController::register));
} }
cfg.service(scope); // Add common auth and debug routes (logout is common to both flows)
main_scope = main_scope
.route("/logout", web::get().to(AuthController::logout))
// Debug routes
.route("/debug", web::get().to(DebugController::debug_info));
// Register the main scope service
cfg.service(main_scope);
// Protected routes that require authentication // Protected routes that require authentication
cfg.service( cfg.service(

View File

@ -1,8 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %} {% block title %}Login - Hostbasket{% endblock %} {%
block content %}
{% block title %}Login - Hostbasket{% endblock %}
{% block content %}
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
@ -11,36 +8,64 @@
<h2>Login</h2> <h2>Login</h2>
</div> </div>
<div class="card-body"> <div class="card-body">
{% if not gitea_enabled %}
<form method="post" action="/login"> <form method="post" action="/login">
<div class="mb-3"> <div class="mb-3">
<label for="email" class="form-label">Email</label> <label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" required> <input
type="email"
class="form-control"
id="email"
name="email"
required
/>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="password" class="form-label">Password</label> <label for="password" class="form-label"
<input type="password" class="form-control" id="password" name="password" required> >Password</label
>
<input
type="password"
class="form-control"
id="password"
name="password"
required
/>
</div> </div>
<button type="submit" class="btn btn-primary">Login</button> <button type="submit" class="btn btn-primary">
Login
</button>
</form> </form>
{% else %}
<hr> <hr />
{% if gitea_enabled %}
<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" <img
style="margin-right: 5px;"> 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>
{% endif %} {% endif %}
<hr> <hr />
{% if not gitea_enabled %}
<div class="text-center"> <div class="text-center">
<p>Don't have an account? <a href="/register">Register</a></p> <p>
Don't have an account?
<a href="/register">Register</a>
</p>
</div> </div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,7 +1,4 @@
{% extends "base.html" %} {% extends "base.html" %} {% block title %}Register - Hostbasket{% endblock %}
{% block title %}Register - Hostbasket{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
@ -14,41 +11,61 @@
<form method="post" action="/register"> <form method="post" action="/register">
<div class="mb-3"> <div class="mb-3">
<label for="name" class="form-label">Name</label> <label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" name="name" required> <input
type="text"
class="form-control"
id="name"
name="name"
required
/>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="email" class="form-label">Email</label> <label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" required> <input
type="email"
class="form-control"
id="email"
name="email"
required
/>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="password" class="form-label">Password</label> <label for="password" class="form-label"
<input type="password" class="form-control" id="password" name="password" required> >Password</label
>
<input
type="password"
class="form-control"
id="password"
name="password"
required
/>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="password_confirmation" class="form-label">Confirm Password</label> <label
<input type="password" class="form-control" id="password_confirmation" for="password_confirmation"
name="password_confirmation" required> class="form-label"
>Confirm Password</label
>
<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 />
{% if gitea_enabled %}
<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>
{% endif %}
<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>
</div> </div>
</div> </div>

View File

@ -43,11 +43,13 @@
<a class="nav-link {{ active_class(current=active_page, page=" login") }}" <a class="nav-link {{ active_class(current=active_page, page=" login") }}"
href="/login">Login</a> href="/login">Login</a>
</li> </li>
{% if gitea_enabled == false %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {{ active_class(current=active_page, page=" register") }}" <a class="nav-link {{ active_class(current=active_page, page=" register") }}"
href="/register">Register</a> href="/register">Register</a>
</li> </li>
{% endif %} {% endif %}
{% endif %}
</ul> </ul>
</div> </div>
</div> </div>
@ -61,6 +63,7 @@
<summary>Debug Info</summary> <summary>Debug Info</summary>
<pre>user_json: {{ user_json }}</pre> <pre>user_json: {{ user_json }}</pre>
<pre>user object exists: {{ user is defined }}</pre> <pre>user object exists: {{ user is defined }}</pre>
<pre>is_gitea_flow_active: {{ gitea_enabled }}</pre>
{% if user is defined %} {% if user is defined %}
<pre>user.name: {{ user.name }}</pre> <pre>user.name: {{ user.name }}</pre>
{% endif %} {% endif %}

4
start.sh Normal file → Executable file
View File

@ -4,5 +4,7 @@
cd "$(dirname "$0")" cd "$(dirname "$0")"
export SECRET_KEY=1234 export SECRET_KEY=1234
export GITEA_CLIENT_ID=""
export GITEA_CLIENT_SECRET=""
export GITEA_INSTANCE_URL="https://git.ourworld.tf"
cargo run cargo run

View File

@ -3,8 +3,8 @@
# Get the directory of the script and change to it # Get the directory of the script and change to it
cd "$(dirname "$0")" cd "$(dirname "$0")"
export GITEA_CLIENT_ID="your_client_id" export GITEA_CLIENT_ID="9f409b35-6258-4ac3-8370-05adc187c1f5"
export GITEA_CLIENT_SECRET="your_client_secret" export GITEA_CLIENT_SECRET="gto_4s77ae33m5ernlf2423wx6wjyyqatqoe567rym7fcu3sqmu5azea"
export GITEA_INSTANCE_URL="https://gitea.example.com" export GITEA_INSTANCE_URL="https://git.ourworld.tf"
export APP_URL="http://localhost:9999" export APP_URL="http://localhost:9999"
cargo run --features gitea cargo run