Path: blob/master/gate-controller/src-tauri/src/webhook_server.rs
1072 views
use tiny_http::{Server, Response, Request, Header, Method};1use tauri::{AppHandle, Manager, State, Emitter};2use std::io::{Cursor, Read};3use serde_json::json;4use chrono::Utc;56use crate::config;7use crate::relay::{self, ConnectionManager, RelayActionPayload};89#[derive(Clone, serde::Serialize)]10struct WebhookLog {11timestamp: String,12success: bool,13details: String,14error_message: Option<String>,15}1617#[derive(serde::Deserialize)]18struct WebhookPayload {19id: String,20action: String,21channel: Option<u8>,22toggle: Option<bool>,23period: Option<u64>,24}2526fn handle_request(req: &mut Request, app_handle: &AppHandle) -> Response<Cursor<Vec<u8>>> {27let log_and_emit = |success: bool, details: String, error_message: Option<String>| {28let log_entry = WebhookLog {29timestamp: Utc::now().to_rfc3339(),30success,31details: details.clone(),32error_message,33};3435// Emit event to frontend36if let Err(e) = app_handle.emit("webhook-log", &log_entry) {37eprintln!("Failed to emit event: {}", e);38}3940// Append to log file41if let Err(e) = config::append_to_log(app_handle, &format!(42"{} - Success: {} - Details: {} - Error: {}",43log_entry.timestamp,44log_entry.success,45log_entry.details,46log_entry.error_message.as_deref().unwrap_or("N/A")47)) {48eprintln!("Failed to write to log file: {}", e);49}50};5152let json_error = |status_code: i32, message: &str, details: String| {53log_and_emit(false, details, Some(message.to_string()));54let response_json = json!({55"status": "error",56"message": message57});58let response_body = serde_json::to_string(&response_json).unwrap_or_default();59let mut response = Response::from_string(response_body).with_status_code(status_code);60let json_header = Header::from_bytes(b"Content-Type", b"application/json").unwrap();61response.add_header(json_header);62response63};6465// --- Auth Check ---66let config = match config::load_config(app_handle) {67Ok(c) => c,68Err(e) => return json_error(500, "Failed to load configuration", e),69};7071let stored_token = match config.webhook_token {72Some(token) if !token.is_empty() => token,73_ => {74return json_error(75401,76"Unauthorized: Webhook token not configured",77"Authentication failed".to_string(),78);79}80};8182let auth_header = req.headers().iter().find(|h| h.field.equiv("Authorization"));83if let Some(header) = auth_header {84let header_val = header.value.as_str();85if header_val.starts_with("Bearer ") {86let token = &header_val[7..];87if token != stored_token.as_str() {88return json_error(89401,90"Unauthorized: Invalid token",91"Authentication failed".to_string(),92);93}94} else {95return json_error(96401,97"Unauthorized: Invalid auth header format",98"Authentication failed".to_string(),99);100}101} else {102return json_error(103401,104"Unauthorized: Missing Authorization header",105"Authentication failed".to_string(),106);107}108109// --- POS Health Check ---110// Convert the URL to lowercase for case-insensitive checking.111if req.url().to_lowercase().contains("?check=true") {112log_and_emit(true, "Health check successful".to_string(), None);113let response_json = json!({114"status": "ok",115"message": "Webhook server is running and token is valid."116});117let response_body = serde_json::to_string(&response_json).unwrap_or_default();118let mut response = Response::from_string(response_body).with_status_code(200);119let json_header = Header::from_bytes(b"Content-Type", b"application/json").unwrap();120response.add_header(json_header);121return response;122}123124// --- Request Handling ---125// Get only the part of the URL before '?' for validation.126let path = req.url().split('?').next().unwrap_or("");127if req.method() != &Method::Post || path != "/webhook" {128return json_error(129404,130"Not Found: Expected POST /webhook",131format!("Invalid request: {} {}", req.method(), req.url()),132);133}134135let mut content = String::new();136if let Err(e) = req.as_reader().read_to_string(&mut content) {137return json_error(138400,139&format!("Bad Request: Failed to read body: {}", e),140"Request body error".to_string(),141);142}143144let payload: WebhookPayload = match serde_json::from_str(&content) {145Ok(p) => p,146Err(e) => return json_error(400, &format!("Bad Request: Invalid JSON: {}", e), content),147};148149let details = format!(150"Trigger relay '{}', action '{}', channel {:?}, toggle {:?}, period {:?}",151payload.id, payload.action, payload.channel, payload.toggle, payload.period152);153154// --- Relay Trigger ---155let connections_state: State<ConnectionManager> = app_handle.state();156let relay_payload = RelayActionPayload {157id: payload.id.clone(),158action: payload.action.clone(),159channel: payload.channel,160toggle: payload.toggle,161period: payload.period,162};163164match relay::trigger_relay_action(app_handle.clone(), connections_state, relay_payload) {165Ok(_) => {166log_and_emit(true, details, None);167let response_json = json!({168"status": "success",169"id": payload.id,170"action": payload.action171});172let response_body = serde_json::to_string(&response_json).unwrap_or_default();173let mut response = Response::from_string(response_body);174let json_header = Header::from_bytes(b"Content-Type", b"application/json").unwrap();175response.add_header(json_header);176response177}178Err(e) => json_error(500, &e, details),179}180}181182pub fn start_server(app_handle: AppHandle) {183std::thread::spawn(move || {184let server = match Server::http("0.0.0.0:4848") {185Ok(s) => s,186Err(e) => {187eprintln!("Failed to start webhook server: {}", e);188return;189}190};191println!("Webhook server started on http://0.0.0.0:4848");192193for mut request in server.incoming_requests() {194let response = handle_request(&mut request, &app_handle);195if let Err(e) = request.respond(response) {196eprintln!("Failed to send response: {}", e);197}198}199});200}201202203