notification: add API endpoints for channels, preferences, history, and acknowledgement

New endpoints under /api/notifications:
- CRUD for notification channels (email, webhook, UnifiedPush)
- CRUD for notification preferences (global, per-domain, per-service)
- Notification history listing
- Test notification endpoint

Acknowledgement endpoints added to scoped checker routes:
- POST /api/domains/:domain/checkers/:checkerId/acknowledge
- DELETE /api/domains/:domain/checkers/:checkerId/acknowledge

Thread NotificationController through route declarations for scoped
checker routes (domain and service level).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
nemunaire 2026-04-11 08:55:00 +07:00
commit e41ea2fb98
8 changed files with 637 additions and 3 deletions

View file

@ -0,0 +1,514 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2026 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package controller
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"git.happydns.org/happyDomain/internal/api/middleware"
notifUC "git.happydns.org/happyDomain/internal/usecase/notification"
"git.happydns.org/happyDomain/model"
)
// NotificationController handles notification-related API endpoints.
type NotificationController struct {
dispatcher *notifUC.Dispatcher
channelStore notifUC.NotificationChannelStorage
prefStore notifUC.NotificationPreferenceStorage
recordStore notifUC.NotificationRecordStorage
}
// NewNotificationController creates a new NotificationController.
func NewNotificationController(
dispatcher *notifUC.Dispatcher,
channelStore notifUC.NotificationChannelStorage,
prefStore notifUC.NotificationPreferenceStorage,
recordStore notifUC.NotificationRecordStorage,
) *NotificationController {
return &NotificationController{
dispatcher: dispatcher,
channelStore: channelStore,
prefStore: prefStore,
recordStore: recordStore,
}
}
// --- Channel CRUD ---
// ListChannels returns all notification channels for the authenticated user.
//
// @Summary List notification channels
// @Tags notifications
// @Produce json
// @Success 200 {array} happydns.NotificationChannel
// @Router /notifications/channels [get]
func (nc *NotificationController) ListChannels(c *gin.Context) {
user := middleware.MyUser(c)
channels, err := nc.channelStore.ListChannelsByUser(user.Id)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
if channels == nil {
channels = []*happydns.NotificationChannel{}
}
c.JSON(http.StatusOK, channels)
}
// CreateChannel creates a new notification channel.
//
// @Summary Create a notification channel
// @Tags notifications
// @Accept json
// @Produce json
// @Param body body happydns.NotificationChannel true "Channel configuration"
// @Success 201 {object} happydns.NotificationChannel
// @Router /notifications/channels [post]
func (nc *NotificationController) CreateChannel(c *gin.Context) {
user := middleware.MyUser(c)
var ch happydns.NotificationChannel
if err := c.ShouldBindJSON(&ch); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
ch.UserId = user.Id
if err := nc.channelStore.CreateChannel(&ch); err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusCreated, ch)
}
// GetChannel returns a specific notification channel.
//
// @Summary Get a notification channel
// @Tags notifications
// @Produce json
// @Param channelId path string true "Channel ID"
// @Success 200 {object} happydns.NotificationChannel
// @Router /notifications/channels/{channelId} [get]
func (nc *NotificationController) GetChannel(c *gin.Context) {
user := middleware.MyUser(c)
channelId, err := happydns.NewIdentifierFromString(c.Param("channelId"))
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid channel ID"})
return
}
ch, err := nc.channelStore.GetChannel(channelId)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Channel not found"})
return
}
if !ch.UserId.Equals(user.Id) {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Access denied"})
return
}
c.JSON(http.StatusOK, ch)
}
// UpdateChannel updates a notification channel.
//
// @Summary Update a notification channel
// @Tags notifications
// @Accept json
// @Produce json
// @Param channelId path string true "Channel ID"
// @Param body body happydns.NotificationChannel true "Channel configuration"
// @Success 200 {object} happydns.NotificationChannel
// @Router /notifications/channels/{channelId} [put]
func (nc *NotificationController) UpdateChannel(c *gin.Context) {
user := middleware.MyUser(c)
channelId, err := happydns.NewIdentifierFromString(c.Param("channelId"))
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid channel ID"})
return
}
existing, err := nc.channelStore.GetChannel(channelId)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Channel not found"})
return
}
if !existing.UserId.Equals(user.Id) {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Access denied"})
return
}
var ch happydns.NotificationChannel
if err := c.ShouldBindJSON(&ch); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
ch.Id = channelId
ch.UserId = user.Id
if err := nc.channelStore.UpdateChannel(&ch); err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, ch)
}
// DeleteChannel deletes a notification channel.
//
// @Summary Delete a notification channel
// @Tags notifications
// @Param channelId path string true "Channel ID"
// @Success 204
// @Router /notifications/channels/{channelId} [delete]
func (nc *NotificationController) DeleteChannel(c *gin.Context) {
user := middleware.MyUser(c)
channelId, err := happydns.NewIdentifierFromString(c.Param("channelId"))
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid channel ID"})
return
}
existing, err := nc.channelStore.GetChannel(channelId)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Channel not found"})
return
}
if !existing.UserId.Equals(user.Id) {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Access denied"})
return
}
if err := nc.channelStore.DeleteChannel(channelId); err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.Status(http.StatusNoContent)
}
// TestChannel sends a test notification through a channel.
//
// @Summary Send a test notification
// @Tags notifications
// @Param channelId path string true "Channel ID"
// @Success 200 {object} map[string]string
// @Router /notifications/channels/{channelId}/test [post]
func (nc *NotificationController) TestChannel(c *gin.Context) {
user := middleware.MyUser(c)
channelId, err := happydns.NewIdentifierFromString(c.Param("channelId"))
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid channel ID"})
return
}
ch, err := nc.channelStore.GetChannel(channelId)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Channel not found"})
return
}
if !ch.UserId.Equals(user.Id) {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Access denied"})
return
}
if err := nc.dispatcher.SendTestNotification(ch, user); err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Test notification sent"})
}
// --- Preference CRUD ---
// ListPreferences returns all notification preferences for the authenticated user.
//
// @Summary List notification preferences
// @Tags notifications
// @Produce json
// @Success 200 {array} happydns.NotificationPreference
// @Router /notifications/preferences [get]
func (nc *NotificationController) ListPreferences(c *gin.Context) {
user := middleware.MyUser(c)
prefs, err := nc.prefStore.ListPreferencesByUser(user.Id)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
if prefs == nil {
prefs = []*happydns.NotificationPreference{}
}
c.JSON(http.StatusOK, prefs)
}
// CreatePreference creates a new notification preference.
//
// @Summary Create a notification preference
// @Tags notifications
// @Accept json
// @Produce json
// @Param body body happydns.NotificationPreference true "Preference configuration"
// @Success 201 {object} happydns.NotificationPreference
// @Router /notifications/preferences [post]
func (nc *NotificationController) CreatePreference(c *gin.Context) {
user := middleware.MyUser(c)
var pref happydns.NotificationPreference
if err := c.ShouldBindJSON(&pref); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
pref.UserId = user.Id
if err := nc.prefStore.CreatePreference(&pref); err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusCreated, pref)
}
// GetPreference returns a specific notification preference.
//
// @Summary Get a notification preference
// @Tags notifications
// @Produce json
// @Param prefId path string true "Preference ID"
// @Success 200 {object} happydns.NotificationPreference
// @Router /notifications/preferences/{prefId} [get]
func (nc *NotificationController) GetPreference(c *gin.Context) {
user := middleware.MyUser(c)
prefId, err := happydns.NewIdentifierFromString(c.Param("prefId"))
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid preference ID"})
return
}
pref, err := nc.prefStore.GetPreference(prefId)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Preference not found"})
return
}
if !pref.UserId.Equals(user.Id) {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Access denied"})
return
}
c.JSON(http.StatusOK, pref)
}
// UpdatePreference updates a notification preference.
//
// @Summary Update a notification preference
// @Tags notifications
// @Accept json
// @Produce json
// @Param prefId path string true "Preference ID"
// @Param body body happydns.NotificationPreference true "Preference configuration"
// @Success 200 {object} happydns.NotificationPreference
// @Router /notifications/preferences/{prefId} [put]
func (nc *NotificationController) UpdatePreference(c *gin.Context) {
user := middleware.MyUser(c)
prefId, err := happydns.NewIdentifierFromString(c.Param("prefId"))
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid preference ID"})
return
}
existing, err := nc.prefStore.GetPreference(prefId)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Preference not found"})
return
}
if !existing.UserId.Equals(user.Id) {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Access denied"})
return
}
var pref happydns.NotificationPreference
if err := c.ShouldBindJSON(&pref); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
pref.Id = prefId
pref.UserId = user.Id
if err := nc.prefStore.UpdatePreference(&pref); err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, pref)
}
// DeletePreference deletes a notification preference.
//
// @Summary Delete a notification preference
// @Tags notifications
// @Param prefId path string true "Preference ID"
// @Success 204
// @Router /notifications/preferences/{prefId} [delete]
func (nc *NotificationController) DeletePreference(c *gin.Context) {
user := middleware.MyUser(c)
prefId, err := happydns.NewIdentifierFromString(c.Param("prefId"))
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid preference ID"})
return
}
existing, err := nc.prefStore.GetPreference(prefId)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Preference not found"})
return
}
if !existing.UserId.Equals(user.Id) {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Access denied"})
return
}
if err := nc.prefStore.DeletePreference(prefId); err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.Status(http.StatusNoContent)
}
// --- History ---
// ListHistory returns recent notification records for the authenticated user.
//
// @Summary List notification history
// @Tags notifications
// @Produce json
// @Param limit query int false "Maximum number of records" default(50)
// @Success 200 {array} happydns.NotificationRecord
// @Router /notifications/history [get]
func (nc *NotificationController) ListHistory(c *gin.Context) {
user := middleware.MyUser(c)
limit := 50
if l := c.Query("limit"); l != "" {
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
limit = parsed
}
}
records, err := nc.recordStore.ListRecordsByUser(user.Id, limit)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
if records == nil {
records = []*happydns.NotificationRecord{}
}
c.JSON(http.StatusOK, records)
}
// --- Acknowledgement ---
// AcknowledgeIssue marks a checker issue as acknowledged.
//
// @Summary Acknowledge a checker issue
// @Tags checkers
// @Accept json
// @Produce json
// @Param domain path string true "Domain identifier"
// @Param checkerId path string true "Checker ID"
// @Param body body happydns.AcknowledgeRequest true "Acknowledgement"
// @Success 200 {object} happydns.NotificationState
// @Router /domains/{domain}/checkers/{checkerId}/acknowledge [post]
func (nc *NotificationController) AcknowledgeIssue(c *gin.Context) {
user := middleware.MyUser(c)
target := targetFromContext(c)
checkerID := c.Param("checkerId")
var req happydns.AcknowledgeRequest
if err := c.ShouldBindJSON(&req); err != nil {
// Body is optional for acknowledgement.
req = happydns.AcknowledgeRequest{}
}
if err := nc.dispatcher.AcknowledgeIssue(user.Id, checkerID, target, user.Email, req.Annotation); err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
state, err := nc.dispatcher.GetState(user.Id, checkerID, target)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, state)
}
// ClearAcknowledgement removes an acknowledgement from a checker issue.
//
// @Summary Clear acknowledgement
// @Tags checkers
// @Produce json
// @Param domain path string true "Domain identifier"
// @Param checkerId path string true "Checker ID"
// @Success 200 {object} happydns.NotificationState
// @Router /domains/{domain}/checkers/{checkerId}/acknowledge [delete]
func (nc *NotificationController) ClearAcknowledgement(c *gin.Context) {
user := middleware.MyUser(c)
target := targetFromContext(c)
checkerID := c.Param("checkerId")
if err := nc.dispatcher.ClearAcknowledgement(user.Id, checkerID, target); err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
state, err := nc.dispatcher.GetState(user.Id, checkerID, target)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, state)
}

View file

@ -61,7 +61,8 @@ func DeclareCheckerRoutes(
// DeclareScopedCheckerRoutes registers checker routes scoped to a domain or service.
// Called for both /api/domains/:domain/checkers and .../services/:serviceid/checkers.
func DeclareScopedCheckerRoutes(scopedRouter *gin.RouterGroup, cc *controller.CheckerController) {
// nc may be nil if the notification system is not configured.
func DeclareScopedCheckerRoutes(scopedRouter *gin.RouterGroup, cc *controller.CheckerController, nc *controller.NotificationController) {
checkers := scopedRouter.Group("/checkers")
checkers.GET("", cc.ListAvailableChecks)
checkers.GET("/metrics", cc.GetDomainMetrics)
@ -106,4 +107,10 @@ func DeclareScopedCheckerRoutes(scopedRouter *gin.RouterGroup, cc *controller.Ch
// Results (under execution).
executionID.GET("/results", cc.GetExecutionResults)
executionID.GET("/results/:ruleName", cc.GetExecutionResult)
// Acknowledgement (requires notification system).
if nc != nil {
checkerID.POST("/acknowledge", nc.AcknowledgeIssue)
checkerID.DELETE("/acknowledge", nc.ClearAcknowledgement)
}
}

View file

@ -42,6 +42,7 @@ func DeclareDomainRoutes(
serviceUC happydns.ServiceUsecase,
cc *controller.CheckerController,
checkStatusUC *checkerUC.CheckStatusUsecase,
nc *controller.NotificationController,
) {
dc := controller.NewDomainController(
domainUC,
@ -67,7 +68,7 @@ func DeclareDomainRoutes(
// Mount domain-scoped checker routes.
if cc != nil {
DeclareScopedCheckerRoutes(apiDomainsRoutes, cc)
DeclareScopedCheckerRoutes(apiDomainsRoutes, cc, nc)
}
DeclareZoneRoutes(
@ -78,5 +79,6 @@ func DeclareDomainRoutes(
zoneServiceUC,
serviceUC,
cc,
nc,
)
}

View file

@ -0,0 +1,69 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2026 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package route
import (
"github.com/gin-gonic/gin"
"git.happydns.org/happyDomain/internal/api/controller"
notifUC "git.happydns.org/happyDomain/internal/usecase/notification"
)
// DeclareNotificationRoutes registers notification routes under /api/notifications.
func DeclareNotificationRoutes(
apiAuthRoutes *gin.RouterGroup,
dispatcher *notifUC.Dispatcher,
channelStore notifUC.NotificationChannelStorage,
prefStore notifUC.NotificationPreferenceStorage,
recordStore notifUC.NotificationRecordStorage,
) *controller.NotificationController {
nc := controller.NewNotificationController(dispatcher, channelStore, prefStore, recordStore)
notif := apiAuthRoutes.Group("/notifications")
// Channels
channels := notif.Group("/channels")
channels.GET("", nc.ListChannels)
channels.POST("", nc.CreateChannel)
channelID := channels.Group("/:channelId")
channelID.GET("", nc.GetChannel)
channelID.PUT("", nc.UpdateChannel)
channelID.DELETE("", nc.DeleteChannel)
channelID.POST("/test", nc.TestChannel)
// Preferences
prefs := notif.Group("/preferences")
prefs.GET("", nc.ListPreferences)
prefs.POST("", nc.CreatePreference)
prefID := prefs.Group("/:prefId")
prefID.GET("", nc.GetPreference)
prefID.PUT("", nc.UpdatePreference)
prefID.DELETE("", nc.DeletePreference)
// History
notif.GET("/history", nc.ListHistory)
return nc
}

View file

@ -27,6 +27,7 @@ import (
"git.happydns.org/happyDomain/internal/api/controller"
"git.happydns.org/happyDomain/internal/api/middleware"
checkerUC "git.happydns.org/happyDomain/internal/usecase/checker"
notifUC "git.happydns.org/happyDomain/internal/usecase/notification"
happydns "git.happydns.org/happyDomain/model"
)
@ -58,6 +59,11 @@ type Dependencies struct {
CheckPlanUC *checkerUC.CheckPlanUsecase
CheckStatusUC *checkerUC.CheckStatusUsecase
PlannedProvider checkerUC.PlannedJobProvider
NotificationDispatcher *notifUC.Dispatcher
NotificationChannels notifUC.NotificationChannelStorage
NotificationPrefs notifUC.NotificationPreferenceStorage
NotificationRecords notifUC.NotificationRecordStorage
}
// @title happyDomain API
@ -126,6 +132,18 @@ func DeclareRoutes(cfg *happydns.Options, router *gin.RouterGroup, dep Dependenc
)
}
// Initialize notification controller if dispatcher is available.
var nc *controller.NotificationController
if dep.NotificationDispatcher != nil {
nc = DeclareNotificationRoutes(
apiAuthRoutes,
dep.NotificationDispatcher,
dep.NotificationChannels,
dep.NotificationPrefs,
dep.NotificationRecords,
)
}
DeclareAuthenticationCheckRoutes(apiAuthRoutes, lc)
DeclareDomainRoutes(
apiAuthRoutes,
@ -139,6 +157,7 @@ func DeclareRoutes(cfg *happydns.Options, router *gin.RouterGroup, dep Dependenc
dep.Service,
cc,
dep.CheckStatusUC,
nc,
)
DeclareProviderRoutes(apiAuthRoutes, dep.Provider)
DeclareProviderSettingsRoutes(apiAuthRoutes, dep.ProviderSettings)

View file

@ -37,6 +37,7 @@ func DeclareZoneServiceRoutes(
serviceUC happydns.ServiceUsecase,
zoneUC happydns.ZoneUsecase,
cc *controller.CheckerController,
nc *controller.NotificationController,
) {
sc := controller.NewServiceController(zoneServiceUC, serviceUC, zoneUC)
@ -51,6 +52,6 @@ func DeclareZoneServiceRoutes(
// Mount service-scoped checker routes.
if cc != nil {
DeclareScopedCheckerRoutes(apiZonesSubdomainServiceIDRoutes, cc)
DeclareScopedCheckerRoutes(apiZonesSubdomainServiceIDRoutes, cc, nc)
}
}

View file

@ -38,6 +38,7 @@ func DeclareZoneRoutes(
zoneServiceUC happydns.ZoneServiceUsecase,
serviceUC happydns.ServiceUsecase,
cc *controller.CheckerController,
nc *controller.NotificationController,
) {
var checkStatusUC *checkerUC.CheckStatusUsecase
if cc != nil {
@ -74,6 +75,7 @@ func DeclareZoneRoutes(
serviceUC,
zoneUC,
cc,
nc,
)
apiZonesRoutes.POST("/records", zc.AddRecords)

View file

@ -23,6 +23,7 @@ package notification
import (
"errors"
"fmt"
"log"
"time"
@ -283,6 +284,25 @@ func (d *Dispatcher) sendAndRecord(ch *happydns.NotificationChannel, payload *no
}
}
// SendTestNotification sends a test notification through the given channel.
func (d *Dispatcher) SendTestNotification(ch *happydns.NotificationChannel, user *happydns.User) error {
sender, ok := d.senders[ch.Type]
if !ok {
return fmt.Errorf("no sender for channel type %q", ch.Type)
}
payload := &notifPkg.NotificationPayload{
User: user,
CheckerID: "test",
DomainName: "example.com",
OldStatus: happydns.StatusOK,
NewStatus: happydns.StatusWarn,
BaseURL: d.baseURL,
}
return sender.Send(ch, payload)
}
func (d *Dispatcher) updateState(state *happydns.NotificationState, newStatus happydns.Status) {
state.LastStatus = newStatus
state.LastNotifiedAt = time.Now()