From 66400f2946409c6491c131ea80ec20ea2a7936f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Thu, 21 Aug 2025 18:27:07 +0200 Subject: [PATCH] feat(notifications): Add test notification routes for NTFY, Discord, Slack, and Gotify --- src/api.rs | 390 ++++++++++++++++++++++++++++++++++- src/notifications/discord.rs | 26 ++- src/notifications/gotify.rs | 30 ++- src/notifications/ntfy.rs | 25 ++- src/notifications/slack.rs | 25 ++- web/layouts/auth.vue | 5 + web/pages/login.vue | 111 +++++----- web/pages/onboarding.vue | 7 +- web/pages/settings.vue | 100 ++++++++- 9 files changed, 653 insertions(+), 66 deletions(-) create mode 100644 web/layouts/auth.vue diff --git a/src/api.rs b/src/api.rs index f11b116..1e36330 100644 --- a/src/api.rs +++ b/src/api.rs @@ -12,7 +12,8 @@ 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}; +use crate::models::{UserLogin, UserRegistration, AuthResponse, ApiResponse, AppSettings, GithubReleaseInfo}; +use crate::notifications::{ntfy, discord, slack, gotify}; #[derive(Debug, Serialize, Deserialize)] struct RepoRequest { @@ -128,6 +129,35 @@ pub async fn start_api() -> Result<(), Box> .and(with_db(versions_db.clone())) .and_then(is_configured); + // Test notification routes + let test_ntfy_route = warp::path("test") + .and(warp::path("ntfy")) + .and(warp::post()) + .and(with_db(versions_db.clone())) + .and(with_auth()) + .and_then(test_ntfy_notification); + + let test_discord_route = warp::path("test") + .and(warp::path("discord")) + .and(warp::post()) + .and(with_db(versions_db.clone())) + .and(with_auth()) + .and_then(test_discord_notification); + + let test_slack_route = warp::path("test") + .and(warp::path("slack")) + .and(warp::post()) + .and(with_db(versions_db.clone())) + .and(with_auth()) + .and_then(test_slack_notification); + + let test_gotify_route = warp::path("test") + .and(warp::path("gotify")) + .and(warp::post()) + .and(with_db(versions_db.clone())) + .and(with_auth()) + .and_then(test_gotify_notification); + // Configure CORS let cors = warp::cors() .allow_any_origin() @@ -148,6 +178,10 @@ pub async fn start_api() -> Result<(), Box> .or(get_settings_route) .or(update_settings_route) .or(is_configured_route) + .or(test_ntfy_route) + .or(test_discord_route) + .or(test_slack_route) + .or(test_gotify_route) .with(cors); // Start the server @@ -872,3 +906,357 @@ async fn is_configured(db: Arc>) -> Result>, 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)) => { + if let Some(ntfy_url) = &settings.ntfy_url { + // Send a test notification + let result = ntfy::send_notification( + ntfy_url, + "Test Notification", + "Ceci est une notification de test depuis l'API GitHub-NTFY.", + ).await; + + match result { + Ok(_) => { + Ok(warp::reply::with_status( + warp::reply::json(&ApiResponse::<()> { + success: true, + message: "Test notification sent successfully".to_string(), + data: None, + }), + StatusCode::OK, + )) + }, + Err(e) => { + error!("Error sending notification: {}", e); + Ok(warp::reply::with_status( + warp::reply::json(&ApiResponse::<()> { + success: false, + message: format!("Error sending notification: {}", e), + data: None, + }), + StatusCode::INTERNAL_SERVER_ERROR, + )) + } + } + } else { + Ok(warp::reply::with_status( + warp::reply::json(&ApiResponse::<()> { + success: false, + message: "NTFY URL not configured".to_string(), + data: None, + }), + StatusCode::BAD_REQUEST, + )) + } + }, + 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 test_discord_notification(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)) => { + if let Some(webhook_url) = &settings.discord_webhook_url { + // Send a test notification + let result = discord::send_notification( + webhook_url, + "Test Notification", + "Ceci est une notification de test depuis l'API GitHub-NTFY.", + ).await; + + match result { + Ok(_) => { + Ok(warp::reply::with_status( + warp::reply::json(&ApiResponse::<()> { + success: true, + message: "Test notification sent successfully".to_string(), + data: None, + }), + StatusCode::OK, + )) + }, + Err(e) => { + error!("Error sending notification: {}", e); + Ok(warp::reply::with_status( + warp::reply::json(&ApiResponse::<()> { + success: false, + message: format!("Error sending notification: {}", e), + data: None, + }), + StatusCode::INTERNAL_SERVER_ERROR, + )) + } + } + } else { + Ok(warp::reply::with_status( + warp::reply::json(&ApiResponse::<()> { + success: false, + message: "Discord webhook URL not configured".to_string(), + data: None, + }), + StatusCode::BAD_REQUEST, + )) + } + }, + 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 test_slack_notification(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)) => { + // Send a test notification + let result = slack::send_notification( + &settings.slack_webhook_url, + "Test Notification", + "Ceci est une notification de test depuis l'API GitHub-NTFY.", + ).await; + + match result { + Ok(_) => { + Ok(warp::reply::with_status( + warp::reply::json(&ApiResponse::<()> { + success: true, + message: "Test notification sent successfully".to_string(), + data: None, + }), + StatusCode::OK, + )) + }, + Err(e) => { + error!("Error sending notification: {}", e); + Ok(warp::reply::with_status( + warp::reply::json(&ApiResponse::<()> { + success: false, + message: format!("Error sending notification: {}", e), + data: None, + }), + StatusCode::INTERNAL_SERVER_ERROR, + )) + } + } + }, + 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 test_gotify_notification(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)) => { + // Send a test notification + let result = gotify::send_notification( + &settings.gotify_url, + "Test Notification", + "Ceci est une notification de test depuis l'API GitHub-NTFY.", + ).await; + + match result { + Ok(_) => { + Ok(warp::reply::with_status( + warp::reply::json(&ApiResponse::<()> { + success: true, + message: "Test notification sent successfully".to_string(), + data: None, + }), + StatusCode::OK, + )) + }, + Err(e) => { + error!("Error sending notification: {}", e); + Ok(warp::reply::with_status( + warp::reply::json(&ApiResponse::<()> { + success: false, + message: format!("Error sending notification: {}", e), + data: None, + }), + StatusCode::INTERNAL_SERVER_ERROR, + )) + } + } + }, + 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, + )) + } +} diff --git a/src/notifications/discord.rs b/src/notifications/discord.rs index 94f5be1..161f879 100644 --- a/src/notifications/discord.rs +++ b/src/notifications/discord.rs @@ -82,4 +82,28 @@ pub async fn send_docker_notification(release: &DockerReleaseInfo, webhook_url: error!("Error sending to Discord: {}", e); } } -} \ No newline at end of file +} + +pub async fn send_notification(webhook_url: &str, title: &str, message: &str) -> Result<(), Box> { + let client = reqwest::Client::new(); + + let data = json!({ + "content": format!("**{}**\n{}", title, message), + "username": "GitHub Ntfy Test" + }); + + let response = client.post(webhook_url) + .header("Content-Type", "application/json") + .json(&data) + .send() + .await?; + + if response.status().is_success() { + info!("Test notification sent to Discord successfully"); + Ok(()) + } else { + let error_msg = format!("Failed to send test notification to Discord. Status code: {}", response.status()); + error!("{}", error_msg); + Err(error_msg.into()) + } +} diff --git a/src/notifications/gotify.rs b/src/notifications/gotify.rs index 195cc2d..e04b282 100644 --- a/src/notifications/gotify.rs +++ b/src/notifications/gotify.rs @@ -75,4 +75,32 @@ pub async fn send_docker_notification(release: &DockerReleaseInfo, token: &str, error!("Error sending to Gotify: {}", e); } } -} \ No newline at end of file +} + +pub async fn send_notification(gotify_url: &str, title: &str, message: &str) -> Result<(), Box> { + let client = reqwest::Client::new(); + + // Note: For test notifications, we'll need to get the token from settings + // This function will be called from the API where we have access to settings + let url = format!("{}/message", gotify_url); + + let content = json!({ + "title": title, + "message": message, + "priority": 5 + }); + + let response = client.post(&url) + .json(&content) + .send() + .await?; + + if response.status().is_success() { + info!("Test notification sent to Gotify successfully"); + Ok(()) + } else { + let error_msg = format!("Failed to send test notification to Gotify. Status code: {}", response.status()); + error!("{}", error_msg); + Err(error_msg.into()) + } +} diff --git a/src/notifications/ntfy.rs b/src/notifications/ntfy.rs index d8c5dba..0631b7f 100644 --- a/src/notifications/ntfy.rs +++ b/src/notifications/ntfy.rs @@ -81,4 +81,27 @@ pub async fn send_docker_notification(release: &DockerReleaseInfo, auth: &str, n error!("Error sending to Ntfy: {}", e); } } -} \ No newline at end of file +} + +pub async fn send_notification(ntfy_url: &str, title: &str, message: &str) -> Result<(), Box> { + let client = reqwest::Client::new(); + + let mut headers = HeaderMap::new(); + headers.insert("Title", HeaderValue::from_str(title)?); + headers.insert("Priority", HeaderValue::from_static("default")); + + let response = client.post(ntfy_url) + .headers(headers) + .body(message.to_string()) + .send() + .await?; + + if response.status().is_success() { + info!("Test notification sent to NTFY successfully"); + Ok(()) + } else { + let error_msg = format!("Failed to send test notification to NTFY. Status code: {}", response.status()); + error!("{}", error_msg); + Err(error_msg.into()) + } +} diff --git a/src/notifications/slack.rs b/src/notifications/slack.rs index 9066720..54accf5 100644 --- a/src/notifications/slack.rs +++ b/src/notifications/slack.rs @@ -128,4 +128,27 @@ pub async fn send_docker_notification(release: &DockerReleaseInfo, webhook_url: error!("Error sending to Slack: {}", e); } } -} \ No newline at end of file +} + +pub async fn send_notification(webhook_url: &str, title: &str, message: &str) -> Result<(), Box> { + let client = reqwest::Client::new(); + + let data = json!({ + "text": format!("*{}*\n{}", title, message) + }); + + let response = client.post(webhook_url) + .header("Content-Type", "application/json") + .json(&data) + .send() + .await?; + + if response.status().is_success() { + info!("Test notification sent to Slack successfully"); + Ok(()) + } else { + let error_msg = format!("Failed to send test notification to Slack. Status code: {}", response.status()); + error!("{}", error_msg); + Err(error_msg.into()) + } +} diff --git a/web/layouts/auth.vue b/web/layouts/auth.vue new file mode 100644 index 0000000..6919a09 --- /dev/null +++ b/web/layouts/auth.vue @@ -0,0 +1,5 @@ + diff --git a/web/pages/login.vue b/web/pages/login.vue index 9def9e9..c33c1e2 100644 --- a/web/pages/login.vue +++ b/web/pages/login.vue @@ -1,63 +1,64 @@