From afe436f66bba4a568d3a0b2693cdafef6c74e60b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Mon, 23 Jun 2025 14:45:38 +0200 Subject: [PATCH 01/22] feat(auth): Implement user authentication with login, registration, and session management --- Dockerfile | 13 +- entrypoint.sh | 24 ++- src/api.rs | 380 ++++++++++++++++++++++++++++++++++++- src/config.rs | 46 +++++ src/database.rs | 291 +++++++++++++++++++++++++++- src/main.rs | 24 ++- src/models.rs | 61 +++++- web/composables/useAuth.js | 144 ++++++++++++++ web/pages/login.vue | 91 +++++++++ web/pages/onboarding.vue | 298 +++++++++++++++++++++++++++++ web/pages/register.vue | 141 ++++++++++++++ web/pages/settings.vue | 259 +++++++++++++++++++++++++ 12 files changed, 1747 insertions(+), 25 deletions(-) create mode 100644 web/composables/useAuth.js create mode 100644 web/pages/login.vue create mode 100644 web/pages/onboarding.vue create mode 100644 web/pages/register.vue create mode 100644 web/pages/settings.vue diff --git a/Dockerfile b/Dockerfile index e9637ea..548430c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,9 +17,16 @@ COPY nginx.conf /etc/nginx/nginx.conf COPY entrypoint.sh /app/entrypoint.sh RUN chmod +x /app/entrypoint.sh -# Créer le répertoire de données -RUN mkdir -p /github-ntfy && chmod 755 /github-ntfy +# Créer le répertoire de données et définir les permissions +RUN mkdir -p /github-ntfy && chmod 777 /github-ntfy -EXPOSE 5000 80 3000 +# Variables d'environnement (optionnelles) +ENV DB_PATH=/github-ntfy +ENV RUST_LOG=info + +# Volumes pour la persistance des données +VOLUME ["/github-ntfy"] + +EXPOSE 5000 80 ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/entrypoint.sh b/entrypoint.sh index 22b49c1..7f25e1b 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,11 +1,25 @@ #!/bin/sh -# Génère le contenu du fichier auth.txt à partir des variables d'environnement -echo -n "$USERNAME:$PASSWORD" | base64 > /auth.txt +# Check if USERNAME and PASSWORD environment variables are defined +if [ -n "$USERNAME" ] && [ -n "$PASSWORD" ]; then + # Generate auth.txt file content from environment variables + echo -n "$USERNAME:$PASSWORD" | base64 > /auth.txt + echo "Authentication file generated from environment variables" +else + echo "USERNAME and/or PASSWORD variables not defined" + echo "Authentication will be managed by the onboarding system via the web interface" +fi -# Démarrer nginx en arrière-plan +# Set database directory permissions +if [ -d "/github-ntfy" ]; then + chmod -R 777 /github-ntfy + echo "Permissions applied to data directory" +fi + +# Start nginx in the background +echo "Starting Nginx..." nginx -g 'daemon off;' & - -# Démarrer l'API principale +# Start the main application +echo "Starting application..." exec /usr/local/bin/github-ntfy \ No newline at end of file diff --git a/src/api.rs b/src/api.rs index 46677f2..e625180 100644 --- a/src/api.rs +++ b/src/api.rs @@ -5,10 +5,15 @@ use std::env; use std::sync::Arc; use tokio::sync::Mutex; use warp::{Filter, Reply, Rejection}; -use warp::http::StatusCode; +use warp::http::{StatusCode, header}; use serde::{Serialize, Deserialize}; use warp::cors::Cors; use chrono::Utc; +use crate::database::{ + get_user_by_username, verify_password, create_user, create_session, + get_session, delete_session, get_app_settings, update_app_settings +}; +use crate::models::{UserLogin, UserRegistration, AuthResponse, ApiResponse, AppSettings}; #[derive(Debug, Serialize, Deserialize)] struct RepoRequest { @@ -28,14 +33,24 @@ pub async fn start_api() -> Result<(), Box> let db_path = env::var("DB_PATH").unwrap_or_else(|_| "/github-ntfy".to_string()); std::fs::create_dir_all(&db_path).ok(); let repos_path = format!("{}/watched_repos.db", db_path); + let versions_path = format!("{}/ghntfy_versions.db", db_path); match Connection::open(&repos_path) { Ok(conn) => { info!("Database connection established successfully"); let db = Arc::new(Mutex::new(conn)); + let versions_conn = match Connection::open(&versions_path) { + Ok(c) => c, + Err(e) => { + error!("Unable to open versions database: {}", e); + return Err(Box::new(e)); + } + }; + + let versions_db = Arc::new(Mutex::new(versions_conn)); + // Route definitions - let add_github = warp::path("app_repo") .and(warp::post()) .and(warp::body::json()) .and(with_db(db.clone())) @@ -74,11 +89,50 @@ pub async fn start_api() -> Result<(), Box> .and(with_db(db.clone())) .and_then(get_latest_updates); + let login_route = warp::path("auth") + .and(warp::path("login")) + .and(warp::post()) + .and(warp::body::json()) + .and(with_db(versions_db.clone())) + .and_then(login); + + let register_route = warp::path("auth") + .and(warp::path("register")) + .and(warp::post()) + .and(warp::body::json()) + .and(with_db(versions_db.clone())) + .and_then(register); + + let logout_route = warp::path("auth") + .and(warp::path("logout")) + .and(warp::post()) + .and(with_auth()) + .and(with_db(versions_db.clone())) + .and_then(logout); + + let get_settings_route = warp::path("settings") + .and(warp::get()) + .and(with_db(versions_db.clone())) + .and(with_auth()) + .and_then(get_settings); + + let update_settings_route = warp::path("settings") + .and(warp::put()) + .and(warp::body::json()) + .and(with_db(versions_db.clone())) + .and(with_auth()) + .and_then(update_settings); + + let is_configured_route = warp::path("is_configured") + .and(warp::get()) + .and(with_db(versions_db.clone())) + .and_then(is_configured); + // Configure CORS let cors = warp::cors() .allow_any_origin() - .allow_headers(vec!["Content-Type"]) - .allow_methods(vec!["GET", "POST"]); + .allow_headers(vec!["Content-Type", "Authorization"]) + .allow_methods(vec!["GET", "POST", "PUT", "DELETE"]); // Combine all routes with CORS let routes = add_github @@ -87,7 +141,13 @@ pub async fn start_api() -> Result<(), Box> .or(get_docker) .or(delete_github) .or(delete_docker) - .or(get_updates) + .or(get_updates) + .or(login_route) + .or(register_route) + .or(logout_route) + .or(get_settings_route) + .or(update_settings_route) + .or(is_configured_route) .with(cors); // Start the server @@ -481,3 +541,313 @@ async fn get_latest_updates(db: Arc>) -> Result>) -> Result { + let conn = db.lock().await; + + match verify_password(&conn, &login.username, &login.password) { + Ok(true) => { + if let Ok(Some(user)) = get_user_by_username(&conn, &login.username) { + if let Ok(token) = create_session(&conn, user.id) { + let auth_response = AuthResponse { + token, + user: user.clone(), + }; + + Ok(warp::reply::with_status( + warp::reply::json(&ApiResponse { + success: true, + message: "Login successful".to_string(), + data: Some(auth_response), + }), + StatusCode::OK, + )) + } else { + Ok(warp::reply::with_status( + warp::reply::json(&ApiResponse::<()> { + success: false, + message: "Error creating session".to_string(), + data: None, + }), + StatusCode::INTERNAL_SERVER_ERROR, + )) + } + } else { + Ok(warp::reply::with_status( + warp::reply::json(&ApiResponse::<()> { + success: false, + message: "User not found".to_string(), + data: None, + }), + StatusCode::NOT_FOUND, + )) + } + }, + Ok(false) => { + Ok(warp::reply::with_status( + warp::reply::json(&ApiResponse::<()> { + success: false, + message: "Incorrect username or password".to_string(), + data: None, + }), + StatusCode::UNAUTHORIZED, + )) + }, + Err(_) => { + Ok(warp::reply::with_status( + warp::reply::json(&ApiResponse::<()> { + success: false, + message: "Internal server error".to_string(), + data: None, + }), + StatusCode::INTERNAL_SERVER_ERROR, + )) + } + } +} + +async fn register(registration: UserRegistration, db: Arc>) -> Result { + let conn = db.lock().await; + + // Check if a user already exists with this username + if let Ok(Some(_)) = get_user_by_username(&conn, ®istration.username) { + return Ok(warp::reply::with_status( + warp::reply::json(&ApiResponse::<()> { + success: false, + message: "A user with this name already exists".to_string(), + data: None, + }), + StatusCode::CONFLICT, + )); + } + + // Create the new user + match create_user(&conn, ®istration.username, ®istration.password, registration.is_admin) { + Ok(user_id) => { + if let Ok(Some(user)) = get_user_by_username(&conn, ®istration.username) { + if let Ok(token) = create_session(&conn, user_id) { + let auth_response = AuthResponse { + token, + user, + }; + + Ok(warp::reply::with_status( + warp::reply::json(&ApiResponse { + success: true, + message: "Registration successful".to_string(), + data: Some(auth_response), + }), + StatusCode::CREATED, + )) + } else { + Ok(warp::reply::with_status( + warp::reply::json(&ApiResponse::<()> { + success: false, + message: "Error creating session".to_string(), + data: None, + }), + StatusCode::INTERNAL_SERVER_ERROR, + )) + } + } else { + Ok(warp::reply::with_status( + warp::reply::json(&ApiResponse::<()> { + success: false, + message: "Error retrieving user".to_string(), + data: None, + }), + StatusCode::INTERNAL_SERVER_ERROR, + )) + } + }, + Err(_) => { + Ok(warp::reply::with_status( + warp::reply::json(&ApiResponse::<()> { + success: false, + message: "Error creating user".to_string(), + data: None, + }), + StatusCode::INTERNAL_SERVER_ERROR, + )) + } + } +} + +async fn logout(token: String, db: Arc>) -> Result { + let conn = db.lock().await; + + match delete_session(&conn, &token) { + Ok(_) => { + Ok(warp::reply::with_status( + warp::reply::json(&ApiResponse::<()> { + success: true, + message: "Logout successful".to_string(), + data: None, + }), + StatusCode::OK, + )) + }, + Err(_) => { + Ok(warp::reply::with_status( + warp::reply::json(&ApiResponse::<()> { + success: false, + message: "Error during logout".to_string(), + data: None, + }), + StatusCode::INTERNAL_SERVER_ERROR, + )) + } + } +} + +async fn get_settings(db: Arc>, token: String) -> Result { + let conn = db.lock().await; + + // Verify authentication + if let Ok(Some(session)) = get_session(&conn, &token) { + if session.expires_at < Utc::now() { + return Ok(warp::reply::with_status( + warp::reply::json(&ApiResponse::<()> { + success: false, + message: "Session expired".to_string(), + data: None, + }), + StatusCode::UNAUTHORIZED, + )); + } + + // Retrieve settings + match get_app_settings(&conn) { + Ok(Some(settings)) => { + Ok(warp::reply::with_status( + warp::reply::json(&ApiResponse { + success: true, + message: "Settings retrieved successfully".to_string(), + data: Some(settings), + }), + StatusCode::OK, + )) + }, + Ok(None) => { + Ok(warp::reply::with_status( + warp::reply::json(&ApiResponse::<()> { + success: false, + message: "No settings found".to_string(), + data: None, + }), + StatusCode::NOT_FOUND, + )) + }, + Err(_) => { + Ok(warp::reply::with_status( + warp::reply::json(&ApiResponse::<()> { + success: false, + message: "Error retrieving settings".to_string(), + data: None, + }), + StatusCode::INTERNAL_SERVER_ERROR, + )) + } + } + } else { + Ok(warp::reply::with_status( + warp::reply::json(&ApiResponse::<()> { + success: false, + message: "Unauthorized".to_string(), + data: None, + }), + StatusCode::UNAUTHORIZED, + )) + } +} + +async fn update_settings(settings: AppSettings, db: Arc>, token: String) -> Result { + let conn = db.lock().await; + + // Verify authentication + if let Ok(Some(session)) = get_session(&conn, &token) { + if session.expires_at < Utc::now() { + return Ok(warp::reply::with_status( + warp::reply::json(&ApiResponse::<()> { + success: false, + message: "Session expired".to_string(), + data: None, + }), + StatusCode::UNAUTHORIZED, + )); + } + + // Update settings + match update_app_settings(&conn, &settings) { + Ok(_) => { + Ok(warp::reply::with_status( + warp::reply::json(&ApiResponse::<()> { + success: true, + message: "Settings updated successfully".to_string(), + data: None, + }), + StatusCode::OK, + )) + }, + Err(_) => { + Ok(warp::reply::with_status( + warp::reply::json(&ApiResponse::<()> { + success: false, + message: "Error updating settings".to_string(), + data: None, + }), + StatusCode::INTERNAL_SERVER_ERROR, + )) + } + } + } else { + Ok(warp::reply::with_status( + warp::reply::json(&ApiResponse::<()> { + success: false, + message: "Unauthorized".to_string(), + data: None, + }), + StatusCode::UNAUTHORIZED, + )) + } +} + +// Function to check if the application is configured +async fn is_configured(db: Arc>) -> Result { + let conn = db.lock().await; + + // Check if at least one admin user exists + let admin_exists = match conn.query_row( + "SELECT COUNT(*) FROM users WHERE is_admin = 1", + [], + |row| row.get::<_, i64>(0) + ) { + Ok(count) => count > 0, + Err(_) => false, + }; + + // Check if settings are configured + let settings_exist = match get_app_settings(&conn) { + Ok(Some(settings)) => { + // Check if at least one notification service is configured + settings.ntfy_url.is_some() || + settings.discord_webhook_url.is_some() || + settings.slack_webhook_url.is_some() || + settings.gotify_url.is_some() + }, + _ => false, + }; + + Ok(warp::reply::with_status( + warp::reply::json(&ApiResponse { + success: true, + message: "Configuration status retrieved".to_string(), + data: Some(json!({ + "configured": admin_exists && settings_exist, + "admin_exists": admin_exists, + "settings_exist": settings_exist + })), + }), + StatusCode::OK, + )) +} diff --git a/src/config.rs b/src/config.rs index cf3675f..1529fd9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,9 +1,12 @@ use dotenv::dotenv; +use log::info; use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION}; use std::env; use std::fs::File; use std::io::Read; +use rusqlite::Connection; use crate::docker::create_dockerhub_token; +use crate::database::get_app_settings; // Configuration pub struct Config { @@ -57,6 +60,49 @@ impl Config { } } + pub fn from_database(conn: &Connection) -> Self { + // First, try to load from database + if let Ok(Some(settings)) = get_app_settings(conn) { + let docker_username = settings.docker_username; + let docker_password = settings.docker_password.clone(); + + let docker_token = if let (Some(username), Some(password)) = (&docker_username, &docker_password) { + create_dockerhub_token(username, password) + } else { + None + }; + + // Read authentication file (for compatibility with the old system) + let mut auth = String::new(); + if let Ok(mut file) = File::open("/auth.txt") { + file.read_to_string(&mut auth).ok(); + auth = auth.trim().to_string(); + } + + let timeout = settings.check_interval.unwrap_or(3600) as f64; + + info!("Configuration loaded from database"); + + return Config { + github_token: settings.github_token, + docker_username, + docker_password, + docker_token, + ntfy_url: settings.ntfy_url, + gotify_url: settings.gotify_url, + gotify_token: settings.gotify_token, + discord_webhook_url: settings.discord_webhook_url, + slack_webhook_url: settings.slack_webhook_url, + auth, + timeout, + }; + } + + // Fallback to environment variables if database is not available + info!("No configuration found in database, using environment variables"); + Self::from_env() + } + pub fn github_headers(&self) -> HeaderMap { let mut headers = HeaderMap::new(); if let Some(token) = &self.github_token { diff --git a/src/database.rs b/src/database.rs index 2742a85..948dfcd 100644 --- a/src/database.rs +++ b/src/database.rs @@ -2,6 +2,10 @@ use log::info; pub(crate) use rusqlite::{Connection, Result as SqliteResult, OpenFlags}; use std::env; use std::path::Path; +use chrono::Utc; +use rand::Rng; +use bcrypt::{hash, verify, DEFAULT_COST}; +use crate::models::{User, Session, AppSettings}; pub fn init_databases() -> SqliteResult<(Connection, Connection)> { let db_path = env::var("DB_PATH").unwrap_or_else(|_| "/github-ntfy".to_string()); @@ -34,6 +38,110 @@ pub fn init_databases() -> SqliteResult<(Connection, Connection)> { [], )?; + conn.execute( + "CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + is_admin INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL + )", + [], + )?; + + conn.execute( + "CREATE TABLE IF NOT EXISTS sessions ( + token TEXT PRIMARY KEY, + user_id INTEGER NOT NULL, + expires_at TEXT NOT NULL, + FOREIGN KEY (user_id) REFERENCES users(id) + )", + [], + )?; + + conn.execute( + "CREATE TABLE IF NOT EXISTS app_settings ( + id INTEGER PRIMARY KEY CHECK (id = 1), + ntfy_url TEXT, + github_token TEXT, + docker_username TEXT, + docker_password TEXT, + gotify_url TEXT, + gotify_token TEXT, + discord_webhook_url TEXT, + slack_webhook_url TEXT, + check_interval INTEGER DEFAULT 3600, + last_updated TEXT NOT NULL + )", + [], + )?; + + let admin_exists = conn + .query_row("SELECT COUNT(*) FROM users WHERE is_admin = 1", [], |row| { + row.get::<_, i64>(0) + }) + .unwrap_or(0); + + if admin_exists == 0 { + if let (Ok(username), Ok(password)) = (env::var("USERNAME"), env::var("PASSWORD")) { + if !username.is_empty() && !password.is_empty() { + let hashed_password = hash(password, DEFAULT_COST).unwrap_or_else(|_| String::new()); + let now = Utc::now().to_rfc3339(); + if let Err(e) = conn.execute( + "INSERT INTO users (username, password_hash, is_admin, created_at) VALUES (?, ?, 1, ?)", + &[&username, &hashed_password, &now], + ) { + info!("Erreur lors de la création de l'utilisateur admin: {}", e); + } else { + info!("Utilisateur admin créé avec succès depuis les variables d'environnement"); + } + } + } + } + + let settings_exist = conn + .query_row("SELECT COUNT(*) FROM app_settings", [], |row| { + row.get::<_, i64>(0) + }) + .unwrap_or(0); + + if settings_exist == 0 { + let ntfy_url = env::var("NTFY_URL").ok(); + let github_token = env::var("GHNTFY_TOKEN").ok(); + let docker_username = env::var("DOCKER_USERNAME").ok(); + let docker_password = env::var("DOCKER_PASSWORD").ok(); + let gotify_url = env::var("GOTIFY_URL").ok(); + let gotify_token = env::var("GOTIFY_TOKEN").ok(); + let discord_webhook_url = env::var("DISCORD_WEBHOOK_URL").ok(); + let slack_webhook_url = env::var("SLACK_WEBHOOK_URL").ok(); + let check_interval = env::var("GHNTFY_TIMEOUT") + .ok() + .and_then(|s| s.parse::().ok()) + .unwrap_or(3600); + let now = Utc::now().to_rfc3339(); + + if let Err(e) = conn.execute( + "INSERT INTO app_settings (id, ntfy_url, github_token, docker_username, docker_password, gotify_url, gotify_token, discord_webhook_url, slack_webhook_url, check_interval, last_updated) + VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + rusqlite::params![ + ntfy_url, + github_token, + docker_username, + docker_password, + gotify_url, + gotify_token, + discord_webhook_url, + slack_webhook_url, + check_interval, + now + ], + ) { + info!("Erreur lors de l'initialisation des paramètres: {}", e); + } else { + info!("Paramètres initialisés avec succès depuis les variables d'environnement"); + } + } + let conn2 = Connection::open_with_flags(&repos_path, OpenFlags::SQLITE_OPEN_CREATE | OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_URI)?; info!("Database open at {}", repos_path); @@ -100,4 +208,185 @@ pub fn update_version(conn: &Connection, repo: &str, version: &str, changelog: O )?; Ok(()) -} \ No newline at end of file +} + +pub fn create_user(conn: &Connection, username: &str, password: &str, is_admin: bool) -> SqliteResult { + let hashed_password = hash(password, DEFAULT_COST).map_err(|e| { + rusqlite::Error::UserFunctionError(Box::new(std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))) + })?; + + let now = Utc::now().to_rfc3339(); + + conn.execute( + "INSERT INTO users (username, password_hash, is_admin, created_at) VALUES (?, ?, ?, ?)", + &[username, &hashed_password, &(if is_admin { 1 } else { 0 }).to_string(), &now], + )?; + + Ok(conn.last_insert_rowid()) +} + +pub fn get_user_by_username(conn: &Connection, username: &str) -> SqliteResult> { + let mut stmt = conn.prepare("SELECT id, username, password_hash, is_admin, created_at FROM users WHERE username = ?")?; + let mut rows = stmt.query(&[username])?; + + if let Some(row) = rows.next()? { + let id = row.get(0)?; + let username = row.get(1)?; + let password_hash = row.get(2)?; + let is_admin: i64 = row.get(3)?; + let created_at_str: String = row.get(4)?; + let created_at = chrono::DateTime::parse_from_rfc3339(&created_at_str) + .map(|dt| dt.with_timezone(&Utc)) + .map_err(|e| { + rusqlite::Error::UserFunctionError(Box::new(std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))) + })?; + + Ok(Some(User { + id, + username, + password_hash, + is_admin: is_admin == 1, + created_at, + })) + } else { + Ok(None) + } +} + +pub fn verify_password(conn: &Connection, username: &str, password: &str) -> SqliteResult { + if let Some(user) = get_user_by_username(conn, username)? { + Ok(verify(password, &user.password_hash).unwrap_or(false)) + } else { + Ok(false) + } +} + +pub fn create_session(conn: &Connection, user_id: i64) -> SqliteResult { + let token = generate_session_token(); + let expires_at = Utc::now() + chrono::Duration::days(7); + let expires_at_str = expires_at.to_rfc3339(); + + conn.execute( + "INSERT INTO sessions (token, user_id, expires_at) VALUES (?, ?, ?)", + &[&token, &user_id.to_string(), &expires_at_str], + )?; + + Ok(token) +} + +pub fn get_session(conn: &Connection, token: &str) -> SqliteResult> { + let mut stmt = conn.prepare("SELECT token, user_id, expires_at FROM sessions WHERE token = ?")?; + let mut rows = stmt.query(&[token])?; + + if let Some(row) = rows.next()? { + let token = row.get(0)?; + let user_id = row.get(1)?; + let expires_at_str: String = row.get(2)?; + let expires_at = chrono::DateTime::parse_from_rfc3339(&expires_at_str) + .map(|dt| dt.with_timezone(&Utc)) + .map_err(|e| { + rusqlite::Error::UserFunctionError(Box::new(std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))) + })?; + + Ok(Some(Session { + token, + user_id, + expires_at, + })) + } else { + Ok(None) + } +} + +pub fn delete_session(conn: &Connection, token: &str) -> SqliteResult<()> { + conn.execute( + "DELETE FROM sessions WHERE token = ?", + &[token], + )?; + + Ok(()) +} + +pub fn get_app_settings(conn: &Connection) -> SqliteResult> { + let mut stmt = conn.prepare( + "SELECT id, ntfy_url, github_token, docker_username, docker_password, + gotify_url, gotify_token, discord_webhook_url, slack_webhook_url, + check_interval, last_updated + FROM app_settings + WHERE id = 1" + )?; + + let mut rows = stmt.query([])?; + + if let Some(row) = rows.next()? { + let id = row.get(0)?; + let ntfy_url = row.get(1)?; + let github_token = row.get(2)?; + let docker_username = row.get(3)?; + let docker_password = row.get(4)?; + let gotify_url = row.get(5)?; + let gotify_token = row.get(6)?; + let discord_webhook_url = row.get(7)?; + let slack_webhook_url = row.get(8)?; + let check_interval = row.get(9)?; + let last_updated_str: String = row.get(10)?; + let last_updated = chrono::DateTime::parse_from_rfc3339(&last_updated_str) + .map(|dt| dt.with_timezone(&Utc)) + .map_err(|e| { + rusqlite::Error::UserFunctionError(Box::new(std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))) + })?; + + Ok(Some(AppSettings { + id: Some(id), + ntfy_url, + github_token, + docker_username, + docker_password, + gotify_url, + gotify_token, + discord_webhook_url, + slack_webhook_url, + check_interval, + last_updated, + })) + } else { + Ok(None) + } +} + +pub fn update_app_settings(conn: &Connection, settings: &AppSettings) -> SqliteResult<()> { + let now = Utc::now().to_rfc3339(); + + conn.execute( + "UPDATE app_settings + SET ntfy_url = ?, github_token = ?, docker_username = ?, docker_password = ?, + gotify_url = ?, gotify_token = ?, discord_webhook_url = ?, slack_webhook_url = ?, + check_interval = ?, last_updated = ? + WHERE id = 1", + rusqlite::params![ + settings.ntfy_url, + settings.github_token, + settings.docker_username, + settings.docker_password, + settings.gotify_url, + settings.gotify_token, + settings.discord_webhook_url, + settings.slack_webhook_url, + settings.check_interval, + now + ], + )?; + + Ok(()) +} + +fn generate_session_token() -> String { + let mut rng = rand::thread_rng(); + let token_bytes: Vec = (0..32).map(|_| rng.gen::()).collect(); + + // Convertir en hexadécimal + token_bytes.iter() + .map(|b| format!("{:02x}", b)) + .collect::>() + .join("") +} diff --git a/src/main.rs b/src/main.rs index e68c773..d63b458 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,24 +28,28 @@ fn start_api() { async fn main() -> Result<(), Box> { env_logger::init(); - let config = config::Config::from_env(); + // Initialize databases let (conn_versions, conn_repos) = database::init_databases()?; + // Load configuration from database, with fallback to environment variables + let config = config::Config::from_database(&conn_versions); + + // Start the REST API start_api(); let client = reqwest::Client::new(); + // Check if configuration is complete if config.auth.is_empty() || (config.ntfy_url.is_none() && config.gotify_url.is_none() && config.discord_webhook_url.is_none() && config.slack_webhook_url.is_none()) { - error!("Incorrect configuration!"); - error!("auth: can be generated with the command: echo -n 'username:password' | base64"); - error!("NTFY_URL: URL of the ntfy server"); - error!("GOTIFY_URL: URL of the gotify server"); - error!("GOTIFY_TOKEN: Gotify token"); - error!("DISCORD_WEBHOOK_URL: Discord webhook URL"); - error!("SLACK_WEBHOOK_URL: Slack webhook URL"); - error!("GHNTFY_TIMEOUT: interval between checks"); - return Ok(()); + info!("No notification service is configured."); + info!("Please configure at least one notification service via the web interface or environment variables."); + info!("The REST API is still available for configuration."); + + // Continue running to allow configuration through the API + loop { + thread::sleep(Duration::from_secs(60)); + } } info!("Starting version monitoring..."); diff --git a/src/models.rs b/src/models.rs index bfaec27..205b453 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,4 +1,6 @@ use serde::Deserialize; +use serde::Serialize; +use std::collections::HashMap; // Structures for GitHub data #[derive(Debug, Deserialize, Clone)] @@ -39,4 +41,61 @@ pub struct NotifiedRelease { pub repo: String, pub tag_name: String, pub notified_at: chrono::DateTime, -} \ No newline at end of file +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct User { + pub id: i64, + pub username: String, + pub password_hash: String, + pub is_admin: bool, + pub created_at: chrono::DateTime, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct UserLogin { + pub username: String, + pub password: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct UserRegistration { + pub username: String, + pub password: String, + pub is_admin: bool, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Session { + pub token: String, + pub user_id: i64, + pub expires_at: chrono::DateTime, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AppSettings { + pub id: Option, + pub ntfy_url: Option, + pub github_token: Option, + pub docker_username: Option, + pub docker_password: Option, + pub gotify_url: Option, + pub gotify_token: Option, + pub discord_webhook_url: Option, + pub slack_webhook_url: Option, + pub check_interval: Option, + pub last_updated: chrono::DateTime, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AuthResponse { + pub token: String, + pub user: User, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ApiResponse { + pub success: bool, + pub message: String, + pub data: Option, +} diff --git a/web/composables/useAuth.js b/web/composables/useAuth.js new file mode 100644 index 0000000..969f910 --- /dev/null +++ b/web/composables/useAuth.js @@ -0,0 +1,144 @@ +// Composable for managing authentication +export const useAuth = () => { + const user = useState('user', () => null); + const token = useState('token', () => null); + const isFirstLogin = useState('isFirstLogin', () => false); + + // Initialize authentication state from localStorage + onMounted(() => { + const storedToken = localStorage.getItem('token'); + const storedUser = localStorage.getItem('user'); + + if (storedToken && storedUser) { + token.value = storedToken; + user.value = JSON.parse(storedUser); + } + }); + + // Login function + const login = async (username, password) => { + try { + const response = await fetch('/auth/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ username, password }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || 'Login failed'); + } + + const data = await response.json(); + + if (!data.success || !data.data) { + throw new Error(data.message || 'Login failed'); + } + + // Store authentication information + token.value = data.data.token; + user.value = data.data.user; + + localStorage.setItem('token', data.data.token); + localStorage.setItem('user', JSON.stringify(data.data.user)); + + // Check if this is the first login + const configResponse = await fetch('/is_configured'); + if (configResponse.ok) { + const configData = await configResponse.json(); + isFirstLogin.value = !configData.data.settings_exist; + } + + return data; + } catch (error) { + console.error('Login error:', error); + throw error; + } + }; + + // Registration function + const register = async (username, password, isAdmin = false) => { + try { + const response = await fetch('/auth/register', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ username, password, is_admin: isAdmin }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || 'Registration failed'); + } + + const data = await response.json(); + + if (!data.success || !data.data) { + throw new Error(data.message || 'Registration failed'); + } + + // Store authentication information + token.value = data.data.token; + user.value = data.data.user; + + localStorage.setItem('token', data.data.token); + localStorage.setItem('user', JSON.stringify(data.data.user)); + + // By default, consider a new registration needs onboarding + isFirstLogin.value = true; + + return data; + } catch (error) { + console.error('Registration error:', error); + throw error; + } + }; + + // Logout function + const logout = async () => { + try { + if (token.value) { + await fetch('/auth/logout', { + method: 'POST', + headers: { + 'Authorization': token.value, + }, + }); + } + } catch (error) { + console.error('Logout error:', error); + } finally { + // Clean up local authentication data + token.value = null; + user.value = null; + localStorage.removeItem('token'); + localStorage.removeItem('user'); + } + }; + + // Check if user is authenticated + const isAuthenticated = computed(() => !!token.value && !!user.value); + + // Check if user is admin + const isAdmin = computed(() => isAuthenticated.value && user.value?.is_admin); + + // Get token for authenticated requests + const getAuthHeader = () => { + return token.value ? { Authorization: token.value } : {}; + }; + + return { + user, + token, + isFirstLogin, + login, + register, + logout, + isAuthenticated, + isAdmin, + getAuthHeader, + }; +}; diff --git a/web/pages/login.vue b/web/pages/login.vue new file mode 100644 index 0000000..db1ec93 --- /dev/null +++ b/web/pages/login.vue @@ -0,0 +1,91 @@ + + + diff --git a/web/pages/onboarding.vue b/web/pages/onboarding.vue new file mode 100644 index 0000000..d991014 --- /dev/null +++ b/web/pages/onboarding.vue @@ -0,0 +1,298 @@ + + + diff --git a/web/pages/register.vue b/web/pages/register.vue new file mode 100644 index 0000000..abd3d0f --- /dev/null +++ b/web/pages/register.vue @@ -0,0 +1,141 @@ + + + diff --git a/web/pages/settings.vue b/web/pages/settings.vue new file mode 100644 index 0000000..62878b3 --- /dev/null +++ b/web/pages/settings.vue @@ -0,0 +1,259 @@ + + + + From 11e33961dcb393f68107d7b2412cbe94db8b54fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Mon, 23 Jun 2025 14:53:49 +0200 Subject: [PATCH 02/22] Update src/models.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/models.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/models.rs b/src/models.rs index 205b453..6633b21 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,6 +1,5 @@ use serde::Deserialize; use serde::Serialize; -use std::collections::HashMap; // Structures for GitHub data #[derive(Debug, Deserialize, Clone)] From c060604c2196c9aab617853de30ac18c72717834 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Mon, 23 Jun 2025 14:54:04 +0200 Subject: [PATCH 03/22] Update entrypoint.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/entrypoint.sh b/entrypoint.sh index 7f25e1b..7170dbb 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -12,7 +12,7 @@ fi # Set database directory permissions if [ -d "/github-ntfy" ]; then - chmod -R 777 /github-ntfy + chmod -R 755 /github-ntfy echo "Permissions applied to data directory" fi From e022b7ac2d4bab4e39c26e4780f1f0043348718a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Mon, 23 Jun 2025 14:54:13 +0200 Subject: [PATCH 04/22] Update Dockerfile Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 548430c..6f7e02f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ COPY entrypoint.sh /app/entrypoint.sh RUN chmod +x /app/entrypoint.sh # Créer le répertoire de données et définir les permissions -RUN mkdir -p /github-ntfy && chmod 777 /github-ntfy +RUN mkdir -p /github-ntfy && chmod 755 /github-ntfy # Variables d'environnement (optionnelles) ENV DB_PATH=/github-ntfy From 8ca81b2ed31e1b33bc547157a3b68c147d49d900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Mon, 23 Jun 2025 15:04:52 +0200 Subject: [PATCH 05/22] feat(api): Add GitHub repository endpoint and implement authorization header handling --- Cargo.lock | 45 +++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 6 ++++-- src/api.rs | 22 ++++++++++++++++++++++ src/database.rs | 22 +++++++++++++++++----- 4 files changed, 88 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index db0c4c3..f838d2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -130,6 +130,19 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bcrypt" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e65938ed058ef47d92cf8b346cc76ef48984572ade631927e9937b5ffc7662c7" +dependencies = [ + "base64 0.22.1", + "blowfish", + "getrandom 0.2.16", + "subtle", + "zeroize", +] + [[package]] name = "bitflags" version = "2.9.1" @@ -145,6 +158,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + [[package]] name = "bumpalo" version = "3.18.1" @@ -188,10 +211,21 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-link", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -461,11 +495,13 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" name = "github-ntfy" version = "2.0.0" dependencies = [ + "bcrypt", "chrono", "dotenv", "env_logger", "log", "openssl", + "rand", "reqwest", "rusqlite", "serde", @@ -865,6 +901,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "ipnet" version = "2.11.0" diff --git a/Cargo.toml b/Cargo.toml index 5449103..e10d025 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,8 @@ serde_json = "1" log = "0.4" env_logger = "0.11" dotenv = "0.15" -chrono = "0.4" +chrono = { version = "0.4", features = ["serde"] } warp = "0.3" -openssl = { version = "0.10", features = ["vendored"] } \ No newline at end of file +openssl = { version = "0.10", features = ["vendored"] } +rand = "0.8" +bcrypt = "0.15" diff --git a/src/api.rs b/src/api.rs index e625180..6983458 100644 --- a/src/api.rs +++ b/src/api.rs @@ -51,6 +51,7 @@ pub async fn start_api() -> Result<(), Box> let versions_db = Arc::new(Mutex::new(versions_conn)); // Route definitions + let add_github = warp::path("app_github_repo") .and(warp::post()) .and(warp::body::json()) .and(with_db(db.clone())) @@ -166,6 +167,27 @@ fn with_db(db: Arc>) -> impl Filter impl Filter + Clone { + warp::header::("Authorization") + .map(|header: String| { + if header.starts_with("Bearer ") { + header[7..].to_string() + } else { + header + } + }) + .or_else(|_| async { + Err(warp::reject::custom(AuthError::MissingToken)) + }) +} + +#[derive(Debug)] +enum AuthError { + MissingToken, +} + +impl warp::reject::Reject for AuthError {} + async fn add_github_repo(body: RepoRequest, db: Arc>) -> Result { let repo = body.repo; diff --git a/src/database.rs b/src/database.rs index 948dfcd..9ffc4dc 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1,5 +1,5 @@ use log::info; -pub(crate) use rusqlite::{Connection, Result as SqliteResult, OpenFlags}; +pub(crate) use rusqlite::{Connection, Result as SqliteResult, OpenFlags, Error as SqliteError}; use std::env; use std::path::Path; use chrono::Utc; @@ -212,7 +212,10 @@ pub fn update_version(conn: &Connection, repo: &str, version: &str, changelog: O pub fn create_user(conn: &Connection, username: &str, password: &str, is_admin: bool) -> SqliteResult { let hashed_password = hash(password, DEFAULT_COST).map_err(|e| { - rusqlite::Error::UserFunctionError(Box::new(std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))) + SqliteError::SqliteFailure( + rusqlite::ffi::Error::new(1), // Code d'erreur personnalisé + Some(e.to_string()) + ) })?; let now = Utc::now().to_rfc3339(); @@ -238,7 +241,10 @@ pub fn get_user_by_username(conn: &Connection, username: &str) -> SqliteResult SqliteResult SqliteResult> let last_updated = chrono::DateTime::parse_from_rfc3339(&last_updated_str) .map(|dt| dt.with_timezone(&Utc)) .map_err(|e| { - rusqlite::Error::UserFunctionError(Box::new(std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))) + SqliteError::SqliteFailure( + rusqlite::ffi::Error::new(1), + Some(e.to_string()) + ) })?; Ok(Some(AppSettings { From af15ab974d4f230ffa1bd823a975dae381bb9088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Mon, 23 Jun 2025 15:05:19 +0200 Subject: [PATCH 06/22] refactor(api, database, main, ntfy): Simplify database lock handling and clean up unused imports --- src/api.rs | 13 ++++++------- src/database.rs | 1 - src/main.rs | 1 - src/notifications/ntfy.rs | 2 +- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/api.rs b/src/api.rs index 6983458..57bbd10 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,13 +1,12 @@ use log::{error, info}; -use rusqlite::{Connection, Result as SqliteResult, params}; +use rusqlite::{Connection, params}; use serde_json::json; use std::env; use std::sync::Arc; use tokio::sync::Mutex; use warp::{Filter, Reply, Rejection}; -use warp::http::{StatusCode, header}; +use warp::http::StatusCode; use serde::{Serialize, Deserialize}; -use warp::cors::Cors; use chrono::Utc; use crate::database::{ get_user_by_username, verify_password, create_user, create_session, @@ -198,7 +197,7 @@ async fn add_github_repo(body: RepoRequest, db: Arc>) -> Resul )); } - let mut db_guard = db.lock().await; + let db_guard = db.lock().await; // Check if repository already exists match db_guard.query_row( @@ -250,7 +249,7 @@ async fn add_docker_repo(body: RepoRequest, db: Arc>) -> Resul )); } - let mut db_guard = db.lock().await; + let db_guard = db.lock().await; // Check if repository already exists match db_guard.query_row( @@ -388,7 +387,7 @@ async fn delete_github_repo(body: RepoRequest, db: Arc>) -> Re )); } - let mut db_guard = db.lock().await; + let db_guard = db.lock().await; // Check if repository exists match db_guard.query_row( @@ -440,7 +439,7 @@ async fn delete_docker_repo(body: RepoRequest, db: Arc>) -> Re )); } - let mut db_guard = db.lock().await; + let db_guard = db.lock().await; // Check if repository exists match db_guard.query_row( diff --git a/src/database.rs b/src/database.rs index 9ffc4dc..fa7bbfa 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1,7 +1,6 @@ use log::info; pub(crate) use rusqlite::{Connection, Result as SqliteResult, OpenFlags, Error as SqliteError}; use std::env; -use std::path::Path; use chrono::Utc; use rand::Rng; use bcrypt::{hash, verify, DEFAULT_COST}; diff --git a/src/main.rs b/src/main.rs index d63b458..56d841e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,6 @@ mod api; use log::{error, info}; use std::thread; use std::time::Duration; -use tokio::task; // Function to start the API in a separate thread fn start_api() { diff --git a/src/notifications/ntfy.rs b/src/notifications/ntfy.rs index ec0bf49..d8c5dba 100644 --- a/src/notifications/ntfy.rs +++ b/src/notifications/ntfy.rs @@ -1,5 +1,5 @@ use log::{error, info}; -use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION}; +use reqwest::header::{HeaderMap, HeaderValue}; use crate::models::{GithubReleaseInfo, DockerReleaseInfo}; pub async fn send_github_notification(release: &GithubReleaseInfo, auth: &str, ntfy_url: &str) { From 7c8b04808e9cd316286bda8d74c296d55bc55be3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Mon, 23 Jun 2025 15:15:33 +0200 Subject: [PATCH 07/22] feat(nginx): Enhance static file handling and consolidate API route configurations --- nginx.conf | 64 +++++++++++++++++------------------------------------- 1 file changed, 20 insertions(+), 44 deletions(-) diff --git a/nginx.conf b/nginx.conf index acac427..0570dd6 100644 --- a/nginx.conf +++ b/nginx.conf @@ -6,6 +6,10 @@ http { include mime.types; default_type application/octet-stream; + # Ajout pour gérer les fichiers statiques correctement + sendfile on; + keepalive_timeout 65; + server { listen 80; @@ -16,55 +20,27 @@ http { try_files $uri $uri/ /index.html; } - # Routes API pour le backend Rust - location /app_repo { - proxy_pass http://127.0.0.1:5000; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - location /watched_repos { - proxy_pass http://127.0.0.1:5000; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - location /delete_repo { - proxy_pass http://127.0.0.1:5000; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - location /app_docker_repo { - proxy_pass http://127.0.0.1:5000; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - location /watched_docker_repos { - proxy_pass http://127.0.0.1:5000; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - location /delete_docker_repo { - proxy_pass http://127.0.0.1:5000; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - location /latest_updates { + # Configuration groupée pour toutes les routes API + location ~* ^/(app_github_repo|app_docker_repo|watched_repos|watched_docker_repos|delete_repo|delete_docker_repo|latest_updates|auth|settings|is_configured) { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + + # Configuration importante pour les WebSockets si utilisés + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # Augmenter les timeouts pour les requêtes longues + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; } + + # Ajouter des logs pour le débogage + error_log /var/log/nginx/error.log warn; + access_log /var/log/nginx/access.log; } } From 3f069f8c1599b35284bd06270e52e8ed8a1f884a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Mon, 23 Jun 2025 15:26:15 +0200 Subject: [PATCH 08/22] feat(nginx): Improve server configuration for static files and API routing --- .github/workflows/build_pr.yml | 2 +- nginx.conf | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_pr.yml b/.github/workflows/build_pr.yml index 466aa56..6536feb 100644 --- a/.github/workflows/build_pr.yml +++ b/.github/workflows/build_pr.yml @@ -78,7 +78,7 @@ jobs: - name: Vérifier le contenu du répertoire output run: | - ls -la web/.output || echo "Le répertoire .output n'existe pas!" + ls -la web/.output/public || echo "Le répertoire .output n'existe pas!" - name: Upload frontend comme artifact uses: actions/upload-artifact@v4 diff --git a/nginx.conf b/nginx.conf index 0570dd6..99cf65b 100644 --- a/nginx.conf +++ b/nginx.conf @@ -10,23 +10,34 @@ http { sendfile on; keepalive_timeout 65; + # Ajout de cette variable pour préserver le port dans les redirections + port_in_redirect off; + absolute_redirect off; + server { listen 80; + server_name _; # Configuration pour servir le frontend Nuxt statique location / { root /var/www/html; index index.html; try_files $uri $uri/ /index.html; + + # Activer les options pour faciliter le débogage + add_header X-Content-Type-Options "nosniff"; + add_header X-Frame-Options "DENY"; + add_header X-Served-By "nginx"; } # Configuration groupée pour toutes les routes API location ~* ^/(app_github_repo|app_docker_repo|watched_repos|watched_docker_repos|delete_repo|delete_docker_repo|latest_updates|auth|settings|is_configured) { proxy_pass http://127.0.0.1:5000; - proxy_set_header Host $host; + proxy_set_header Host $host:$server_port; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port $server_port; # Configuration importante pour les WebSockets si utilisés proxy_http_version 1.1; From f844365b9c9f748903ab672332b41d4c9910bc66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Mon, 23 Jun 2025 15:46:31 +0200 Subject: [PATCH 09/22] feat(onboarding): Move LatestUpdates and repository sections to a new index.vue component --- web/app.vue | 13 +++---------- web/pages/index.vue | 12 ++++++++++++ 2 files changed, 15 insertions(+), 10 deletions(-) create mode 100644 web/pages/index.vue diff --git a/web/app.vue b/web/app.vue index f082a17..1f6cac2 100644 --- a/web/app.vue +++ b/web/app.vue @@ -3,14 +3,7 @@
- - - - -
- - -
+
@@ -18,9 +11,9 @@ diff --git a/web/pages/index.vue b/web/pages/index.vue new file mode 100644 index 0000000..755b91b --- /dev/null +++ b/web/pages/index.vue @@ -0,0 +1,12 @@ + From 1e6e1191163ef1dc6531df0075382a1d947886d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Mon, 23 Jun 2025 15:55:01 +0200 Subject: [PATCH 10/22] feat(onboarding): Add authentication middleware for route protection --- web/middleware/auth.js | 14 ++++++++++++++ web/middleware/global.global.js | 7 +++++++ 2 files changed, 21 insertions(+) create mode 100644 web/middleware/auth.js create mode 100644 web/middleware/global.global.js diff --git a/web/middleware/auth.js b/web/middleware/auth.js new file mode 100644 index 0000000..e1cdc25 --- /dev/null +++ b/web/middleware/auth.js @@ -0,0 +1,14 @@ +export default defineNuxtRouteMiddleware((to) => { + if (to.path === '/login' || to.path === '/register') { + return; + } + + if (process.client) { + const token = localStorage.getItem('token'); + const user = localStorage.getItem('user'); + + if (!token || !user) { + return navigateTo('/login'); + } + } +}); diff --git a/web/middleware/global.global.js b/web/middleware/global.global.js new file mode 100644 index 0000000..194641d --- /dev/null +++ b/web/middleware/global.global.js @@ -0,0 +1,7 @@ +export default defineNuxtRouteMiddleware(to => { + const { auth } = useNuxtApp().$middleware || {}; + if (auth) { + return auth(to); + } +}); + From edcde5bb52c80759f143c91c7a78238489ebceca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Mon, 23 Jun 2025 16:08:29 +0200 Subject: [PATCH 11/22] feat(onboarding): Add authentication middleware for route protection --- web/middleware/auth.js | 14 -------------- web/middleware/global.global.js | 7 ------- web/nuxt.config.ts | 5 ++++- web/plugins/auth.js | 24 ++++++++++++++++++++++++ 4 files changed, 28 insertions(+), 22 deletions(-) delete mode 100644 web/middleware/auth.js delete mode 100644 web/middleware/global.global.js create mode 100644 web/plugins/auth.js diff --git a/web/middleware/auth.js b/web/middleware/auth.js deleted file mode 100644 index e1cdc25..0000000 --- a/web/middleware/auth.js +++ /dev/null @@ -1,14 +0,0 @@ -export default defineNuxtRouteMiddleware((to) => { - if (to.path === '/login' || to.path === '/register') { - return; - } - - if (process.client) { - const token = localStorage.getItem('token'); - const user = localStorage.getItem('user'); - - if (!token || !user) { - return navigateTo('/login'); - } - } -}); diff --git a/web/middleware/global.global.js b/web/middleware/global.global.js deleted file mode 100644 index 194641d..0000000 --- a/web/middleware/global.global.js +++ /dev/null @@ -1,7 +0,0 @@ -export default defineNuxtRouteMiddleware(to => { - const { auth } = useNuxtApp().$middleware || {}; - if (auth) { - return auth(to); - } -}); - diff --git a/web/nuxt.config.ts b/web/nuxt.config.ts index 2445f80..646bd9a 100644 --- a/web/nuxt.config.ts +++ b/web/nuxt.config.ts @@ -15,5 +15,8 @@ export default defineNuxtConfig({ '@tailwindcss/postcss': {}, autoprefixer: {}, }, - } + }, + plugins: [ + '~/plugins/auth.js' + ] }) \ No newline at end of file diff --git a/web/plugins/auth.js b/web/plugins/auth.js new file mode 100644 index 0000000..512597c --- /dev/null +++ b/web/plugins/auth.js @@ -0,0 +1,24 @@ +// Authentication verification plugin +export default defineNuxtPlugin(() => { + console.log('Authentication plugin loaded'); + + addRouteMiddleware('auth', (to) => { + console.log('Auth middleware executed for route:', to.path); + + if (to.path === '/login' || to.path === '/register') { + return; + } + + if (process.client) { + const token = localStorage.getItem('token'); + const user = localStorage.getItem('user'); + + console.log('Authentication check:', !!token, !!user); + + if (!token || !user) { + console.log('Redirecting to /login'); + return navigateTo('/login'); + } + } + }, { global: true }); +}); From c6945a6948cb10881a18d89947047e6ce4a0796a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Mon, 23 Jun 2025 16:36:16 +0200 Subject: [PATCH 12/22] feat(onboarding): Enhance registration process with admin approval and update AppHeader for authenticated users --- web/components/AppHeader.vue | 37 ++++++++++++++++++++++++++++++++++-- web/composables/useAuth.js | 14 ++++++++++++-- web/pages/register.vue | 29 +++++++++++++++++++++++++--- 3 files changed, 73 insertions(+), 7 deletions(-) diff --git a/web/components/AppHeader.vue b/web/components/AppHeader.vue index dea7631..2a2351e 100644 --- a/web/components/AppHeader.vue +++ b/web/components/AppHeader.vue @@ -1,6 +1,39 @@ + diff --git a/web/composables/useAuth.js b/web/composables/useAuth.js index 969f910..6199c22 100644 --- a/web/composables/useAuth.js +++ b/web/composables/useAuth.js @@ -59,14 +59,19 @@ export const useAuth = () => { }; // Registration function - const register = async (username, password, isAdmin = false) => { + const register = async (username, password, isAdmin = false, isPending = false) => { try { const response = await fetch('/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ username, password, is_admin: isAdmin }), + body: JSON.stringify({ + username, + password, + is_admin: isAdmin, + is_pending: isPending + }), }); if (!response.ok) { @@ -80,6 +85,11 @@ export const useAuth = () => { throw new Error(data.message || 'Registration failed'); } + // If registration is pending, don't store auth info + if (isPending) { + return data; + } + // Store authentication information token.value = data.data.token; user.value = data.data.user; diff --git a/web/pages/register.vue b/web/pages/register.vue index abd3d0f..b7b6d95 100644 --- a/web/pages/register.vue +++ b/web/pages/register.vue @@ -6,6 +6,10 @@

Sign up to manage your notifications

+
+ New accounts require administrator approval. Your registration will be submitted for review. +
+
@@ -68,7 +72,7 @@ block :loading="loading" > - Register + {{ existingUsers && !isAdminUser ? 'Submit Registration Request' : 'Register' }}
@@ -99,15 +103,24 @@ const form = reactive({ const error = ref(''); const loading = ref(false); const existingUsers = ref(false); +const isAdminUser = ref(false); -// Check if users already exist +// Check if users already exist and if current user is admin onMounted(async () => { try { + // Check if admin exists const response = await fetch('/is_configured'); if (response.ok) { const data = await response.json(); existingUsers.value = data.data && data.data.admin_exists; } + + // Check if current user is admin (if logged in) + const userData = localStorage.getItem('user'); + if (userData) { + const user = JSON.parse(userData); + isAdminUser.value = user.is_admin === true; + } } catch (err) { console.error('Error checking for existing users:', err); } @@ -124,11 +137,21 @@ async function handleRegister() { loading.value = true; error.value = ''; - await auth.register(form.username, form.password, existingUsers.value ? false : form.isAdmin); + // If users exist and current user is not admin, set pending flag + const isPending = existingUsers.value && !isAdminUser.value; + + await auth.register(form.username, form.password, existingUsers.value ? false : form.isAdmin, isPending); // Redirect to onboarding page if it's the first user if (!existingUsers.value) { router.push('/onboarding'); + } else if (isPending) { + // Show success message for pending registration + error.value = 'Registration submitted for approval. You will be notified when approved.'; + form.username = ''; + form.password = ''; + form.confirmPassword = ''; + // Don't redirect } else { router.push('/'); } From 844880d1fe31368b4f27174b0d787660e185c2f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Mon, 23 Jun 2025 16:57:10 +0200 Subject: [PATCH 13/22] feat(onboarding): Implement admin account creation step in onboarding process --- web/pages/login.vue | 4 +- web/pages/onboarding.vue | 124 +++++++++++++++++++++++++---- web/pages/register.vue | 164 --------------------------------------- web/plugins/auth.js | 2 +- 4 files changed, 114 insertions(+), 180 deletions(-) delete mode 100644 web/pages/register.vue diff --git a/web/pages/login.vue b/web/pages/login.vue index db1ec93..a0d6338 100644 --- a/web/pages/login.vue +++ b/web/pages/login.vue @@ -48,8 +48,8 @@

First time? - - Create an account + + Setup your application

diff --git a/web/pages/onboarding.vue b/web/pages/onboarding.vue index d991014..9068533 100644 --- a/web/pages/onboarding.vue +++ b/web/pages/onboarding.vue @@ -3,7 +3,7 @@

Application Setup

-

Configure your notification services to start receiving alerts

+

Configure your application and create an administrator account

@@ -14,8 +14,50 @@
- +
+
+

Create Administrator Account

+

This account will have full access to manage the application

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+ + +
- -
+ +
- -
+ +
- -
+ +
{ - if (!auth.isAuthenticated.value) { - router.push('/login'); +// Check if admin exists and redirect accordingly +onMounted(async () => { + try { + const response = await fetch('/is_configured'); + if (response.ok) { + const data = await response.json(); + const adminExists = data.data && data.data.admin_exists; + + // If admin exists, redirect to login or dashboard + if (adminExists) { + if (auth.isAuthenticated.value) { + router.push('/'); + } else { + router.push('/login'); + } + } + } + } catch (err) { + console.error('Error checking configuration:', err); } }); +// Admin user creation data +const adminUser = reactive({ + username: '', + password: '', + confirmPassword: '', +}); + // Onboarding steps const steps = [ + { title: 'Create Admin', description: 'Create your administrator account' }, { title: 'Notification Service', description: 'Choose your main notification service' }, { title: 'GitHub Settings', description: 'Configure options for GitHub' }, { title: 'Docker Hub Settings', description: 'Configure options for Docker Hub' }, @@ -231,9 +296,42 @@ const settings = reactive({ }); // Function to proceed to next step -function nextStep() { +async function nextStep() { // Validate current step if (step.value === 1) { + // Validate admin user creation + if (!adminUser.username) { + error.value = 'Please enter a username'; + return; + } + if (!adminUser.password) { + error.value = 'Please enter a password'; + return; + } + if (adminUser.password !== adminUser.confirmPassword) { + error.value = 'Passwords do not match'; + return; + } + + // Create admin user + try { + error.value = ''; + loading.value = true; + + // Register admin user + await auth.register(adminUser.username, adminUser.password, true); + + // Continue to next step + loading.value = false; + step.value++; + return; + } catch (err) { + error.value = err.message || 'Error creating admin user'; + loading.value = false; + return; + } + } + else if (step.value === 2) { if (!selectedService.value) { error.value = 'Please select a notification service'; return; diff --git a/web/pages/register.vue b/web/pages/register.vue deleted file mode 100644 index b7b6d95..0000000 --- a/web/pages/register.vue +++ /dev/null @@ -1,164 +0,0 @@ - - - diff --git a/web/plugins/auth.js b/web/plugins/auth.js index 512597c..dd86583 100644 --- a/web/plugins/auth.js +++ b/web/plugins/auth.js @@ -5,7 +5,7 @@ export default defineNuxtPlugin(() => { addRouteMiddleware('auth', (to) => { console.log('Auth middleware executed for route:', to.path); - if (to.path === '/login' || to.path === '/register') { + if (to.path === '/login' || to.path === '/onboarding') { return; } From bf8239097cbf96b5f8e73725d76cf2680b0918ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Tue, 24 Jun 2025 13:05:08 +0200 Subject: [PATCH 14/22] feat(onboarding): Add admin onboarding button and enhance onboarding logic for admin users --- web/components/AppHeader.vue | 11 +++++++ web/pages/onboarding.vue | 57 ++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/web/components/AppHeader.vue b/web/components/AppHeader.vue index 2a2351e..57382f5 100644 --- a/web/components/AppHeader.vue +++ b/web/components/AppHeader.vue @@ -4,6 +4,17 @@

Github Ntfy

+ + Onboarding + + const auth = useAuth(); const router = useRouter(); +const route = useRoute(); // Check if admin exists and redirect accordingly onMounted(async () => { try { + // Check if force parameter is present in the URL + const forceOnboarding = route.query.force === 'true'; + + // If forcing onboarding and user is authenticated as admin, allow access + if (forceOnboarding && auth.isAuthenticated.value && auth.isAdmin.value) { + // Load existing settings if available + await loadExistingSettings(); + return; + } + + // Otherwise check if admin exists const response = await fetch('/is_configured'); if (response.ok) { const data = await response.json(); @@ -393,4 +405,49 @@ async function saveSettings() { loading.value = false; } } + +// Function to load existing settings +async function loadExistingSettings() { + try { + if (!auth.isAuthenticated.value) return; + + const response = await fetch('/settings', { + headers: { + 'Authorization': auth.token.value + } + }); + + if (response.ok) { + const data = await response.json(); + if (data.success && data.data) { + // Populate settings with existing values + const existingSettings = data.data; + + // Update notification service selection + if (existingSettings.ntfy_url) { + selectedService.value = 'ntfy'; + settings.ntfy_url = existingSettings.ntfy_url; + } else if (existingSettings.discord_webhook_url) { + selectedService.value = 'discord'; + settings.discord_webhook_url = existingSettings.discord_webhook_url; + } else if (existingSettings.slack_webhook_url) { + selectedService.value = 'slack'; + settings.slack_webhook_url = existingSettings.slack_webhook_url; + } else if (existingSettings.gotify_url) { + selectedService.value = 'gotify'; + settings.gotify_url = existingSettings.gotify_url; + settings.gotify_token = existingSettings.gotify_token; + } + + // Update other settings + settings.github_token = existingSettings.github_token || ''; + settings.docker_username = existingSettings.docker_username || ''; + settings.docker_password = existingSettings.docker_password || ''; + settings.check_interval = existingSettings.check_interval || 3600; + } + } + } catch (err) { + console.error('Error loading existing settings:', err); + } +} From fde4574b76be265402ec1956747709b4e28e419d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Tue, 24 Jun 2025 13:19:46 +0200 Subject: [PATCH 15/22] feat(onboarding): Adjust onboarding steps for admin account creation and notification service selection --- web/pages/onboarding.vue | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/web/pages/onboarding.vue b/web/pages/onboarding.vue index 5ff104d..eabc834 100644 --- a/web/pages/onboarding.vue +++ b/web/pages/onboarding.vue @@ -15,7 +15,7 @@
-
+

Create Administrator Account

This account will have full access to manage the application

@@ -57,16 +57,13 @@
-
+
@@ -133,7 +130,7 @@
-
+
-
+
-
+
@@ -207,7 +204,7 @@
@@ -281,7 +278,7 @@ const steps = [ { title: 'Advanced Settings', description: 'Configure additional options' } ]; -const step = ref(1); +const step = ref(0); const selectedService = ref(null); const error = ref(''); const loading = ref(false); @@ -310,7 +307,7 @@ const settings = reactive({ // Function to proceed to next step async function nextStep() { // Validate current step - if (step.value === 1) { + if (step.value === 0) { // Validate admin user creation if (!adminUser.username) { error.value = 'Please enter a username'; @@ -343,7 +340,7 @@ async function nextStep() { return; } } - else if (step.value === 2) { + else if (step.value === 1) { if (!selectedService.value) { error.value = 'Please select a notification service'; return; From 31d3c34697a52360bf487769bf3635bfbd4691a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Tue, 24 Jun 2025 13:43:00 +0200 Subject: [PATCH 16/22] feat(onboarding): Add NTFY username and password fields to onboarding and update auth handling --- src/database.rs | 21 ++++++++++++++++--- src/models.rs | 1 + web/pages/onboarding.vue | 45 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/src/database.rs b/src/database.rs index fa7bbfa..86c9aca 100644 --- a/src/database.rs +++ b/src/database.rs @@ -70,6 +70,7 @@ pub fn init_databases() -> SqliteResult<(Connection, Connection)> { discord_webhook_url TEXT, slack_webhook_url TEXT, check_interval INTEGER DEFAULT 3600, + auth TEXT, last_updated TEXT NOT NULL )", [], @@ -319,7 +320,7 @@ pub fn get_app_settings(conn: &Connection) -> SqliteResult> let mut stmt = conn.prepare( "SELECT id, ntfy_url, github_token, docker_username, docker_password, gotify_url, gotify_token, discord_webhook_url, slack_webhook_url, - check_interval, last_updated + check_interval, auth, last_updated FROM app_settings WHERE id = 1" )?; @@ -337,7 +338,8 @@ pub fn get_app_settings(conn: &Connection) -> SqliteResult> let discord_webhook_url = row.get(7)?; let slack_webhook_url = row.get(8)?; let check_interval = row.get(9)?; - let last_updated_str: String = row.get(10)?; + let auth = row.get(10)?; + let last_updated_str: String = row.get(11)?; let last_updated = chrono::DateTime::parse_from_rfc3339(&last_updated_str) .map(|dt| dt.with_timezone(&Utc)) .map_err(|e| { @@ -358,6 +360,7 @@ pub fn get_app_settings(conn: &Connection) -> SqliteResult> discord_webhook_url, slack_webhook_url, check_interval, + auth, last_updated, })) } else { @@ -372,7 +375,7 @@ pub fn update_app_settings(conn: &Connection, settings: &AppSettings) -> SqliteR "UPDATE app_settings SET ntfy_url = ?, github_token = ?, docker_username = ?, docker_password = ?, gotify_url = ?, gotify_token = ?, discord_webhook_url = ?, slack_webhook_url = ?, - check_interval = ?, last_updated = ? + check_interval = ?, auth = ?, last_updated = ? WHERE id = 1", rusqlite::params![ settings.ntfy_url, @@ -384,10 +387,22 @@ pub fn update_app_settings(conn: &Connection, settings: &AppSettings) -> SqliteR settings.discord_webhook_url, settings.slack_webhook_url, settings.check_interval, + settings.auth, now ], )?; + // If auth credentials are provided, write them to the auth.txt file + if let Some(auth) = &settings.auth { + if !auth.is_empty() { + if let Err(e) = std::fs::write("/auth.txt", auth) { + log::error!("Error writing to auth.txt file: {}", e); + } else { + log::info!("Successfully updated auth.txt file"); + } + } + } + Ok(()) } diff --git a/src/models.rs b/src/models.rs index 6633b21..77b3b27 100644 --- a/src/models.rs +++ b/src/models.rs @@ -83,6 +83,7 @@ pub struct AppSettings { pub discord_webhook_url: Option, pub slack_webhook_url: Option, pub check_interval: Option, + pub auth: Option, pub last_updated: chrono::DateTime, } diff --git a/web/pages/onboarding.vue b/web/pages/onboarding.vue index eabc834..0b47691 100644 --- a/web/pages/onboarding.vue +++ b/web/pages/onboarding.vue @@ -62,7 +62,7 @@
@@ -78,6 +78,28 @@ class="w-full" />
+
+ + +
+
+ + +

+ Username and password will be used to generate the auth.txt file +

+
@@ -294,6 +316,8 @@ const notificationServices = [ // Application settings const settings = reactive({ ntfy_url: '', + ntfy_username: '', + ntfy_password: '', github_token: '', docker_username: '', docker_password: '', @@ -350,6 +374,9 @@ async function nextStep() { if (selectedService.value === 'ntfy' && !settings.ntfy_url) { error.value = 'Please enter the NTFY URL'; return; + } else if (selectedService.value === 'ntfy' && (!settings.ntfy_username || !settings.ntfy_password)) { + error.value = 'Please enter both NTFY username and password'; + return; } else if (selectedService.value === 'discord' && !settings.discord_webhook_url) { error.value = 'Please enter the Discord webhook URL'; return; @@ -379,6 +406,13 @@ async function saveSettings() { last_updated: now }; + // Format NTFY auth if credentials are provided + if (selectedService.value === 'ntfy' && settings.ntfy_username && settings.ntfy_password) { + // Create auth string in the format expected by the backend + const authString = `${settings.ntfy_username}:${settings.ntfy_password}`; + settingsData.auth = authString; + } + // Send settings to server const response = await fetch('/settings', { method: 'PUT', @@ -424,6 +458,15 @@ async function loadExistingSettings() { if (existingSettings.ntfy_url) { selectedService.value = 'ntfy'; settings.ntfy_url = existingSettings.ntfy_url; + + // Parse auth string if it exists (format: username:password) + if (existingSettings.auth) { + const authParts = existingSettings.auth.split(':'); + if (authParts.length === 2) { + settings.ntfy_username = authParts[0]; + settings.ntfy_password = authParts[1]; + } + } } else if (existingSettings.discord_webhook_url) { selectedService.value = 'discord'; settings.discord_webhook_url = existingSettings.discord_webhook_url; From bfc0c34029246e66e2671d842cab18a66d5d9374 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Tue, 24 Jun 2025 13:56:37 +0200 Subject: [PATCH 17/22] feat(onboarding): Add NTFY username and password fields to settings and update auth handling --- web/pages/login.vue | 4 ++-- web/pages/settings.vue | 49 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/web/pages/login.vue b/web/pages/login.vue index a0d6338..9def9e9 100644 --- a/web/pages/login.vue +++ b/web/pages/login.vue @@ -14,7 +14,7 @@ v-model="form.username" type="text" required - class="block w-full px-3 py-2 mt-1 text-gray-900 placeholder-gray-500 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" + class="block w-full px-3 py-2 mt-1 text-white placeholder-gray-500 bg-gray-700 border border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
@@ -25,7 +25,7 @@ v-model="form.password" type="password" required - class="block w-full px-3 py-2 mt-1 text-gray-900 placeholder-gray-500 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" + class="block w-full px-3 py-2 mt-1 text-white placeholder-gray-500 bg-gray-700 border border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" />
diff --git a/web/pages/settings.vue b/web/pages/settings.vue index 62878b3..d1cbb29 100644 --- a/web/pages/settings.vue +++ b/web/pages/settings.vue @@ -16,12 +16,30 @@

NTFY

- +
+ + + +

+ Username and password will be used to generate the auth.txt file +

+
@@ -175,6 +193,8 @@ onMounted(async () => { const settings = reactive({ ntfy_url: '', + ntfy_username: '', + ntfy_password: '', github_token: '', docker_username: '', docker_password: '', @@ -211,6 +231,15 @@ async function loadSettings() { if (data.success && data.data) { // Update settings with loaded values Object.assign(settings, data.data); + + // Parse NTFY auth string if it exists + if (data.data.auth) { + const authParts = data.data.auth.split(':'); + if (authParts.length === 2) { + settings.ntfy_username = authParts[0]; + settings.ntfy_password = authParts[1]; + } + } } } catch (err) { error.value = err.message || 'An error occurred while loading settings'; @@ -233,6 +262,13 @@ async function saveSettings() { last_updated: now }; + // Format NTFY auth if credentials are provided + if (settings.ntfy_url && settings.ntfy_username && settings.ntfy_password) { + // Create auth string in the format expected by the backend + const authString = `${settings.ntfy_username}:${settings.ntfy_password}`; + settingsData.auth = authString; + } + // Send settings to server const response = await fetch('/settings', { method: 'PUT', @@ -256,4 +292,3 @@ async function saveSettings() { } } - From 288192bd2968883492cda0c536b385e88f21e8cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Tue, 24 Jun 2025 14:06:10 +0200 Subject: [PATCH 18/22] feat(onboarding): Remove admin onboarding button and streamline onboarding logic for existing admin check --- web/components/AppHeader.vue | 11 ----------- web/pages/onboarding.vue | 18 +++++++----------- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/web/components/AppHeader.vue b/web/components/AppHeader.vue index 57382f5..2a2351e 100644 --- a/web/components/AppHeader.vue +++ b/web/components/AppHeader.vue @@ -4,17 +4,6 @@

Github Ntfy

- - Onboarding - - { try { - // Check if force parameter is present in the URL - const forceOnboarding = route.query.force === 'true'; - - // If forcing onboarding and user is authenticated as admin, allow access - if (forceOnboarding && auth.isAuthenticated.value && auth.isAdmin.value) { - // Load existing settings if available - await loadExistingSettings(); - return; - } - - // Otherwise check if admin exists + // Check if admin exists const response = await fetch('/is_configured'); if (response.ok) { const data = await response.json(); const adminExists = data.data && data.data.admin_exists; // If admin exists, redirect to login or dashboard + // This ensures onboarding can only be done once if (adminExists) { if (auth.isAuthenticated.value) { router.push('/'); } else { router.push('/login'); } + return; } } + + // Only load existing settings if we're continuing with onboarding + // (only happens when no admin exists yet) + await loadExistingSettings(); } catch (err) { console.error('Error checking configuration:', err); } From c2a86cb9d47114c7a314a50cf3681fb08fa2b9f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Tue, 24 Jun 2025 14:16:54 +0200 Subject: [PATCH 19/22] feat(onboarding): Update onboarding UI and enhance CI/CD workflows [bump-minor] --- .github/workflows/create_dev.yml | 4 +- .github/workflows/create_release.yml | 57 +++++++++++++++++----- .github/workflows/dependabot-build.yml | 67 ++++++++++++++++++-------- README.md | 18 ++----- web/components/AppHeader.vue | 4 +- 5 files changed, 99 insertions(+), 51 deletions(-) diff --git a/.github/workflows/create_dev.yml b/.github/workflows/create_dev.yml index f35f54e..260ac9e 100644 --- a/.github/workflows/create_dev.yml +++ b/.github/workflows/create_dev.yml @@ -95,7 +95,7 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Configurer Docker + - name: Configurer Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login Docker Hub @@ -104,7 +104,7 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Télécharger l'exécutable binaire + - name: Télécharger le binaire uses: actions/download-artifact@v4 with: name: github-ntfy diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index 113334e..8fc19d3 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -29,18 +29,44 @@ jobs: MINOR=$(echo $VERSION | cut -d. -f2) PATCH=$(echo $VERSION | cut -d. -f3) - # Incrémenter le patch - PATCH=$((PATCH + 1)) + # Récupérer le dernier message de commit + COMMIT_MSG=$(git log -1 --pretty=%B) + + # Déterminer quel niveau de version doit être incrémenté + if echo "$COMMIT_MSG" | grep -q "\[bump-major\]"; then + echo "Incrémentation de la version majeure détectée dans le message de commit" + MAJOR=$((MAJOR + 1)) + MINOR=0 + PATCH=0 + elif echo "$COMMIT_MSG" | grep -q "\[bump-minor\]"; then + echo "Incrémentation de la version mineure détectée dans le message de commit" + MINOR=$((MINOR + 1)) + PATCH=0 + elif echo "$COMMIT_MSG" | grep -q "\[version="; then + # Format personnalisé: [version=X.Y.Z] + CUSTOM_VERSION=$(echo "$COMMIT_MSG" | grep -o '\[version=[0-9]*\.[0-9]*\.[0-9]*\]' | sed 's/\[version=\(.*\)\]/\1/') + if [ ! -z "$CUSTOM_VERSION" ]; then + echo "Version personnalisée détectée: $CUSTOM_VERSION" + MAJOR=$(echo $CUSTOM_VERSION | cut -d. -f1) + MINOR=$(echo $CUSTOM_VERSION | cut -d. -f2) + PATCH=$(echo $CUSTOM_VERSION | cut -d. -f3) + else + # Incrémentation de patch par défaut + PATCH=$((PATCH + 1)) + fi + else + # Incrémentation de patch par défaut + PATCH=$((PATCH + 1)) + fi # Nouvelle version NEW_VERSION="v$MAJOR.$MINOR.$PATCH" echo "Nouvelle version: $NEW_VERSION" echo "tag=$NEW_VERSION" >> $GITHUB_OUTPUT - build-binaries: + build-binary: needs: version runs-on: ubuntu-latest - steps: - name: Checkout code uses: actions/checkout@v4 @@ -109,14 +135,18 @@ jobs: pnpm install pnpm generate + - name: Vérifier le contenu du répertoire output + run: | + ls -la web/.output/public || echo "Le répertoire .output n'existe pas!" + - name: Upload frontend comme artifact uses: actions/upload-artifact@v4 with: name: nuxt-frontend - path: web/.output + path: web/.output/public docker-build-push: - needs: [version, build-binaries, build-frontend] + needs: [version, build-binary, build-frontend] runs-on: ubuntu-latest steps: - name: Checkout code @@ -141,19 +171,20 @@ jobs: uses: actions/download-artifact@v4 with: name: nuxt-frontend - path: web/.output + path: web/.output/public - name: Préparer les fichiers pour Docker run: | chmod +x binaries/github-ntfy mkdir -p docker-build cp binaries/github-ntfy docker-build/ - cp -r web/.output docker-build/web + mkdir -p docker-build/web-output/public + cp -r web/.output/public/* docker-build/web-output/public/ cp nginx.conf docker-build/ cp entrypoint.sh docker-build/ + cp Dockerfile docker-build/ chmod +x docker-build/entrypoint.sh - # Construire et pousser l'image multi-architecture - name: Construire et pousser l'image Docker uses: docker/build-push-action@v6 with: @@ -162,10 +193,10 @@ jobs: tags: | breizhhardware/github-ntfy:latest breizhhardware/github-ntfy:${{ needs.version.outputs.version }} - file: Dockerfile + file: docker-build/Dockerfile create-release: - needs: [version, build-binaries, build-frontend] + needs: [version, build-binary, build-frontend] runs-on: ubuntu-latest steps: - name: Checkout code @@ -181,13 +212,13 @@ jobs: uses: actions/download-artifact@v4 with: name: nuxt-frontend - path: web/.output + path: web/.output/public - name: Préparer les fichiers pour la release run: | mkdir -p release-artifacts cp binaries/github-ntfy release-artifacts/ - tar -czf release-artifacts/frontend.tar.gz -C web/.output . + tar -czf release-artifacts/frontend.tar.gz -C web/.output/public . - name: Créer une release GitHub uses: softprops/action-gh-release@v1 diff --git a/.github/workflows/dependabot-build.yml b/.github/workflows/dependabot-build.yml index 1c90e06..29d4be4 100644 --- a/.github/workflows/dependabot-build.yml +++ b/.github/workflows/dependabot-build.yml @@ -1,15 +1,16 @@ -name: Dependabot Build Check +name: Dependabot Build on: pull_request: - branches: [dev] - -permissions: - contents: read - pull-requests: read + branches: [ 'main', 'dev' ] + paths: + - '**/Cargo.toml' + - '**/Cargo.lock' + - 'web/package.json' + - 'web/pnpm-lock.yaml' jobs: - build: + build-binary: if: ${{ startsWith(github.ref, 'refs/heads/dependabot/') || github.actor == 'dependabot[bot]' }} runs-on: ubuntu-latest steps: @@ -28,18 +29,40 @@ jobs: - name: Créer Cross.toml pour spécifier OpenSSL vendored run: | - cat > Cross.toml << 'EOL' - [target.x86_64-unknown-linux-musl] - image = "ghcr.io/cross-rs/x86_64-unknown-linux-musl:main" - + cat > Cross.toml << 'EOF' [build.env] passthrough = [ - "RUST_BACKTRACE", + "RUSTFLAGS", + "OPENSSL_STATIC", + "OPENSSL_NO_VENDOR" ] - EOL + EOF - - name: Build Backend (Rust) - run: cross build --release --target x86_64-unknown-linux-musl + - name: Construire avec cross et OpenSSL vendored + env: + OPENSSL_STATIC: 1 + RUSTFLAGS: "-C target-feature=+crt-static" + OPENSSL_NO_VENDOR: 0 + run: | + cross build --release --target x86_64-unknown-linux-musl --features vendored-openssl + + - name: Préparer le binaire + run: | + mkdir -p release + cp target/x86_64-unknown-linux-musl/release/github-ntfy release/github-ntfy + + - name: Upload binaire comme artifact + uses: actions/upload-artifact@v4 + with: + name: github-ntfy-dependabot + path: release/github-ntfy + + build-frontend: + if: ${{ github.actor == 'dependabot[bot]' || startsWith(github.ref, 'refs/heads/dependabot/') }} + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 @@ -56,10 +79,14 @@ jobs: run: | cd web pnpm install - pnpm build + pnpm generate - - name: Afficher des informations de débogage + - name: Vérifier le contenu du répertoire output run: | - echo "Acteur: ${{ github.actor }}" - echo "Référence de la branche: ${{ github.head_ref }}" - echo "Event name: ${{ github.event_name }}" \ No newline at end of file + ls -la web/.output/public || echo "Le répertoire .output n'existe pas!" + + - name: Upload frontend comme artifact + uses: actions/upload-artifact@v4 + with: + name: nuxt-frontend-dependabot + path: web/.output/public diff --git a/README.md b/README.md index e569c43..ce07a9d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

Welcome to ntfy_alerts 👋

- Version + Version License: GPL--3 @@ -22,18 +22,6 @@ services: github-ntfy: image: breizhhardware/github-ntfy:latest container_name: github-ntfy - environment: - - USERNAME=username # Required - - PASSWORD=password # Required - - NTFY_URL=ntfy_url # Required if ntfy is used - - GHNTFY_TIMEOUT=timeout # Default is 3600 (1 hour) - - GHNTFY_TOKEN= # Default is empty (Github token) - - DOCKER_USERNAME= # Default is empty (Docker Hub username) - - DOCKER_PASSWORD= # Default is empty (Docker Hub password) - - GOTIFY_URL=gotify_url # Required if gotify is used - - GOTIFY_TOKEN= # Required if gotify is used - - DISCORD_WEBHOOK_URL= # Required if discord is used - - SLACK_WEBHOOK_URL= # Required if Slack is used volumes: - /path/to/data:/data ports: @@ -72,9 +60,9 @@ The GitHub token (GHNTFY_TOKEN) needs to have the following permissions: repo, r ## TODO - [ ] Add support for multi achitecture Docker images -- [ ] Rework web interface +- [x] Rework web interface - [ ] Add support for more notification services (Telegram, Matrix, etc.) -- [ ] Add web oneboarding instead of using environment variables +- [x] Add web oneboarding instead of using environment variables ## Author 👤 BreizhHardware diff --git a/web/components/AppHeader.vue b/web/components/AppHeader.vue index 2a2351e..d77a4fc 100644 --- a/web/components/AppHeader.vue +++ b/web/components/AppHeader.vue @@ -1,7 +1,9 @@