@@ -10,20 +10,57 @@ import (
1010 "net/http"
1111 "net/url"
1212 "postgresus-backend/internal/util/encryption"
13+ "strings"
1314
1415 "github.com/google/uuid"
16+ "gorm.io/gorm"
1517)
1618
19+ type WebhookHeader struct {
20+ Key string `json:"key"`
21+ Value string `json:"value"`
22+ }
23+
1724type WebhookNotifier struct {
1825 NotifierID uuid.UUID `json:"notifierId" gorm:"primaryKey;column:notifier_id"`
1926 WebhookURL string `json:"webhookUrl" gorm:"not null;column:webhook_url"`
2027 WebhookMethod WebhookMethod `json:"webhookMethod" gorm:"not null;column:webhook_method"`
28+ BodyTemplate * string `json:"bodyTemplate" gorm:"column:body_template;type:text"`
29+ HeadersJSON string `json:"-" gorm:"column:headers;type:text"`
30+
31+ Headers []WebhookHeader `json:"headers" gorm:"-"`
2132}
2233
2334func (t * WebhookNotifier ) TableName () string {
2435 return "webhook_notifiers"
2536}
2637
38+ func (t * WebhookNotifier ) BeforeSave (_ * gorm.DB ) error {
39+ if len (t .Headers ) > 0 {
40+ data , err := json .Marshal (t .Headers )
41+
42+ if err != nil {
43+ return err
44+ }
45+
46+ t .HeadersJSON = string (data )
47+ } else {
48+ t .HeadersJSON = "[]"
49+ }
50+
51+ return nil
52+ }
53+
54+ func (t * WebhookNotifier ) AfterFind (_ * gorm.DB ) error {
55+ if t .HeadersJSON != "" {
56+ if err := json .Unmarshal ([]byte (t .HeadersJSON ), & t .Headers ); err != nil {
57+ return err
58+ }
59+ }
60+
61+ return nil
62+ }
63+
2764func (t * WebhookNotifier ) Validate (encryptor encryption.FieldEncryptor ) error {
2865 if t .WebhookURL == "" {
2966 return errors .New ("webhook URL is required" )
@@ -49,66 +86,9 @@ func (t *WebhookNotifier) Send(
4986
5087 switch t .WebhookMethod {
5188 case WebhookMethodGET :
52- reqURL := fmt .Sprintf ("%s?heading=%s&message=%s" ,
53- webhookURL ,
54- url .QueryEscape (heading ),
55- url .QueryEscape (message ),
56- )
57-
58- resp , err := http .Get (reqURL )
59- if err != nil {
60- return fmt .Errorf ("failed to send GET webhook: %w" , err )
61- }
62- defer func () {
63- if cerr := resp .Body .Close (); cerr != nil {
64- logger .Error ("failed to close response body" , "error" , cerr )
65- }
66- }()
67-
68- if resp .StatusCode < 200 || resp .StatusCode >= 300 {
69- body , _ := io .ReadAll (resp .Body )
70- return fmt .Errorf (
71- "webhook GET returned status: %s, body: %s" ,
72- resp .Status ,
73- string (body ),
74- )
75- }
76-
77- return nil
78-
89+ return t .sendGET (webhookURL , heading , message , logger )
7990 case WebhookMethodPOST :
80- payload := map [string ]string {
81- "heading" : heading ,
82- "message" : message ,
83- }
84-
85- body , err := json .Marshal (payload )
86- if err != nil {
87- return fmt .Errorf ("failed to marshal webhook payload: %w" , err )
88- }
89-
90- resp , err := http .Post (webhookURL , "application/json" , bytes .NewReader (body ))
91- if err != nil {
92- return fmt .Errorf ("failed to send POST webhook: %w" , err )
93- }
94-
95- defer func () {
96- if cerr := resp .Body .Close (); cerr != nil {
97- logger .Error ("failed to close response body" , "error" , cerr )
98- }
99- }()
100-
101- if resp .StatusCode < 200 || resp .StatusCode >= 300 {
102- body , _ := io .ReadAll (resp .Body )
103- return fmt .Errorf (
104- "webhook POST returned status: %s, body: %s" ,
105- resp .Status ,
106- string (body ),
107- )
108- }
109-
110- return nil
111-
91+ return t .sendPOST (webhookURL , heading , message , logger )
11292 default :
11393 return fmt .Errorf ("unsupported webhook method: %s" , t .WebhookMethod )
11494 }
@@ -120,15 +100,130 @@ func (t *WebhookNotifier) HideSensitiveData() {
120100func (t * WebhookNotifier ) Update (incoming * WebhookNotifier ) {
121101 t .WebhookURL = incoming .WebhookURL
122102 t .WebhookMethod = incoming .WebhookMethod
103+ t .BodyTemplate = incoming .BodyTemplate
104+ t .Headers = incoming .Headers
123105}
124106
125107func (t * WebhookNotifier ) EncryptSensitiveData (encryptor encryption.FieldEncryptor ) error {
126108 if t .WebhookURL != "" {
127109 encrypted , err := encryptor .Encrypt (t .NotifierID , t .WebhookURL )
110+
128111 if err != nil {
129112 return fmt .Errorf ("failed to encrypt webhook URL: %w" , err )
130113 }
114+
131115 t .WebhookURL = encrypted
132116 }
117+
133118 return nil
134119}
120+
121+ func (t * WebhookNotifier ) sendGET (webhookURL , heading , message string , logger * slog.Logger ) error {
122+ reqURL := fmt .Sprintf ("%s?heading=%s&message=%s" ,
123+ webhookURL ,
124+ url .QueryEscape (heading ),
125+ url .QueryEscape (message ),
126+ )
127+
128+ req , err := http .NewRequest (http .MethodGet , reqURL , nil )
129+ if err != nil {
130+ return fmt .Errorf ("failed to create GET request: %w" , err )
131+ }
132+
133+ t .applyHeaders (req )
134+
135+ client := & http.Client {}
136+ resp , err := client .Do (req )
137+ if err != nil {
138+ return fmt .Errorf ("failed to send GET webhook: %w" , err )
139+ }
140+
141+ defer func () {
142+ if cerr := resp .Body .Close (); cerr != nil {
143+ logger .Error ("failed to close response body" , "error" , cerr )
144+ }
145+ }()
146+
147+ if resp .StatusCode < 200 || resp .StatusCode >= 300 {
148+ body , _ := io .ReadAll (resp .Body )
149+ return fmt .Errorf (
150+ "webhook GET returned status: %s, body: %s" ,
151+ resp .Status ,
152+ string (body ),
153+ )
154+ }
155+
156+ return nil
157+ }
158+
159+ func (t * WebhookNotifier ) sendPOST (webhookURL , heading , message string , logger * slog.Logger ) error {
160+ body := t .buildRequestBody (heading , message )
161+
162+ req , err := http .NewRequest (http .MethodPost , webhookURL , bytes .NewReader (body ))
163+ if err != nil {
164+ return fmt .Errorf ("failed to create POST request: %w" , err )
165+ }
166+
167+ hasContentType := false
168+
169+ for _ , h := range t .Headers {
170+ if strings .EqualFold (h .Key , "Content-Type" ) {
171+ hasContentType = true
172+ break
173+ }
174+ }
175+
176+ if ! hasContentType {
177+ req .Header .Set ("Content-Type" , "application/json" )
178+ }
179+
180+ t .applyHeaders (req )
181+
182+ client := & http.Client {}
183+ resp , err := client .Do (req )
184+ if err != nil {
185+ return fmt .Errorf ("failed to send POST webhook: %w" , err )
186+ }
187+
188+ defer func () {
189+ if cerr := resp .Body .Close (); cerr != nil {
190+ logger .Error ("failed to close response body" , "error" , cerr )
191+ }
192+ }()
193+
194+ if resp .StatusCode < 200 || resp .StatusCode >= 300 {
195+ respBody , _ := io .ReadAll (resp .Body )
196+ return fmt .Errorf (
197+ "webhook POST returned status: %s, body: %s" ,
198+ resp .Status ,
199+ string (respBody ),
200+ )
201+ }
202+
203+ return nil
204+ }
205+
206+ func (t * WebhookNotifier ) buildRequestBody (heading , message string ) []byte {
207+ if t .BodyTemplate != nil && * t .BodyTemplate != "" {
208+ result := * t .BodyTemplate
209+ result = strings .ReplaceAll (result , "{{heading}}" , heading )
210+ result = strings .ReplaceAll (result , "{{message}}" , message )
211+ return []byte (result )
212+ }
213+
214+ payload := map [string ]string {
215+ "heading" : heading ,
216+ "message" : message ,
217+ }
218+ body , _ := json .Marshal (payload )
219+
220+ return body
221+ }
222+
223+ func (t * WebhookNotifier ) applyHeaders (req * http.Request ) {
224+ for _ , h := range t .Headers {
225+ if h .Key != "" {
226+ req .Header .Set (h .Key , h .Value )
227+ }
228+ }
229+ }
0 commit comments