feat: Gotify CRUD + internal handler dispatch for notification service
- Add internal handler support to ServiceAdapter (Handler field) - Dispatcher routes to internal handler when set (no reverse proxy) - GotifyClient: full CRUD (send/list/delete messages, CRUD applications) - Refactor notification handler: route by X-Resource header, not body format - Register notification as ServiceAdapter with auth required - Resources: send-email, send-message, list-messages, delete-message, delete-all-messages, list-applications, create-application, delete-application - Update examples: sendmsg-email.sh, gotify-crud.sh - Env vars: GOTIFY_URL, GOTIFY_APP_TOKEN, GOTIFY_CLIENT_TOKEN
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"forgejo.riotpiao.com/rock/homelab-frontend/internal/auth"
|
"forgejo.riotpiao.com/rock/homelab-frontend/internal/auth"
|
||||||
"forgejo.riotpiao.com/rock/homelab-frontend/internal/config"
|
"forgejo.riotpiao.com/rock/homelab-frontend/internal/config"
|
||||||
|
"forgejo.riotpiao.com/rock/homelab-frontend/internal/notification"
|
||||||
"forgejo.riotpiao.com/rock/homelab-frontend/internal/proxy"
|
"forgejo.riotpiao.com/rock/homelab-frontend/internal/proxy"
|
||||||
"forgejo.riotpiao.com/rock/homelab-frontend/internal/server"
|
"forgejo.riotpiao.com/rock/homelab-frontend/internal/server"
|
||||||
"forgejo.riotpiao.com/rock/homelab-frontend/internal/serviceadapter"
|
"forgejo.riotpiao.com/rock/homelab-frontend/internal/serviceadapter"
|
||||||
@@ -82,6 +83,29 @@ func main() {
|
|||||||
}
|
}
|
||||||
_ = registry.Add(workflowAdapter)
|
_ = registry.Add(workflowAdapter)
|
||||||
|
|
||||||
|
// Add notification service adapter (internal handler, no upstream proxy)
|
||||||
|
notifHandler := notification.NewHandler()
|
||||||
|
notifAdapter := &serviceadapter.ServiceAdapter{
|
||||||
|
Namespace: "notification",
|
||||||
|
ServiceName: "notification",
|
||||||
|
Handler: notifHandler,
|
||||||
|
Spec: serviceadapter.Spec{
|
||||||
|
ServiceName: "notification",
|
||||||
|
Auth: serviceadapter.Auth{Required: true},
|
||||||
|
Resources: []serviceadapter.Resource{
|
||||||
|
{Name: "send-email", Methods: []serviceadapter.Method{{Verb: "POST", UpstreamPath: "/send-email"}}},
|
||||||
|
{Name: "send-message", Methods: []serviceadapter.Method{{Verb: "POST", UpstreamPath: "/send-message"}}},
|
||||||
|
{Name: "list-messages", Methods: []serviceadapter.Method{{Verb: "GET", UpstreamPath: "/list-messages"}}},
|
||||||
|
{Name: "delete-message", Methods: []serviceadapter.Method{{Verb: "DELETE", UpstreamPath: "/delete-message"}}},
|
||||||
|
{Name: "delete-all-messages", Methods: []serviceadapter.Method{{Verb: "DELETE", UpstreamPath: "/delete-all-messages"}}},
|
||||||
|
{Name: "list-applications", Methods: []serviceadapter.Method{{Verb: "GET", UpstreamPath: "/list-applications"}}},
|
||||||
|
{Name: "create-application", Methods: []serviceadapter.Method{{Verb: "POST", UpstreamPath: "/create-application"}}},
|
||||||
|
{Name: "delete-application", Methods: []serviceadapter.Method{{Verb: "DELETE", UpstreamPath: "/delete-application"}}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_ = registry.Add(notifAdapter)
|
||||||
|
|
||||||
// Add other adapters from config
|
// Add other adapters from config
|
||||||
for _, a := range cfg.Adapters {
|
for _, a := range cfg.Adapters {
|
||||||
_ = registry.Add(a)
|
_ = registry.Add(a)
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Example: Gotify CRUD operations via notification service (X-Service routing)
|
||||||
|
|
||||||
|
BASE_URL="${1:-https://api.riotpiao.com}"
|
||||||
|
AUTH_TOKEN="${2:-}"
|
||||||
|
AUTH="-H \"Authorization: Bearer $AUTH_TOKEN\""
|
||||||
|
|
||||||
|
echo "=== Send Gotify Message ==="
|
||||||
|
curl -s -X POST "$BASE_URL" \
|
||||||
|
-H "X-Service: notification" \
|
||||||
|
-H "X-Resource: send-message" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer $AUTH_TOKEN" \
|
||||||
|
-d '{
|
||||||
|
"title": "Deployment Complete",
|
||||||
|
"message": "homelab-frontend v1.2.0 deployed to production",
|
||||||
|
"priority": 5
|
||||||
|
}' | jq .
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== List Messages ==="
|
||||||
|
curl -s -X GET "$BASE_URL?limit=10" \
|
||||||
|
-H "X-Service: notification" \
|
||||||
|
-H "X-Resource: list-messages" \
|
||||||
|
-H "Authorization: Bearer $AUTH_TOKEN" | jq .
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== List Applications ==="
|
||||||
|
curl -s -X GET "$BASE_URL" \
|
||||||
|
-H "X-Service: notification" \
|
||||||
|
-H "X-Resource: list-applications" \
|
||||||
|
-H "Authorization: Bearer $AUTH_TOKEN" | jq .
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Create Application ==="
|
||||||
|
curl -s -X POST "$BASE_URL" \
|
||||||
|
-H "X-Service: notification" \
|
||||||
|
-H "X-Resource: create-application" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer $AUTH_TOKEN" \
|
||||||
|
-d '{
|
||||||
|
"name": "my-monitor",
|
||||||
|
"description": "Monitoring alerts"
|
||||||
|
}' | jq .
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Delete Message (by ID) ==="
|
||||||
|
curl -s -X DELETE "$BASE_URL" \
|
||||||
|
-H "X-Service: notification" \
|
||||||
|
-H "X-Resource: delete-message" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer $AUTH_TOKEN" \
|
||||||
|
-d '{"id": 1}' | jq .
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Delete Application (by ID) ==="
|
||||||
|
curl -s -X DELETE "$BASE_URL" \
|
||||||
|
-H "X-Service: notification" \
|
||||||
|
-H "X-Resource: delete-application" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer $AUTH_TOKEN" \
|
||||||
|
-d '{"id": 1}' | jq .
|
||||||
+11
-27
@@ -1,36 +1,20 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Example: Send email via notification/sendMsg endpoint
|
# Example: Send email via notification service (X-Service routing)
|
||||||
|
|
||||||
BASE_URL="${1:-https://api.riotpiao.com}"
|
BASE_URL="${1:-https://api.riotpiao.com}"
|
||||||
AUTH_TOKEN="${2:-}" # Optional JWT token if auth required
|
AUTH_TOKEN="${2:-}"
|
||||||
|
|
||||||
PAYLOAD=$(cat <<'EOF'
|
# Send email
|
||||||
{
|
curl -s -X POST "$BASE_URL" \
|
||||||
"format": "smtp",
|
|
||||||
"title": "System Alert",
|
|
||||||
"message": "CPU usage exceeded 90% threshold",
|
|
||||||
"priority": 7,
|
|
||||||
"extras": {
|
|
||||||
"to_email": "[email protected]",
|
|
||||||
"cc": "[email protected]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
)
|
|
||||||
|
|
||||||
if [ -n "$AUTH_TOKEN" ]; then
|
|
||||||
curl -X POST "$BASE_URL" \
|
|
||||||
-H "X-Service: notification" \
|
-H "X-Service: notification" \
|
||||||
-H "X-Resource: sendMsg" \
|
-H "X-Resource: send-email" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-H "Authorization: Bearer $AUTH_TOKEN" \
|
-H "Authorization: Bearer $AUTH_TOKEN" \
|
||||||
-d "$PAYLOAD"
|
-d '{
|
||||||
else
|
"to": "[email protected]",
|
||||||
curl -X POST "$BASE_URL" \
|
"cc": "[email protected]",
|
||||||
-H "X-Service: notification" \
|
"subject": "System Alert",
|
||||||
-H "X-Resource: sendMsg" \
|
"body": "CPU usage exceeded 90% threshold"
|
||||||
-H "Content-Type: application/json" \
|
}' | jq .
|
||||||
-d "$PAYLOAD"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
@@ -0,0 +1,257 @@
|
|||||||
|
package notification
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GotifyClient is a CRUD client for the Gotify API.
|
||||||
|
type GotifyClient struct {
|
||||||
|
baseURL string
|
||||||
|
appToken string // token for sending messages (application token)
|
||||||
|
clientToken string // token for reading/managing (client token)
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGotifyClient creates a Gotify API client.
|
||||||
|
// appToken is used for sending messages.
|
||||||
|
// clientToken is used for listing/deleting messages and managing applications.
|
||||||
|
func NewGotifyClient(baseURL, appToken, clientToken string) *GotifyClient {
|
||||||
|
return &GotifyClient{
|
||||||
|
baseURL: baseURL,
|
||||||
|
appToken: appToken,
|
||||||
|
clientToken: clientToken,
|
||||||
|
httpClient: &http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Message Types ---
|
||||||
|
|
||||||
|
// GotifyMessage represents a Gotify message.
|
||||||
|
type GotifyMessage struct {
|
||||||
|
ID int `json:"id,omitempty"`
|
||||||
|
AppID int `json:"appid,omitempty"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Priority int `json:"priority,omitempty"`
|
||||||
|
Date string `json:"date,omitempty"`
|
||||||
|
Extras map[string]interface{} `json:"extras,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GotifyMessageList is a paginated list of messages.
|
||||||
|
type GotifyMessageList struct {
|
||||||
|
Messages []GotifyMessage `json:"messages"`
|
||||||
|
Paging GotifyPaging `json:"paging"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GotifyPaging represents pagination info.
|
||||||
|
type GotifyPaging struct {
|
||||||
|
Size int `json:"size"`
|
||||||
|
Since int `json:"since"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
Next string `json:"next,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Application Types ---
|
||||||
|
|
||||||
|
// GotifyApplication represents a Gotify application.
|
||||||
|
type GotifyApplication struct {
|
||||||
|
ID int `json:"id,omitempty"`
|
||||||
|
Token string `json:"token,omitempty"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Image string `json:"image,omitempty"`
|
||||||
|
Internal bool `json:"internal,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Message CRUD ---
|
||||||
|
|
||||||
|
// SendMessage sends a message via Gotify (uses app token).
|
||||||
|
func (c *GotifyClient) SendMessage(msg GotifyMessage) (*GotifyMessage, error) {
|
||||||
|
body, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshal message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, c.baseURL+"/message", bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("X-Gotify-Key", c.appToken)
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("send message: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||||||
|
return nil, c.readError(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result GotifyMessage
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return nil, fmt.Errorf("decode response: %w", err)
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListMessages lists messages (uses client token).
|
||||||
|
func (c *GotifyClient) ListMessages(limit int) (*GotifyMessageList, error) {
|
||||||
|
url := fmt.Sprintf("%s/message?limit=%d", c.baseURL, limit)
|
||||||
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("X-Gotify-Key", c.clientToken)
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list messages: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, c.readError(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result GotifyMessageList
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return nil, fmt.Errorf("decode response: %w", err)
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteMessage deletes a message by ID (uses client token).
|
||||||
|
func (c *GotifyClient) DeleteMessage(id int) error {
|
||||||
|
url := fmt.Sprintf("%s/message/%d", c.baseURL, id)
|
||||||
|
req, err := http.NewRequest(http.MethodDelete, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("X-Gotify-Key", c.clientToken)
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("delete message: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
|
||||||
|
return c.readError(resp)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAllMessages deletes all messages (uses client token).
|
||||||
|
func (c *GotifyClient) DeleteAllMessages() error {
|
||||||
|
req, err := http.NewRequest(http.MethodDelete, c.baseURL+"/message", nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("X-Gotify-Key", c.clientToken)
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("delete all messages: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
|
||||||
|
return c.readError(resp)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Application CRUD ---
|
||||||
|
|
||||||
|
// ListApplications lists all applications (uses client token).
|
||||||
|
func (c *GotifyClient) ListApplications() ([]GotifyApplication, error) {
|
||||||
|
req, err := http.NewRequest(http.MethodGet, c.baseURL+"/application", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("X-Gotify-Key", c.clientToken)
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list applications: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, c.readError(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []GotifyApplication
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return nil, fmt.Errorf("decode response: %w", err)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateApplication creates a new application (uses client token).
|
||||||
|
func (c *GotifyClient) CreateApplication(app GotifyApplication) (*GotifyApplication, error) {
|
||||||
|
body, err := json.Marshal(app)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshal application: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, c.baseURL+"/application", bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("X-Gotify-Key", c.clientToken)
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create application: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||||||
|
return nil, c.readError(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result GotifyApplication
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return nil, fmt.Errorf("decode response: %w", err)
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteApplication deletes an application by ID (uses client token).
|
||||||
|
func (c *GotifyClient) DeleteApplication(id int) error {
|
||||||
|
url := fmt.Sprintf("%s/application/%d", c.baseURL, id)
|
||||||
|
req, err := http.NewRequest(http.MethodDelete, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("X-Gotify-Key", c.clientToken)
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("delete application: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
|
||||||
|
return c.readError(resp)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helpers ---
|
||||||
|
|
||||||
|
func (c *GotifyClient) readError(resp *http.Response) error {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return fmt.Errorf("gotify API error (HTTP %d): %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
+219
-160
@@ -7,226 +7,285 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/smtp"
|
"net/smtp"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SendMsgRequest represents a sendMsg API request.
|
// Handler handles notification requests routed via X-Resource header.
|
||||||
type SendMsgRequest struct {
|
// Supports: send-email, send-gotify, list-messages, delete-message,
|
||||||
Format string `json:"format"` // "smtp" or "sms"
|
// delete-all-messages, list-applications, create-application, delete-application.
|
||||||
Title string `json:"title"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Priority int `json:"priority,omitempty"`
|
|
||||||
Extras map[string]string `json:"extras,omitempty"` // e.g., {"to_email": "[email protected]", "phone": "+1234567890"}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendMsgResponse represents a sendMsg API response.
|
|
||||||
type SendMsgResponse struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
MessageID string `json:"messageId,omitempty"`
|
|
||||||
Error string `json:"error,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handler handles sendMsg requests and forwards to appropriate channel (email, SMS, or Gotify push).
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
smtpHost string
|
smtpHost string
|
||||||
smtpPort string
|
smtpPort string
|
||||||
smtpFrom string
|
smtpFrom string
|
||||||
smtpUser string
|
smtpUser string
|
||||||
smtpPass string
|
smtpPass string
|
||||||
smsAPIURL string
|
gotify *GotifyClient
|
||||||
smsAPIKey string
|
|
||||||
gotifyURL string
|
|
||||||
gotifyToken string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a new notification handler from environment variables.
|
// NewHandler creates a notification handler from environment variables.
|
||||||
func NewHandler() *Handler {
|
func NewHandler() *Handler {
|
||||||
|
var gotify *GotifyClient
|
||||||
|
gotifyURL := os.Getenv("GOTIFY_URL")
|
||||||
|
if gotifyURL != "" {
|
||||||
|
gotify = NewGotifyClient(
|
||||||
|
gotifyURL,
|
||||||
|
os.Getenv("GOTIFY_APP_TOKEN"),
|
||||||
|
os.Getenv("GOTIFY_CLIENT_TOKEN"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return &Handler{
|
return &Handler{
|
||||||
smtpHost: os.Getenv("SMTP_HOST"),
|
smtpHost: os.Getenv("SMTP_HOST"),
|
||||||
smtpPort: os.Getenv("SMTP_PORT"),
|
smtpPort: os.Getenv("SMTP_PORT"),
|
||||||
smtpFrom: os.Getenv("SMTP_FROM"),
|
smtpFrom: os.Getenv("SMTP_FROM"),
|
||||||
smtpUser: os.Getenv("SMTP_USER"),
|
smtpUser: os.Getenv("SMTP_USER"),
|
||||||
smtpPass: os.Getenv("SMTP_PASS"),
|
smtpPass: os.Getenv("SMTP_PASS"),
|
||||||
smsAPIURL: os.Getenv("SMS_API_URL"),
|
gotify: gotify,
|
||||||
smsAPIKey: os.Getenv("SMS_API_KEY"),
|
|
||||||
gotifyURL: os.Getenv("GOTIFY_URL"),
|
|
||||||
gotifyToken: os.Getenv("GOTIFY_TOKEN"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP handles sendMsg requests.
|
// ServeHTTP routes requests by X-Upstream-Path (set by dispatcher after resource matching).
|
||||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
resource := r.Header.Get("X-Resource")
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req SendMsgRequest
|
switch resource {
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
// --- Email ---
|
||||||
w.Header().Set("Content-Type", "application/json")
|
case "send-email":
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
h.handleSendEmail(w, r)
|
||||||
json.NewEncoder(w).Encode(SendMsgResponse{
|
|
||||||
Status: "error",
|
// --- Gotify Messages ---
|
||||||
Error: "invalid request: " + err.Error(),
|
case "send-message":
|
||||||
})
|
h.handleSendGotify(w, r)
|
||||||
return
|
case "list-messages":
|
||||||
}
|
h.handleListMessages(w, r)
|
||||||
|
case "delete-message":
|
||||||
|
h.handleDeleteMessage(w, r)
|
||||||
|
case "delete-all-messages":
|
||||||
|
h.handleDeleteAllMessages(w, r)
|
||||||
|
|
||||||
|
// --- Gotify Applications ---
|
||||||
|
case "list-applications":
|
||||||
|
h.handleListApplications(w, r)
|
||||||
|
case "create-application":
|
||||||
|
h.handleCreateApplication(w, r)
|
||||||
|
case "delete-application":
|
||||||
|
h.handleDeleteApplication(w, r)
|
||||||
|
|
||||||
// Route based on format
|
|
||||||
var resp SendMsgResponse
|
|
||||||
switch req.Format {
|
|
||||||
case "smtp":
|
|
||||||
resp = h.sendEmail(req)
|
|
||||||
case "sms":
|
|
||||||
resp = h.sendSMS(req)
|
|
||||||
case "gotify":
|
|
||||||
resp = h.sendGotify(req)
|
|
||||||
default:
|
default:
|
||||||
resp = SendMsgResponse{
|
h.writeJSON(w, http.StatusNotFound, map[string]string{
|
||||||
Status: "error",
|
"error": fmt.Sprintf("unknown resource: %s", resource),
|
||||||
Error: "unsupported format: " + req.Format,
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
// --- Email ---
|
||||||
if resp.Error != "" {
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
type SendEmailRequest struct {
|
||||||
} else {
|
To string `json:"to"`
|
||||||
w.WriteHeader(http.StatusOK)
|
CC string `json:"cc,omitempty"`
|
||||||
}
|
Subject string `json:"subject"`
|
||||||
json.NewEncoder(w).Encode(resp)
|
Body string `json:"body"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendEmail sends an email via SMTP.
|
func (h *Handler) handleSendEmail(w http.ResponseWriter, r *http.Request) {
|
||||||
func (h *Handler) sendEmail(req SendMsgRequest) SendMsgResponse {
|
var req SendEmailRequest
|
||||||
toEmail := req.Extras["to_email"]
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
if toEmail == "" {
|
h.writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request: " + err.Error()})
|
||||||
return SendMsgResponse{
|
return
|
||||||
Status: "error",
|
|
||||||
Error: "missing to_email in extras",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
subject := req.Title
|
if req.To == "" {
|
||||||
|
h.writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing 'to' field"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
subject := req.Subject
|
||||||
if subject == "" {
|
if subject == "" {
|
||||||
subject = "Notification"
|
subject = "Notification"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct email body
|
|
||||||
body := req.Message
|
|
||||||
if req.Extras != nil {
|
|
||||||
if cc := req.Extras["cc"]; cc != "" {
|
|
||||||
body = fmt.Sprintf("CC: %s\n\n%s", cc, body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
msg := fmt.Sprintf(
|
msg := fmt.Sprintf(
|
||||||
"From: %s\r\nTo: %s\r\nSubject: %s\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\n%s",
|
"From: %s\r\nTo: %s\r\nSubject: %s\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\n%s",
|
||||||
h.smtpFrom, toEmail, subject, body,
|
h.smtpFrom, req.To, subject, req.Body,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Send via SMTP
|
|
||||||
smtpAddr := fmt.Sprintf("%s:%s", h.smtpHost, h.smtpPort)
|
smtpAddr := fmt.Sprintf("%s:%s", h.smtpHost, h.smtpPort)
|
||||||
auth := smtp.PlainAuth("", h.smtpUser, h.smtpPass, h.smtpHost)
|
auth := smtp.PlainAuth("", h.smtpUser, h.smtpPass, h.smtpHost)
|
||||||
|
|
||||||
if err := smtp.SendMail(smtpAddr, auth, h.smtpFrom, []string{toEmail}, []byte(msg)); err != nil {
|
if err := smtp.SendMail(smtpAddr, auth, h.smtpFrom, []string{req.To}, []byte(msg)); err != nil {
|
||||||
log.Printf("error sending email to %s: %v", toEmail, err)
|
log.Printf("error sending email to %s: %v", req.To, err)
|
||||||
return SendMsgResponse{
|
h.writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to send email: " + err.Error()})
|
||||||
Status: "error",
|
return
|
||||||
Error: "failed to send email: " + err.Error(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return SendMsgResponse{
|
h.writeJSON(w, http.StatusOK, map[string]string{
|
||||||
Status: "success",
|
"status": "success",
|
||||||
MessageID: fmt.Sprintf("email-%s", toEmail),
|
"messageId": fmt.Sprintf("email-%s", req.To),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendSMS sends an SMS via configured provider.
|
// --- Gotify Messages ---
|
||||||
// Placeholder: integrate with Twilio, AWS SNS, or similar.
|
|
||||||
func (h *Handler) sendSMS(req SendMsgRequest) SendMsgResponse {
|
func (h *Handler) handleSendGotify(w http.ResponseWriter, r *http.Request) {
|
||||||
phone := req.Extras["phone"]
|
if h.gotify == nil {
|
||||||
if phone == "" {
|
h.writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "Gotify not configured"})
|
||||||
return SendMsgResponse{
|
return
|
||||||
Status: "error",
|
|
||||||
Error: "missing phone in extras",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implement SMS provider integration (Twilio, AWS SNS, etc.)
|
var msg GotifyMessage
|
||||||
// For now, return error
|
if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
|
||||||
return SendMsgResponse{
|
h.writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request: " + err.Error()})
|
||||||
Status: "error",
|
return
|
||||||
Error: "SMS not implemented yet",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendGotify sends a notification via Gotify server.
|
result, err := h.gotify.SendMessage(msg)
|
||||||
func (h *Handler) sendGotify(req SendMsgRequest) SendMsgResponse {
|
|
||||||
if h.gotifyURL == "" || h.gotifyToken == "" {
|
|
||||||
return SendMsgResponse{
|
|
||||||
Status: "error",
|
|
||||||
Error: "Gotify not configured (missing GOTIFY_URL or GOTIFY_TOKEN)",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct Gotify message
|
|
||||||
gotifyReq := map[string]interface{}{
|
|
||||||
"title": req.Title,
|
|
||||||
"message": req.Message,
|
|
||||||
"priority": req.Priority,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Marshal to JSON
|
|
||||||
body, err := json.Marshal(gotifyReq)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error marshaling Gotify request: %v", err)
|
log.Printf("error sending gotify message: %v", err)
|
||||||
return SendMsgResponse{
|
h.writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||||
Status: "error",
|
return
|
||||||
Error: "failed to marshal request: " + err.Error(),
|
}
|
||||||
|
|
||||||
|
h.writeJSON(w, http.StatusOK, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) handleListMessages(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.gotify == nil {
|
||||||
|
h.writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "Gotify not configured"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
limit := 50
|
||||||
|
if l := r.URL.Query().Get("limit"); l != "" {
|
||||||
|
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
||||||
|
limit = parsed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST to Gotify
|
result, err := h.gotify.ListMessages(limit)
|
||||||
gotifyEndpoint := fmt.Sprintf("%s/message?token=%s", h.gotifyURL, h.gotifyToken)
|
|
||||||
resp, err := http.Post(gotifyEndpoint, "application/json", strings.NewReader(string(body)))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error sending to Gotify: %v", err)
|
log.Printf("error listing gotify messages: %v", err)
|
||||||
return SendMsgResponse{
|
h.writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||||
Status: "error",
|
return
|
||||||
Error: "failed to send to Gotify: " + err.Error(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
// Check response status
|
|
||||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
|
||||||
log.Printf("Gotify returned status %d", resp.StatusCode)
|
|
||||||
return SendMsgResponse{
|
|
||||||
Status: "error",
|
|
||||||
Error: fmt.Sprintf("Gotify returned status %d", resp.StatusCode),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse response
|
h.writeJSON(w, http.StatusOK, result)
|
||||||
var gotifyResp map[string]interface{}
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&gotifyResp); err != nil {
|
|
||||||
log.Printf("error decoding Gotify response: %v", err)
|
|
||||||
return SendMsgResponse{
|
|
||||||
Status: "success",
|
|
||||||
MessageID: "gotify-sent",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract message ID if available
|
func (h *Handler) handleDeleteMessage(w http.ResponseWriter, r *http.Request) {
|
||||||
msgID := "gotify-sent"
|
if h.gotify == nil {
|
||||||
if id, ok := gotifyResp["id"]; ok {
|
h.writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "Gotify not configured"})
|
||||||
msgID = fmt.Sprintf("gotify-%v", id)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return SendMsgResponse{
|
var req struct {
|
||||||
Status: "success",
|
ID int `json:"id"`
|
||||||
MessageID: msgID,
|
|
||||||
}
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
h.writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ID == 0 {
|
||||||
|
h.writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing 'id' field"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.gotify.DeleteMessage(req.ID); err != nil {
|
||||||
|
log.Printf("error deleting gotify message %d: %v", req.ID, err)
|
||||||
|
h.writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) handleDeleteAllMessages(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.gotify == nil {
|
||||||
|
h.writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "Gotify not configured"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.gotify.DeleteAllMessages(); err != nil {
|
||||||
|
log.Printf("error deleting all gotify messages: %v", err)
|
||||||
|
h.writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.writeJSON(w, http.StatusOK, map[string]string{"status": "all messages deleted"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Gotify Applications ---
|
||||||
|
|
||||||
|
func (h *Handler) handleListApplications(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.gotify == nil {
|
||||||
|
h.writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "Gotify not configured"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := h.gotify.ListApplications()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error listing gotify applications: %v", err)
|
||||||
|
h.writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.writeJSON(w, http.StatusOK, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) handleCreateApplication(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.gotify == nil {
|
||||||
|
h.writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "Gotify not configured"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var app GotifyApplication
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&app); err != nil {
|
||||||
|
h.writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := h.gotify.CreateApplication(app)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error creating gotify application: %v", err)
|
||||||
|
h.writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.writeJSON(w, http.StatusCreated, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) handleDeleteApplication(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.gotify == nil {
|
||||||
|
h.writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "Gotify not configured"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
h.writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ID == 0 {
|
||||||
|
h.writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing 'id' field"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.gotify.DeleteApplication(req.ID); err != nil {
|
||||||
|
log.Printf("error deleting gotify application %d: %v", req.ID, err)
|
||||||
|
h.writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helpers ---
|
||||||
|
|
||||||
|
func (h *Handler) writeJSON(w http.ResponseWriter, status int, data interface{}) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
json.NewEncoder(w).Encode(data)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,14 @@ func (d *Dispatcher) Dispatch(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Internal handler: dispatch directly without reverse proxy
|
||||||
|
if adapter.Handler != nil {
|
||||||
|
// Set X-Upstream-Path so the handler knows which method was matched
|
||||||
|
r.Header.Set("X-Upstream-Path", method.UpstreamPath)
|
||||||
|
adapter.Handler.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
upstreamURL := adapter.Spec.Upstream.URL
|
upstreamURL := adapter.Spec.Upstream.URL
|
||||||
if strings.HasPrefix(upstreamURL, "grpc://") {
|
if strings.HasPrefix(upstreamURL, "grpc://") {
|
||||||
d.dispatchGRPC(w, r, upstreamURL, method, adapter)
|
d.dispatchGRPC(w, r, upstreamURL, method, adapter)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package serviceadapter
|
package serviceadapter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -49,6 +50,8 @@ type Status struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ServiceAdapter is a gateway service adapter.
|
// ServiceAdapter is a gateway service adapter.
|
||||||
|
// When Handler is set, the dispatcher routes directly to the internal handler
|
||||||
|
// instead of reverse-proxying to Spec.Upstream.URL.
|
||||||
type ServiceAdapter struct {
|
type ServiceAdapter struct {
|
||||||
Name string // namespace/name
|
Name string // namespace/name
|
||||||
Namespace string
|
Namespace string
|
||||||
@@ -56,4 +59,5 @@ type ServiceAdapter struct {
|
|||||||
Spec Spec
|
Spec Spec
|
||||||
Status Status
|
Status Status
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
|
Handler http.Handler `json:"-" yaml:"-"` // internal handler (skip serialization)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user