Skip to content

Commit a94a4d2

Browse files
FEATURE (notifiers): Add webhook notifier
1 parent 9d52882 commit a94a4d2

File tree

18 files changed

+366
-10
lines changed

18 files changed

+366
-10
lines changed

backend/internal/features/notifiers/enums.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ type NotifierType string
55
const (
66
NotifierTypeEmail NotifierType = "EMAIL"
77
NotifierTypeTelegram NotifierType = "TELEGRAM"
8+
NotifierTypeWebhook NotifierType = "WEBHOOK"
89
)

backend/internal/features/notifiers/model.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"errors"
55
"postgresus-backend/internal/features/notifiers/notifiers/email_notifier"
66
telegram_notifier "postgresus-backend/internal/features/notifiers/notifiers/telegram"
7+
webhook_notifier "postgresus-backend/internal/features/notifiers/notifiers/webhook"
78

89
"github.com/google/uuid"
910
)
@@ -18,6 +19,7 @@ type Notifier struct {
1819
// specific notifier
1920
TelegramNotifier *telegram_notifier.TelegramNotifier `json:"telegramNotifier" gorm:"foreignKey:NotifierID"`
2021
EmailNotifier *email_notifier.EmailNotifier `json:"emailNotifier" gorm:"foreignKey:NotifierID"`
22+
WebhookNotifier *webhook_notifier.WebhookNotifier `json:"webhookNotifier" gorm:"foreignKey:NotifierID"`
2123
}
2224

2325
func (n *Notifier) TableName() string {
@@ -34,6 +36,7 @@ func (n *Notifier) Validate() error {
3436

3537
func (n *Notifier) Send(heading string, message string) error {
3638
err := n.getSpecificNotifier().Send(heading, message)
39+
3740
if err != nil {
3841
lastSendError := err.Error()
3942
n.LastSendError = &lastSendError
@@ -50,6 +53,8 @@ func (n *Notifier) getSpecificNotifier() NotificationSender {
5053
return n.TelegramNotifier
5154
case NotifierTypeEmail:
5255
return n.EmailNotifier
56+
case NotifierTypeWebhook:
57+
return n.WebhookNotifier
5358
default:
5459
panic("unknown notifier type: " + string(n.NotifierType))
5560
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package webhook_notifier
2+
3+
type WebhookMethod string
4+
5+
const (
6+
WebhookMethodPOST WebhookMethod = "POST"
7+
WebhookMethodGET WebhookMethod = "GET"
8+
)
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package webhook_notifier
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"io"
9+
"net/http"
10+
"net/url"
11+
"postgresus-backend/internal/util/logger"
12+
13+
"github.com/google/uuid"
14+
)
15+
16+
var log = logger.GetLogger()
17+
18+
type WebhookNotifier struct {
19+
NotifierID uuid.UUID `json:"notifierId" gorm:"primaryKey;column:notifier_id"`
20+
WebhookURL string `json:"webhookUrl" gorm:"not null;column:webhook_url"`
21+
WebhookMethod WebhookMethod `json:"webhookMethod" gorm:"not null;column:webhook_method"`
22+
}
23+
24+
func (t *WebhookNotifier) TableName() string {
25+
return "webhook_notifiers"
26+
}
27+
28+
func (t *WebhookNotifier) Validate() error {
29+
if t.WebhookURL == "" {
30+
return errors.New("webhook URL is required")
31+
}
32+
33+
if t.WebhookMethod == "" {
34+
return errors.New("webhook method is required")
35+
}
36+
37+
return nil
38+
}
39+
40+
func (t *WebhookNotifier) Send(heading string, message string) error {
41+
switch t.WebhookMethod {
42+
case WebhookMethodGET:
43+
reqURL := fmt.Sprintf("%s?heading=%s&message=%s",
44+
t.WebhookURL,
45+
url.QueryEscape(heading),
46+
url.QueryEscape(message),
47+
)
48+
49+
resp, err := http.Get(reqURL)
50+
if err != nil {
51+
return fmt.Errorf("failed to send GET webhook: %w", err)
52+
}
53+
defer func() {
54+
if cerr := resp.Body.Close(); cerr != nil {
55+
log.Error("failed to close response body", "error", cerr)
56+
}
57+
}()
58+
59+
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
60+
body, _ := io.ReadAll(resp.Body)
61+
return fmt.Errorf(
62+
"webhook GET returned status: %s, body: %s",
63+
resp.Status,
64+
string(body),
65+
)
66+
}
67+
68+
return nil
69+
70+
case WebhookMethodPOST:
71+
payload := map[string]string{
72+
"heading": heading,
73+
"message": message,
74+
}
75+
76+
body, err := json.Marshal(payload)
77+
if err != nil {
78+
return fmt.Errorf("failed to marshal webhook payload: %w", err)
79+
}
80+
81+
resp, err := http.Post(t.WebhookURL, "application/json", bytes.NewReader(body))
82+
if err != nil {
83+
return fmt.Errorf("failed to send POST webhook: %w", err)
84+
}
85+
86+
defer func() {
87+
if cerr := resp.Body.Close(); cerr != nil {
88+
log.Error("failed to close response body", "error", cerr)
89+
}
90+
}()
91+
92+
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
93+
body, _ := io.ReadAll(resp.Body)
94+
return fmt.Errorf(
95+
"webhook POST returned status: %s, body: %s",
96+
resp.Status,
97+
string(body),
98+
)
99+
}
100+
101+
return nil
102+
103+
default:
104+
return fmt.Errorf("unsupported webhook method: %s", t.WebhookMethod)
105+
}
106+
}

backend/internal/features/notifiers/repository.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,21 @@ func (r *NotifierRepository) Save(notifier *Notifier) error {
2222
if notifier.EmailNotifier != nil {
2323
notifier.EmailNotifier.NotifierID = notifier.ID
2424
}
25+
case NotifierTypeWebhook:
26+
if notifier.WebhookNotifier != nil {
27+
notifier.WebhookNotifier.NotifierID = notifier.ID
28+
}
2529
}
2630

2731
if notifier.ID == uuid.Nil {
2832
if err := tx.Create(notifier).
29-
Omit("TelegramNotifier", "EmailNotifier").
33+
Omit("TelegramNotifier", "EmailNotifier", "WebhookNotifier").
3034
Error; err != nil {
3135
return err
3236
}
3337
} else {
3438
if err := tx.Save(notifier).
35-
Omit("TelegramNotifier", "EmailNotifier").
39+
Omit("TelegramNotifier", "EmailNotifier", "WebhookNotifier").
3640
Error; err != nil {
3741
return err
3842
}
@@ -53,6 +57,13 @@ func (r *NotifierRepository) Save(notifier *Notifier) error {
5357
return err
5458
}
5559
}
60+
case NotifierTypeWebhook:
61+
if notifier.WebhookNotifier != nil {
62+
notifier.WebhookNotifier.NotifierID = notifier.ID // Ensure ID is set
63+
if err := tx.Save(notifier.WebhookNotifier).Error; err != nil {
64+
return err
65+
}
66+
}
5667
}
5768

5869
return nil
@@ -66,6 +77,7 @@ func (r *NotifierRepository) FindByID(id uuid.UUID) (*Notifier, error) {
6677
GetDb().
6778
Preload("TelegramNotifier").
6879
Preload("EmailNotifier").
80+
Preload("WebhookNotifier").
6981
Where("id = ?", id).
7082
First(&notifier).Error; err != nil {
7183
return nil, err
@@ -81,6 +93,7 @@ func (r *NotifierRepository) FindByUserID(userID uuid.UUID) ([]*Notifier, error)
8193
GetDb().
8294
Preload("TelegramNotifier").
8395
Preload("EmailNotifier").
96+
Preload("WebhookNotifier").
8497
Where("user_id = ?", userID).
8598
Find(&notifiers).Error; err != nil {
8699
return nil, err
@@ -105,6 +118,12 @@ func (r *NotifierRepository) Delete(notifier *Notifier) error {
105118
return err
106119
}
107120
}
121+
case NotifierTypeWebhook:
122+
if notifier.WebhookNotifier != nil {
123+
if err := tx.Delete(notifier.WebhookNotifier).Error; err != nil {
124+
return err
125+
}
126+
}
108127
}
109128

110129
// Delete the main notifier
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
-- +goose Up
2+
-- +goose StatementBegin
3+
4+
-- Create webhook notifiers table
5+
CREATE TABLE webhook_notifiers (
6+
notifier_id UUID PRIMARY KEY,
7+
webhook_url TEXT NOT NULL,
8+
webhook_method TEXT NOT NULL
9+
);
10+
11+
ALTER TABLE webhook_notifiers
12+
ADD CONSTRAINT fk_webhook_notifiers_notifier
13+
FOREIGN KEY (notifier_id)
14+
REFERENCES notifiers (id)
15+
ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
16+
17+
-- +goose StatementEnd
18+
19+
-- +goose Down
20+
-- +goose StatementBegin
21+
22+
DROP TABLE IF EXISTS webhook_notifiers;
23+
24+
-- +goose StatementEnd

frontend/public/icons/notifiers/webhook.svg

Lines changed: 19 additions & 0 deletions
Loading

frontend/src/entity/notifiers/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@ export { notifierApi } from './api/notifierApi';
22
export type { Notifier } from './models/Notifier';
33
export type { EmailNotifier } from './models/EmailNotifier';
44
export type { TelegramNotifier } from './models/TelegramNotifier';
5+
export type { WebhookNotifier } from './models/WebhookNotifier';
6+
export { WebhookMethod } from './models/WebhookMethod';
57
export { NotifierType } from './models/NotifierType';

frontend/src/entity/notifiers/models/Notifier.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { EmailNotifier } from './EmailNotifier';
22
import type { NotifierType } from './NotifierType';
33
import type { TelegramNotifier } from './TelegramNotifier';
4+
import type { WebhookNotifier } from './WebhookNotifier';
45

56
export interface Notifier {
67
id: string;
@@ -11,4 +12,5 @@ export interface Notifier {
1112
// specific notifier
1213
telegramNotifier?: TelegramNotifier;
1314
emailNotifier?: EmailNotifier;
15+
webhookNotifier?: WebhookNotifier;
1416
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export enum NotifierType {
22
EMAIL = 'EMAIL',
33
TELEGRAM = 'TELEGRAM',
4+
WEBHOOK = 'WEBHOOK',
45
}

0 commit comments

Comments
 (0)