diff --git a/backend/internal/features/notifiers/enums.go b/backend/internal/features/notifiers/enums.go
index 8e960bb..bfff440 100644
--- a/backend/internal/features/notifiers/enums.go
+++ b/backend/internal/features/notifiers/enums.go
@@ -7,4 +7,5 @@ const (
NotifierTypeTelegram NotifierType = "TELEGRAM"
NotifierTypeWebhook NotifierType = "WEBHOOK"
NotifierTypeSlack NotifierType = "SLACK"
+ NotifierTypeDiscord NotifierType = "DISCORD"
)
diff --git a/backend/internal/features/notifiers/model.go b/backend/internal/features/notifiers/model.go
index 6d75c13..32baf16 100644
--- a/backend/internal/features/notifiers/model.go
+++ b/backend/internal/features/notifiers/model.go
@@ -3,6 +3,7 @@ package notifiers
import (
"errors"
"log/slog"
+ discord_notifier "postgresus-backend/internal/features/notifiers/models/discord"
"postgresus-backend/internal/features/notifiers/models/email_notifier"
slack_notifier "postgresus-backend/internal/features/notifiers/models/slack"
telegram_notifier "postgresus-backend/internal/features/notifiers/models/telegram"
@@ -23,6 +24,7 @@ type Notifier struct {
EmailNotifier *email_notifier.EmailNotifier `json:"emailNotifier" gorm:"foreignKey:NotifierID"`
WebhookNotifier *webhook_notifier.WebhookNotifier `json:"webhookNotifier" gorm:"foreignKey:NotifierID"`
SlackNotifier *slack_notifier.SlackNotifier `json:"slackNotifier" gorm:"foreignKey:NotifierID"`
+ DiscordNotifier *discord_notifier.DiscordNotifier `json:"discordNotifier" gorm:"foreignKey:NotifierID"`
}
func (n *Notifier) TableName() string {
@@ -60,6 +62,8 @@ func (n *Notifier) getSpecificNotifier() NotificationSender {
return n.WebhookNotifier
case NotifierTypeSlack:
return n.SlackNotifier
+ case NotifierTypeDiscord:
+ return n.DiscordNotifier
default:
panic("unknown notifier type: " + string(n.NotifierType))
}
diff --git a/backend/internal/features/notifiers/models/discord/model.go b/backend/internal/features/notifiers/models/discord/model.go
new file mode 100644
index 0000000..1317775
--- /dev/null
+++ b/backend/internal/features/notifiers/models/discord/model.go
@@ -0,0 +1,73 @@
+package discord_notifier
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "log/slog"
+ "net/http"
+
+ "github.com/google/uuid"
+)
+
+type DiscordNotifier struct {
+ NotifierID uuid.UUID `json:"notifierId" gorm:"primaryKey;column:notifier_id"`
+ ChannelWebhookURL string `json:"channelWebhookUrl" gorm:"not null;column:channel_webhook_url"`
+}
+
+func (d *DiscordNotifier) TableName() string {
+ return "discord_notifiers"
+}
+
+func (d *DiscordNotifier) Validate() error {
+ if d.ChannelWebhookURL == "" {
+ return errors.New("webhook URL is required")
+ }
+
+ return nil
+}
+
+func (d *DiscordNotifier) Send(logger *slog.Logger, heading string, message string) error {
+ fullMessage := heading
+ if message != "" {
+ fullMessage = fmt.Sprintf("%s\n\n%s", heading, message)
+ }
+
+ payload := map[string]interface{}{
+ "content": fullMessage,
+ }
+
+ jsonPayload, err := json.Marshal(payload)
+ if err != nil {
+ return fmt.Errorf("failed to marshal Discord payload: %w", err)
+ }
+
+ req, err := http.NewRequest("POST", d.ChannelWebhookURL, bytes.NewReader(jsonPayload))
+ if err != nil {
+ return fmt.Errorf("failed to create request: %w", err)
+ }
+
+ req.Header.Set("Content-Type", "application/json")
+
+ client := &http.Client{}
+ resp, err := client.Do(req)
+ if err != nil {
+ return fmt.Errorf("failed to send Discord message: %w", err)
+ }
+ defer func() {
+ _ = resp.Body.Close()
+ }()
+
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ bodyBytes, _ := io.ReadAll(resp.Body)
+ return fmt.Errorf(
+ "discord API returned non-OK status: %s. Error: %s",
+ resp.Status,
+ string(bodyBytes),
+ )
+ }
+
+ return nil
+}
diff --git a/backend/internal/features/notifiers/models/email_notifier/model.go b/backend/internal/features/notifiers/models/email_notifier/model.go
index 73dab3a..3dde579 100644
--- a/backend/internal/features/notifiers/models/email_notifier/model.go
+++ b/backend/internal/features/notifiers/models/email_notifier/model.go
@@ -80,7 +80,7 @@ func (e *EmailNotifier) Send(logger *slog.Logger, heading string, message string
timeout := DefaultTimeout
// Determine if authentication is required
- authRequired := e.SMTPUser != "" && e.SMTPPassword != ""
+ isAuthRequired := e.SMTPUser != "" && e.SMTPPassword != ""
// Handle different port scenarios
if e.SMTPPort == ImplicitTLSPort {
@@ -110,7 +110,7 @@ func (e *EmailNotifier) Send(logger *slog.Logger, heading string, message string
}()
// Set up authentication only if credentials are provided
- if authRequired {
+ if isAuthRequired {
auth := smtp.PlainAuth("", e.SMTPUser, e.SMTPPassword, e.SMTPHost)
if err := client.Auth(auth); err != nil {
return fmt.Errorf("SMTP authentication failed: %w", err)
@@ -173,7 +173,7 @@ func (e *EmailNotifier) Send(logger *slog.Logger, heading string, message string
}
// Authenticate only if credentials are provided
- if authRequired {
+ if isAuthRequired {
auth := smtp.PlainAuth("", e.SMTPUser, e.SMTPPassword, e.SMTPHost)
if err := client.Auth(auth); err != nil {
return fmt.Errorf("SMTP authentication failed: %w", err)
diff --git a/backend/internal/features/notifiers/repository.go b/backend/internal/features/notifiers/repository.go
index f3bc98b..6b4f9de 100644
--- a/backend/internal/features/notifiers/repository.go
+++ b/backend/internal/features/notifiers/repository.go
@@ -30,17 +30,33 @@ func (r *NotifierRepository) Save(notifier *Notifier) (*Notifier, error) {
if notifier.SlackNotifier != nil {
notifier.SlackNotifier.NotifierID = notifier.ID
}
+ case NotifierTypeDiscord:
+ if notifier.DiscordNotifier != nil {
+ notifier.DiscordNotifier.NotifierID = notifier.ID
+ }
}
if notifier.ID == uuid.Nil {
if err := tx.Create(notifier).
- Omit("TelegramNotifier", "EmailNotifier", "WebhookNotifier", "SlackNotifier").
+ Omit(
+ "TelegramNotifier",
+ "EmailNotifier",
+ "WebhookNotifier",
+ "SlackNotifier",
+ "DiscordNotifier",
+ ).
Error; err != nil {
return err
}
} else {
if err := tx.Save(notifier).
- Omit("TelegramNotifier", "EmailNotifier", "WebhookNotifier", "SlackNotifier").
+ Omit(
+ "TelegramNotifier",
+ "EmailNotifier",
+ "WebhookNotifier",
+ "SlackNotifier",
+ "DiscordNotifier",
+ ).
Error; err != nil {
return err
}
@@ -75,6 +91,13 @@ func (r *NotifierRepository) Save(notifier *Notifier) (*Notifier, error) {
return err
}
}
+ case NotifierTypeDiscord:
+ if notifier.DiscordNotifier != nil {
+ notifier.DiscordNotifier.NotifierID = notifier.ID // Ensure ID is set
+ if err := tx.Save(notifier.DiscordNotifier).Error; err != nil {
+ return err
+ }
+ }
}
return nil
@@ -96,6 +119,7 @@ func (r *NotifierRepository) FindByID(id uuid.UUID) (*Notifier, error) {
Preload("EmailNotifier").
Preload("WebhookNotifier").
Preload("SlackNotifier").
+ Preload("DiscordNotifier").
Where("id = ?", id).
First(¬ifier).Error; err != nil {
return nil, err
@@ -113,7 +137,9 @@ func (r *NotifierRepository) FindByUserID(userID uuid.UUID) ([]*Notifier, error)
Preload("EmailNotifier").
Preload("WebhookNotifier").
Preload("SlackNotifier").
+ Preload("DiscordNotifier").
Where("user_id = ?", userID).
+ Order("name ASC").
Find(¬ifiers).Error; err != nil {
return nil, err
}
@@ -149,6 +175,12 @@ func (r *NotifierRepository) Delete(notifier *Notifier) error {
return err
}
}
+ case NotifierTypeDiscord:
+ if notifier.DiscordNotifier != nil {
+ if err := tx.Delete(notifier.DiscordNotifier).Error; err != nil {
+ return err
+ }
+ }
}
// Delete the main notifier
diff --git a/backend/migrations/20250716072247_add_discord_notifier.sql b/backend/migrations/20250716072247_add_discord_notifier.sql
new file mode 100644
index 0000000..3750af4
--- /dev/null
+++ b/backend/migrations/20250716072247_add_discord_notifier.sql
@@ -0,0 +1,23 @@
+-- +goose Up
+-- +goose StatementBegin
+
+-- Create discord notifiers table
+CREATE TABLE discord_notifiers (
+ notifier_id UUID PRIMARY KEY,
+ channel_webhook_url TEXT NOT NULL
+);
+
+ALTER TABLE discord_notifiers
+ ADD CONSTRAINT fk_discord_notifiers_notifier
+ FOREIGN KEY (notifier_id)
+ REFERENCES notifiers (id)
+ ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
+
+-- +goose StatementEnd
+
+-- +goose Down
+-- +goose StatementBegin
+
+DROP TABLE IF EXISTS discord_notifiers;
+
+-- +goose StatementEnd
diff --git a/frontend/public/icons/notifiers/discord.svg b/frontend/public/icons/notifiers/discord.svg
new file mode 100644
index 0000000..c03e8e1
--- /dev/null
+++ b/frontend/public/icons/notifiers/discord.svg
@@ -0,0 +1,8 @@
+
+
+
\ No newline at end of file
diff --git a/frontend/src/entity/notifiers/index.ts b/frontend/src/entity/notifiers/index.ts
index c5c66b4..1ff42ab 100644
--- a/frontend/src/entity/notifiers/index.ts
+++ b/frontend/src/entity/notifiers/index.ts
@@ -14,3 +14,6 @@ export { WebhookMethod } from './models/webhook/WebhookMethod';
export type { SlackNotifier } from './models/slack/SlackNotifier';
export { validateSlackNotifier } from './models/slack/validateSlackNotifier';
+
+export type { DiscordNotifier } from './models/discord/DiscordNotifier';
+export { validateDiscordNotifier } from './models/discord/validateDiscordNotifier';
diff --git a/frontend/src/entity/notifiers/models/Notifier.ts b/frontend/src/entity/notifiers/models/Notifier.ts
index ff31c1c..5adea8e 100644
--- a/frontend/src/entity/notifiers/models/Notifier.ts
+++ b/frontend/src/entity/notifiers/models/Notifier.ts
@@ -1,4 +1,5 @@
import type { NotifierType } from './NotifierType';
+import type { DiscordNotifier } from './discord/DiscordNotifier';
import type { EmailNotifier } from './email/EmailNotifier';
import type { SlackNotifier } from './slack/SlackNotifier';
import type { TelegramNotifier } from './telegram/TelegramNotifier';
@@ -15,4 +16,5 @@ export interface Notifier {
emailNotifier?: EmailNotifier;
webhookNotifier?: WebhookNotifier;
slackNotifier?: SlackNotifier;
+ discordNotifier?: DiscordNotifier;
}
diff --git a/frontend/src/entity/notifiers/models/NotifierType.ts b/frontend/src/entity/notifiers/models/NotifierType.ts
index f842904..31839ab 100644
--- a/frontend/src/entity/notifiers/models/NotifierType.ts
+++ b/frontend/src/entity/notifiers/models/NotifierType.ts
@@ -3,4 +3,5 @@ export enum NotifierType {
TELEGRAM = 'TELEGRAM',
WEBHOOK = 'WEBHOOK',
SLACK = 'SLACK',
+ DISCORD = 'DISCORD',
}
diff --git a/frontend/src/entity/notifiers/models/discord/DiscordNotifier.ts b/frontend/src/entity/notifiers/models/discord/DiscordNotifier.ts
new file mode 100644
index 0000000..4ebac50
--- /dev/null
+++ b/frontend/src/entity/notifiers/models/discord/DiscordNotifier.ts
@@ -0,0 +1,3 @@
+export interface DiscordNotifier {
+ channelWebhookUrl: string;
+}
diff --git a/frontend/src/entity/notifiers/models/discord/validateDiscordNotifier.ts b/frontend/src/entity/notifiers/models/discord/validateDiscordNotifier.ts
new file mode 100644
index 0000000..b9bdb7e
--- /dev/null
+++ b/frontend/src/entity/notifiers/models/discord/validateDiscordNotifier.ts
@@ -0,0 +1,9 @@
+import type { DiscordNotifier } from './DiscordNotifier';
+
+export const validateDiscordNotifier = (notifier: DiscordNotifier): boolean => {
+ if (!notifier.channelWebhookUrl) {
+ return false;
+ }
+
+ return true;
+};
diff --git a/frontend/src/entity/notifiers/models/getNotifierLogoFromType.ts b/frontend/src/entity/notifiers/models/getNotifierLogoFromType.ts
index 7690c7c..5fa5adb 100644
--- a/frontend/src/entity/notifiers/models/getNotifierLogoFromType.ts
+++ b/frontend/src/entity/notifiers/models/getNotifierLogoFromType.ts
@@ -10,6 +10,8 @@ export const getNotifierLogoFromType = (type: NotifierType) => {
return '/icons/notifiers/webhook.svg';
case NotifierType.SLACK:
return '/icons/notifiers/slack.svg';
+ case NotifierType.DISCORD:
+ return '/icons/notifiers/discord.svg';
default:
return '';
}
diff --git a/frontend/src/features/notifiers/ui/edit/EditNotifierComponent.tsx b/frontend/src/features/notifiers/ui/edit/EditNotifierComponent.tsx
index 89b8b99..3244f8b 100644
--- a/frontend/src/features/notifiers/ui/edit/EditNotifierComponent.tsx
+++ b/frontend/src/features/notifiers/ui/edit/EditNotifierComponent.tsx
@@ -6,6 +6,7 @@ import {
NotifierType,
WebhookMethod,
notifierApi,
+ validateDiscordNotifier,
validateEmailNotifier,
validateSlackNotifier,
validateTelegramNotifier,
@@ -13,6 +14,7 @@ import {
} from '../../../../entity/notifiers';
import { getNotifierLogoFromType } from '../../../../entity/notifiers/models/getNotifierLogoFromType';
import { ToastHelper } from '../../../../shared/toast';
+import { EditDiscordNotifierComponent } from './notifiers/EditDiscordNotifierComponent';
import { EditEmailNotifierComponent } from './notifiers/EditEmailNotifierComponent';
import { EditSlackNotifierComponent } from './notifiers/EditSlackNotifierComponent';
import { EditTelegramNotifierComponent } from './notifiers/EditTelegramNotifierComponent';
@@ -120,6 +122,12 @@ export function EditNotifierComponent({
};
}
+ if (type === NotifierType.DISCORD) {
+ notifier.discordNotifier = {
+ channelWebhookUrl: '',
+ };
+ }
+
setNotifier(
JSON.parse(
JSON.stringify({
@@ -171,6 +179,10 @@ export function EditNotifierComponent({
return validateSlackNotifier(notifier.slackNotifier);
}
+ if (notifier.notifierType === NotifierType.DISCORD && notifier.discordNotifier) {
+ return validateDiscordNotifier(notifier.discordNotifier);
+ }
+
return false;
};
@@ -205,6 +217,7 @@ export function EditNotifierComponent({
{ label: 'Email', value: NotifierType.EMAIL },
{ label: 'Webhook', value: NotifierType.WEBHOOK },
{ label: 'Slack', value: NotifierType.SLACK },
+ { label: 'Discord', value: NotifierType.DISCORD },
]}
onChange={(value) => {
setNotifierType(value);
@@ -251,6 +264,14 @@ export function EditNotifierComponent({
setIsUnsaved={setIsUnsaved}
/>
)}
+
+ {notifier?.notifierType === NotifierType.DISCORD && (
+