351 lines
8.2 KiB
Go
351 lines
8.2 KiB
Go
//go:build windows
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"html/template"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type NotifyRequest struct {
|
|
Title string `json:"title"`
|
|
Message string `json:"message"`
|
|
TargetUser string `json:"target_user"`
|
|
}
|
|
|
|
type PageData struct {
|
|
ClientHost string
|
|
Port string
|
|
Title string
|
|
Message string
|
|
TargetUser string
|
|
|
|
Status string
|
|
Error string
|
|
}
|
|
|
|
var (
|
|
tpl *template.Template
|
|
notifyToken string
|
|
defaultPort = "24000"
|
|
listenAddr = ":8088"
|
|
defaultTitle = "Benachrichtigung"
|
|
)
|
|
|
|
func main() {
|
|
notifyToken = os.Getenv("NOTIFY_TOKEN")
|
|
|
|
if v := os.Getenv("SENDER_LISTEN_ADDR"); v != "" {
|
|
listenAddr = v
|
|
}
|
|
if v := os.Getenv("DEFAULT_PORT"); v != "" {
|
|
defaultPort = v
|
|
}
|
|
|
|
var err error
|
|
tpl, err = template.New("index").Parse(pageHTML)
|
|
if err != nil {
|
|
log.Fatalf("template parse error: %v", err)
|
|
}
|
|
|
|
http.HandleFunc("/", indexHandler)
|
|
|
|
log.Printf("Notification-Sender Web UI läuft auf http://localhost%s/", listenAddr)
|
|
log.Fatalf("ListenAndServe: %v", http.ListenAndServe(listenAddr, nil))
|
|
}
|
|
|
|
func indexHandler(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
render(w, PageData{
|
|
Port: defaultPort,
|
|
})
|
|
case http.MethodPost:
|
|
if err := r.ParseForm(); err != nil {
|
|
render(w, PageData{
|
|
Error: "Formular konnte nicht gelesen werden: " + err.Error(),
|
|
Port: defaultPort,
|
|
})
|
|
return
|
|
}
|
|
|
|
data := PageData{
|
|
ClientHost: strings.TrimSpace(r.FormValue("client_host")),
|
|
Port: strings.TrimSpace(r.FormValue("port")),
|
|
Title: strings.TrimSpace(r.FormValue("title")),
|
|
Message: strings.TrimSpace(r.FormValue("message")),
|
|
TargetUser: strings.TrimSpace(r.FormValue("target_user")),
|
|
}
|
|
|
|
if data.Port == "" {
|
|
data.Port = defaultPort
|
|
}
|
|
|
|
if data.ClientHost == "" {
|
|
data.Error = "Client-Host/IP darf nicht leer sein."
|
|
render(w, data)
|
|
return
|
|
}
|
|
if data.Message == "" {
|
|
data.Error = "Nachricht darf nicht leer sein."
|
|
render(w, data)
|
|
return
|
|
}
|
|
if data.Title == "" {
|
|
data.Title = defaultTitle
|
|
}
|
|
|
|
if err := sendNotification(data); err != nil {
|
|
data.Error = "Fehler beim Senden: " + err.Error()
|
|
} else {
|
|
data.Status = "Benachrichtigung wurde gesendet."
|
|
}
|
|
|
|
render(w, data)
|
|
default:
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
func render(w http.ResponseWriter, data PageData) {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
if err := tpl.Execute(w, data); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func sendNotification(d PageData) error {
|
|
// Port validieren (nur zur Sicherheit, falls jemand Unsinn einträgt)
|
|
if _, err := strconv.Atoi(d.Port); err != nil {
|
|
return err
|
|
}
|
|
|
|
// URL bauen:
|
|
// Wenn ClientHost schon ein Schema enthält (http://...), nutzen wir das,
|
|
// sonst "http://<host>:<port>/api/notify"
|
|
var url string
|
|
if strings.Contains(d.ClientHost, "://") {
|
|
url = strings.TrimRight(d.ClientHost, "/") + "/api/notify"
|
|
} else {
|
|
url = "http://" + d.ClientHost + ":" + d.Port + "/api/notify"
|
|
}
|
|
|
|
bodyStruct := NotifyRequest{
|
|
Title: d.Title,
|
|
Message: d.Message,
|
|
TargetUser: d.TargetUser,
|
|
}
|
|
body, err := json.Marshal(bodyStruct)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
if notifyToken != "" {
|
|
req.Header.Set("X-Notify-Token", notifyToken)
|
|
}
|
|
|
|
client := &http.Client{
|
|
Timeout: 5 * time.Second,
|
|
}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 300 {
|
|
return &httpError{Status: resp.Status}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type httpError struct {
|
|
Status string
|
|
}
|
|
|
|
func (e *httpError) Error() string {
|
|
return "HTTP-Status vom Client: " + e.Status
|
|
}
|
|
|
|
const pageHTML = `<!doctype html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Notification Sender</title>
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<style>
|
|
:root {
|
|
--bg: #0f172a;
|
|
--card: #111827;
|
|
--border: #1f2937;
|
|
--fg: #e5e7eb;
|
|
--muted: #9ca3af;
|
|
--accent: #3b82f6;
|
|
--error: #f87171;
|
|
--success: #4ade80;
|
|
}
|
|
*{box-sizing:border-box}
|
|
body{
|
|
margin:0;
|
|
padding:24px;
|
|
font-family: system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,sans-serif;
|
|
background: radial-gradient(circle at top, #1f2937 0, #020617 55%);
|
|
color:var(--fg);
|
|
display:flex;
|
|
justify-content:center;
|
|
}
|
|
.shell{
|
|
width:100%;
|
|
max-width: 640px;
|
|
}
|
|
h1{
|
|
margin:0 0 8px 0;
|
|
font-size:22px;
|
|
letter-spacing:0.03em;
|
|
}
|
|
p.subtitle{
|
|
margin:0 0 16px 0;
|
|
color:var(--muted);
|
|
font-size:14px;
|
|
}
|
|
.card{
|
|
background: linear-gradient(135deg, rgba(15,23,42,0.96), rgba(15,23,42,0.9));
|
|
border-radius:18px;
|
|
border:1px solid var(--border);
|
|
padding:20px 18px 18px 18px;
|
|
box-shadow: 0 20px 45px rgba(0,0,0,0.55);
|
|
}
|
|
label{
|
|
display:block;
|
|
font-size:13px;
|
|
text-transform:uppercase;
|
|
letter-spacing:.08em;
|
|
color:var(--muted);
|
|
margin-bottom:4px;
|
|
}
|
|
input[type=text], textarea{
|
|
width:100%;
|
|
background:#020617;
|
|
border-radius:10px;
|
|
border:1px solid #1f2937;
|
|
padding:7px 9px;
|
|
color: var(--fg);
|
|
font-size:14px;
|
|
outline:none;
|
|
}
|
|
input[type=text]:focus, textarea:focus{
|
|
border-color: var(--accent);
|
|
box-shadow:0 0 0 1px rgba(59,130,246,0.35);
|
|
}
|
|
textarea{
|
|
min-height:80px;
|
|
resize:vertical;
|
|
}
|
|
.row{
|
|
display:flex;
|
|
gap:10px;
|
|
}
|
|
.field{
|
|
margin-bottom:12px;
|
|
flex:1;
|
|
}
|
|
button{
|
|
margin-top:6px;
|
|
border:none;
|
|
padding:9px 16px;
|
|
border-radius:999px;
|
|
background: radial-gradient(circle at top left, #60a5fa, #2563eb);
|
|
color:white;
|
|
font-weight:600;
|
|
font-size:14px;
|
|
cursor:pointer;
|
|
display:inline-flex;
|
|
align-items:center;
|
|
gap:6px;
|
|
}
|
|
button:hover{
|
|
filter:brightness(1.05);
|
|
}
|
|
.status{
|
|
margin-top:10px;
|
|
font-size:13px;
|
|
}
|
|
.status.ok{color:var(--success);}
|
|
.status.err{color:var(--error);}
|
|
.badge{
|
|
display:inline-block;
|
|
font-size:11px;
|
|
padding:2px 7px;
|
|
border-radius:999px;
|
|
background:#111827;
|
|
border:1px solid #1f2937;
|
|
color:var(--muted);
|
|
margin-left:6px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="shell">
|
|
<h1>Notification Sender</h1>
|
|
<p class="subtitle">Sende Benachrichtigungen an deine GoSysInfo-Clients (POST /api/notify).</p>
|
|
<div class="card">
|
|
<form method="POST" action="/">
|
|
<div class="row">
|
|
<div class="field">
|
|
<label for="client_host">Client Host / IP</label>
|
|
<input type="text" id="client_host" name="client_host" value="{{.ClientHost}}" placeholder="z.B. pc-123.stadt-hilden.de oder 10.10.10.5">
|
|
</div>
|
|
<div class="field" style="max-width:120px;">
|
|
<label for="port">Port</label>
|
|
<input type="text" id="port" name="port" value="{{.Port}}">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label for="title">Titel</label>
|
|
<input type="text" id="title" name="title" value="{{.Title}}" placeholder="optional, Standard: Benachrichtigung">
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label for="message">Nachricht</label>
|
|
<textarea id="message" name="message" placeholder="Text für den Toast">{{.Message}}</textarea>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label for="target_user">Target User (optional)</label>
|
|
<input type="text" id="target_user" name="target_user" value="{{.TargetUser}}" placeholder="z.B. DOMAIN\user oder leer für alle">
|
|
</div>
|
|
|
|
<button type="submit">
|
|
<span>Notification senden</span>
|
|
<span>➜</span>
|
|
</button>
|
|
|
|
{{if .Status}}
|
|
<div class="status ok">{{.Status}}</div>
|
|
{{end}}
|
|
{{if .Error}}
|
|
<div class="status err">{{.Error}}</div>
|
|
{{end}}
|
|
</form>
|
|
<div style="margin-top:10px;font-size:11px;color:var(--muted);">
|
|
Hinweis: Dieses Tool sendet JSON an <code>http://<Client>:<Port>/api/notify</code> und nutzt optional den Header <code>X-Notify-Token</code>, wenn die Umgebungsvariable <code>NOTIFY_TOKEN</code> gesetzt ist.
|
|
<span class="badge">UI-Service</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>`
|