Compare commits

..

1 Commits

Author SHA1 Message Date
Félix MARQUET
66400f2946 feat(notifications): Add test notification routes for NTFY, Discord, Slack, and Gotify 2025-08-21 18:27:07 +02:00
11 changed files with 678 additions and 143 deletions

96
Cargo.lock generated
View File

@@ -132,13 +132,13 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bcrypt"
version = "0.17.0"
version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92758ad6077e4c76a6cadbce5005f666df70d4f13b19976b1a8062eef880040f"
checksum = "e65938ed058ef47d92cf8b346cc76ef48984572ade631927e9937b5ffc7662c7"
dependencies = [
"base64 0.22.1",
"blowfish",
"getrandom 0.3.3",
"getrandom 0.2.16",
"subtle",
"zeroize",
]
@@ -501,7 +501,7 @@ dependencies = [
"env_logger",
"log",
"openssl",
"rand 0.9.2",
"rand",
"reqwest",
"rusqlite",
"serde",
@@ -675,7 +675,7 @@ dependencies = [
"httpdate",
"itoa",
"pin-project-lite",
"socket2 0.5.10",
"socket2",
"tokio",
"tower-service",
"tracing",
@@ -752,7 +752,7 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
"socket2 0.5.10",
"socket2",
"system-configuration",
"tokio",
"tower-service",
@@ -910,17 +910,6 @@ dependencies = [
"generic-array",
]
[[package]]
name = "io-uring"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4"
dependencies = [
"bitflags",
"cfg-if",
"libc",
]
[[package]]
name = "ipnet"
version = "2.11.0"
@@ -991,9 +980,9 @@ checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "libsqlite3-sys"
version = "0.35.0"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f"
checksum = "91632f3b4fb6bd1d72aa3d78f41ffecfcf2b1a6648d8c241dbe7dbfaf4875e15"
dependencies = [
"cc",
"pkg-config",
@@ -1320,18 +1309,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.3",
"rand_chacha",
"rand_core",
]
[[package]]
@@ -1341,17 +1320,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.3",
"rand_core",
]
[[package]]
@@ -1363,15 +1332,6 @@ dependencies = [
"getrandom 0.2.16",
]
[[package]]
name = "rand_core"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom 0.3.3",
]
[[package]]
name = "redox_syscall"
version = "0.5.12"
@@ -1412,9 +1372,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "reqwest"
version = "0.12.22"
version = "0.12.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531"
checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813"
dependencies = [
"base64 0.22.1",
"bytes",
@@ -1468,9 +1428,9 @@ dependencies = [
[[package]]
name = "rusqlite"
version = "0.37.0"
version = "0.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f"
checksum = "3de23c3319433716cf134eed225fe9986bc24f63bed9be9f20c329029e672dc7"
dependencies = [
"bitflags",
"fallible-iterator",
@@ -1610,9 +1570,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.142"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"itoa",
"memchr",
@@ -1683,16 +1643,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "socket2"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
dependencies = [
"libc",
"windows-sys 0.59.0",
]
[[package]]
name = "spin"
version = "0.9.8"
@@ -1808,22 +1758,20 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.47.1"
version = "1.45.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
dependencies = [
"backtrace",
"bytes",
"io-uring",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"slab",
"socket2 0.6.0",
"socket2",
"tokio-macros",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -1965,7 +1913,7 @@ dependencies = [
"http 1.3.1",
"httparse",
"log",
"rand 0.8.5",
"rand",
"sha1",
"thiserror",
"url",

View File

@@ -13,7 +13,7 @@ vendored-openssl = ["openssl/vendored"]
[dependencies]
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", features = ["json", "blocking"] }
rusqlite = { version = "0.37", features = ["bundled"] }
rusqlite = { version = "0.36", features = ["bundled"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
log = "0.4"
@@ -22,5 +22,5 @@ dotenv = "0.15"
chrono = { version = "0.4", features = ["serde"] }
warp = "0.3"
openssl = { version = "0.10", features = ["vendored"] }
rand = "0.9"
bcrypt = "0.17"
rand = "0.8"
bcrypt = "0.15"

View File

@@ -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<dyn std::error::Error + Send + Sync>>
.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<dyn std::error::Error + Send + Sync>>
.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<Mutex<Connection>>) -> Result<impl Reply, Rejecti
StatusCode::OK,
))
}
async fn test_ntfy_notification(db: Arc<Mutex<Connection>>, token: String) -> Result<impl Reply, Rejection> {
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<Mutex<Connection>>, token: String) -> Result<impl Reply, Rejection> {
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<Mutex<Connection>>, token: String) -> Result<impl Reply, Rejection> {
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<Mutex<Connection>>, token: String) -> Result<impl Reply, Rejection> {
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,
))
}
}

View File

@@ -82,4 +82,28 @@ pub async fn send_docker_notification(release: &DockerReleaseInfo, webhook_url:
error!("Error sending to Discord: {}", e);
}
}
}
}
pub async fn send_notification(webhook_url: &str, title: &str, message: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
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())
}
}

View File

@@ -75,4 +75,32 @@ pub async fn send_docker_notification(release: &DockerReleaseInfo, token: &str,
error!("Error sending to Gotify: {}", e);
}
}
}
}
pub async fn send_notification(gotify_url: &str, title: &str, message: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
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())
}
}

View File

@@ -81,4 +81,27 @@ pub async fn send_docker_notification(release: &DockerReleaseInfo, auth: &str, n
error!("Error sending to Ntfy: {}", e);
}
}
}
}
pub async fn send_notification(ntfy_url: &str, title: &str, message: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
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())
}
}

View File

@@ -128,4 +128,27 @@ pub async fn send_docker_notification(release: &DockerReleaseInfo, webhook_url:
error!("Error sending to Slack: {}", e);
}
}
}
}
pub async fn send_notification(webhook_url: &str, title: &str, message: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
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())
}
}

5
web/layouts/auth.vue Normal file
View File

@@ -0,0 +1,5 @@
<template>
<div class="min-h-screen bg-gray-900 text-gray-200 flex items-center justify-center">
<slot />
</div>
</template>

View File

@@ -1,63 +1,64 @@
<template>
<div class="flex items-center justify-center min-h-screen bg-gray-900">
<div class="w-full max-w-md p-8 space-y-8 bg-gray-800 rounded-lg shadow-lg">
<div class="text-center">
<h1 class="text-2xl font-bold text-white">Login</h1>
<p class="mt-2 text-sm text-gray-400">Sign in to manage your notifications</p>
</div>
<form @submit.prevent="handleLogin" class="mt-8 space-y-6">
<div>
<label for="username" class="block text-sm font-medium text-gray-400">Username</label>
<input
id="username"
v-model="form.username"
type="text"
required
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"
/>
</div>
<div>
<label for="password" class="block text-sm font-medium text-gray-400">Password</label>
<input
id="password"
v-model="form.password"
type="password"
required
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"
/>
</div>
<div v-if="error" class="p-3 text-sm text-red-500 bg-red-100 rounded-md">
{{ error }}
</div>
<div>
<UButton
type="submit"
color="primary"
block
:loading="loading"
>
Login
</UButton>
</div>
</form>
<div class="text-center mt-4">
<p class="text-sm text-gray-400">
First time?
<NuxtLink to="/onboarding" class="font-medium text-indigo-400 hover:text-indigo-300">
Setup your application
</NuxtLink>
</p>
</div>
<div class="w-full max-w-md p-8 space-y-8 bg-gray-800 rounded-lg shadow-lg">
<div class="text-center">
<h1 class="text-2xl font-bold text-white">Login</h1>
<p class="mt-2 text-sm text-gray-400">Sign in to manage your notifications</p>
</div>
</div>
<form @submit.prevent="handleLogin" class="mt-8 space-y-6">
<div>
<input
id="username"
v-model="form.username"
type="text"
required
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"
/>
</div>
<div>
<div>
<label for="password" class="block text-sm font-medium text-gray-400">Password</label>
<div v-if="error" class="p-3 text-sm text-red-500 bg-red-100 rounded-md">
id="password"
v-model="form.password"
type="password"
required
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"
/>
</div>
{{ error }}
<div v-if="error" class="p-3 text-sm text-red-500 bg-red-100 rounded-md">
{{ error }}
</div>
<UButton
<div>
<UButton
type="submit"
color="primary"
block
:loading="loading"
>
Login
</UButton>
</div>
</form>
<p class="text-sm text-gray-400">
<div class="text-center mt-4">
<p class="text-sm text-gray-400">
First time?
<NuxtLink to="/onboarding" class="font-medium text-indigo-400 hover:text-indigo-300">
Setup your application
</NuxtLink>
</p>
</template>
<script setup>
// Utiliser le layout d'authentification
definePageMeta({
layout: 'auth'
})
const auth = useAuth();
const router = useRouter();

View File

@@ -1,5 +1,5 @@
<template>
<div class="min-h-screen bg-gray-900 p-6">
<div class="min-h-screen p-6">
<div class="max-w-3xl mx-auto bg-gray-800 rounded-lg shadow-lg overflow-hidden">
<div class="p-6 border-b border-gray-700">
<h1 class="text-2xl font-bold text-white">Application Setup</h1>
@@ -247,6 +247,11 @@
</template>
<script setup>
// Utiliser le layout d'authentification
definePageMeta({
layout: 'auth'
})
const auth = useAuth();
const router = useRouter();
const route = useRoute();

View File

@@ -1,6 +1,5 @@
<template>
<div>
<AppHeader />
<div class="container mx-auto px-4 py-8">
<h1 class="text-2xl font-bold text-white mb-8">Settings</h1>
@@ -15,7 +14,19 @@
<div class="space-y-6">
<!-- NTFY -->
<div>
<h3 class="text-lg font-medium mb-2">NTFY</h3>
<div class="flex justify-between items-center mb-2">
<h3 class="text-lg font-medium">NTFY</h3>
<UButton
@click="testNotification('ntfy')"
size="sm"
color="gray"
variant="outline"
:loading="testingNotifications.ntfy"
:disabled="!settings.ntfy_url"
>
Tester
</UButton>
</div>
<div class="space-y-2">
<UInput
v-model="settings.ntfy_url"
@@ -44,7 +55,19 @@
<!-- Discord -->
<div>
<h3 class="text-lg font-medium mb-2">Discord</h3>
<div class="flex justify-between items-center mb-2">
<h3 class="text-lg font-medium">Discord</h3>
<UButton
@click="testNotification('discord')"
size="sm"
color="gray"
variant="outline"
:loading="testingNotifications.discord"
:disabled="!settings.discord_webhook_url"
>
Tester
</UButton>
</div>
<UInput
v-model="settings.discord_webhook_url"
label="Discord Webhook URL"
@@ -55,7 +78,19 @@
<!-- Slack -->
<div>
<h3 class="text-lg font-medium mb-2">Slack</h3>
<div class="flex justify-between items-center mb-2">
<h3 class="text-lg font-medium">Slack</h3>
<UButton
@click="testNotification('slack')"
size="sm"
color="gray"
variant="outline"
:loading="testingNotifications.slack"
:disabled="!settings.slack_webhook_url"
>
Tester
</UButton>
</div>
<UInput
v-model="settings.slack_webhook_url"
label="Slack Webhook URL"
@@ -66,7 +101,19 @@
<!-- Gotify -->
<div>
<h3 class="text-lg font-medium mb-2">Gotify</h3>
<div class="flex justify-between items-center mb-2">
<h3 class="text-lg font-medium">Gotify</h3>
<UButton
@click="testNotification('gotify')"
size="sm"
color="gray"
variant="outline"
:loading="testingNotifications.gotify"
:disabled="!settings.gotify_url || !settings.gotify_token"
>
Tester
</UButton>
</div>
<div class="space-y-2">
<UInput
v-model="settings.gotify_url"
@@ -208,6 +255,12 @@ const settings = reactive({
const error = ref('');
const success = ref('');
const loading = ref(false);
const testingNotifications = reactive({
ntfy: false,
discord: false,
slack: false,
gotify: false
});
// Load current settings
async function loadSettings() {
@@ -291,4 +344,41 @@ async function saveSettings() {
loading.value = false;
}
}
// Function to test notifications
async function testNotification(type) {
try {
// Set loading state for the specific notification type
testingNotifications[type] = true;
error.value = '';
success.value = '';
// Send test notification via our API endpoint
const response = await fetch(`/test/${type}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': auth.token.value
}
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.message || 'Error sending test notification');
}
const data = await response.json();
if (data.success) {
success.value = `Notification de test envoyée avec succès via ${type.toUpperCase()}`;
} else {
throw new Error(data.message || 'Error sending test notification');
}
} catch (err) {
error.value = err.message || 'Une erreur est survenue lors de l\'envoi de la notification de test';
} finally {
// Reset loading state for the specific notification type
testingNotifications[type] = false;
}
}
</script>