Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
version: 10
- uses: actions/setup-go@v6
with:
go-version: 1.26.3
go-version: 1.26.4
- name: Install nfpm
run: |
echo 'deb [trusted=yes] https://repo.goreleaser.com/apt/ /' | sudo tee /etc/apt/sources.list.d/goreleaser.list
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/snapshot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ jobs:
version: 10
- uses: actions/setup-go@v6
with:
go-version: 1.26.3
go-version: 1.26.4
- name: Unit tests
env:
TEST_POSTGRES_HOST: runner.local
Expand Down Expand Up @@ -90,7 +90,7 @@ jobs:
version: 10
- uses: actions/setup-go@v6
with:
go-version: 1.26.3
go-version: 1.26.4
- name: Install nfpm
run: |
echo 'deb [trusted=yes] https://repo.goreleaser.com/apt/ /' | sudo tee /etc/apt/sources.list.d/goreleaser.list
Expand Down
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,4 @@ build/
**/*.mock.go
**/*.generated.go
**/*.generated.ts
i18n/*.md
coverage.out
695 changes: 695 additions & 0 deletions AGENTS.md

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## 2.42.0

- Ignition now includes alerts for important events
- Get notified when certificates are expiring, renewals succeed or fail, nginx reloads fails and many more
- Inbox to see what happened, browse history, and mark alerts as read
- Send alerts by email (SMTP, Resend, or AWS SES), SMS or other systems (AWS SNS), to Telegram, Slack, Discord, and
custom webhooks with more integrations available in future versions.
- Choose which events go to each channel and set your preferred notification language
- Minor fixes and improvements

## 2.41.0
Expand Down
9 changes: 7 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ LDFLAGS := -X 'dillmann.com.br/nginx-ignition/core/common/version.Number=$(VERSI
./database/... \
./integration/docker/... \
./integration/truenas/... \
./notification/smtp/... \
./vpn/netbird/... \
./vpn/tailscale/...

Expand Down Expand Up @@ -119,6 +120,7 @@ LDFLAGS := -X 'dillmann.com.br/nginx-ignition/core/common/version.Number=$(VERSI
./database/... \
./integration/docker/... \
./integration/truenas/... \
./notification/smtp/... \
./vpn/netbird/... \
./vpn/tailscale/...
go tool golangci-lint run --fix \
Expand All @@ -133,15 +135,16 @@ LDFLAGS := -X 'dillmann.com.br/nginx-ignition/core/common/version.Number=$(VERSI
./database/... \
./integration/docker/... \
./integration/truenas/... \
./notification/smtp/... \
./vpn/netbird/... \
./vpn/tailscale/...

clean:
@find api application certificate core database i18n integration vpn -type f -name "*.mock.go" -delete
@find api application certificate core database i18n integration notification vpn -type f -name "*.mock.go" -delete

.backend-test-mocks: .backend-prerequisites
@echo "Generating mock files..."
@find api application certificate core database i18n integration vpn -type f -name "*.go" \
@find api application certificate core database i18n integration notification vpn -type f -name "*.go" \
-not -name "*_test.go" \
-exec sh -c 'grep -q "^type [a-zA-Z0-9_]* interface" "$$1" && echo "$$1"' _ {} \; | \
while read -r file; do \
Expand Down Expand Up @@ -176,6 +179,7 @@ clean:
./database/... \
./integration/docker/... \
./integration/truenas/... \
./notification/smtp/... \
./vpn/netbird/... \
./vpn/tailscale/...

Expand Down Expand Up @@ -208,6 +212,7 @@ update-dependencies: .backend-prerequisites .frontend-prerequisites .update-ngin
cd database && go get -u ./...
cd integration/docker && go get -u ./...
cd integration/truenas && go get -u ./...
cd notification/smtp && go get -u ./...
cd tools && go get -u ./...
cd vpn/netbird && go get -u ./...
cd vpn/tailscale && go get -u ./...
Expand Down
2 changes: 1 addition & 1 deletion api/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module dillmann.com.br/nginx-ignition/api

go 1.26.3
go 1.26.4

require (
github.com/gin-gonic/gin v1.12.0
Expand Down
2 changes: 2 additions & 0 deletions api/installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"dillmann.com.br/nginx-ignition/api/i18n"
"dillmann.com.br/nginx-ignition/api/integration"
"dillmann.com.br/nginx-ignition/api/nginx"
"dillmann.com.br/nginx-ignition/api/notification"
"dillmann.com.br/nginx-ignition/api/settings"
"dillmann.com.br/nginx-ignition/api/stream"
"dillmann.com.br/nginx-ignition/api/user"
Expand All @@ -29,6 +30,7 @@ func Install() error {
cache.Install,
certificate.Install,
user.Install,
notification.Install,
host.Install,
i18n.Install,
integration.Install,
Expand Down
40 changes: 40 additions & 0 deletions api/notification/artifacts_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package notification

import (
"time"

"github.com/google/uuid"

"dillmann.com.br/nginx-ignition/core/notification"
)

func sampleNotification(userID uuid.UUID) *notification.Notification {
notificationID := uuid.New()

return &notification.Notification{
ID: notificationID,
UserID: userID,
Title: "Certificate renewed",
Summary: "example.com was renewed",
Category: notification.CategoryCertificateRenewed,
ReadAt: new(time.Now()),
CreatedAt: time.Now(),
Payload: notification.Payload{
OccurredAt: time.Now(),
Tags: map[string]string{"domain": "example.com"},
Sections: []notification.DeliverableContentSection{
{Body: "Renewal completed"},
},
Actions: []notification.DeliverableAction{
{Label: "View", URL: "/certificates"},
},
},
RelatedEntities: []notification.RelatedEntity{
{Type: "certificate", ID: uuid.New(), Name: "example.com"},
},
Submissions: []notification.ProviderSubmission{
{Provider: "SMTP", Status: notification.ProviderSubmissionStatusSuccess},
},
DeliveryCompleted: true,
}
}
27 changes: 27 additions & 0 deletions api/notification/available_providers_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package notification

import (
"net/http"

"github.com/gin-gonic/gin"

"dillmann.com.br/nginx-ignition/core/notification"
)

type availableProvidersHandler struct {
commands notification.Commands
}

func (h availableProvidersHandler) handle(ctx *gin.Context) {
data, err := h.commands.GetAvailableProviders(ctx.Request.Context())
if err != nil {
panic(err)
}

payload := make([]availableProviderResponse, len(data))
for index, item := range data {
payload[index] = toAvailableProviderDTO(item)
}

ctx.JSON(http.StatusOK, payload)
}
45 changes: 45 additions & 0 deletions api/notification/available_providers_handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package notification

import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"

"dillmann.com.br/nginx-ignition/core/notification"
)

func Test_availableProvidersHandler(t *testing.T) {
t.Run("handle", func(t *testing.T) {
t.Run("returns 200 OK with providers list on success", func(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()

providers := []notification.AvailableProvider{}
commands := notification.NewMockedCommands(controller)
commands.EXPECT().
GetAvailableProviders(gomock.Any()).
Return(providers, nil)

recorder := httptest.NewRecorder()
ginContext, _ := gin.CreateTestContext(recorder)
ginContext.Request = httptest.NewRequest(
http.MethodGet,
"/available-providers",
nil,
)

handler := availableProvidersHandler{commands: commands}
handler.handle(ginContext)

assert.Equal(t, http.StatusOK, recorder.Code)
var response []availableProviderResponse
json.Unmarshal(recorder.Body.Bytes(), &response)
assert.Len(t, response, 0)
})
})
}
31 changes: 31 additions & 0 deletions api/notification/categories_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package notification

import (
"net/http"

"github.com/gin-gonic/gin"

"dillmann.com.br/nginx-ignition/core/notification"
)

type categoriesHandler struct {
commands notification.Commands
}

func (h categoriesHandler) handle(ctx *gin.Context) {
if _, ok := currentUserID(ctx); !ok {
return
}

data, err := h.commands.ListCategories(ctx.Request.Context())
if err != nil {
panic(err)
}

payload := make([]categoryResponse, len(data))
for index, item := range data {
payload[index] = toCategoryDTO(item)
}

ctx.JSON(http.StatusOK, payload)
}
65 changes: 65 additions & 0 deletions api/notification/categories_handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package notification

import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"

"dillmann.com.br/nginx-ignition/api/common/authorization"
"dillmann.com.br/nginx-ignition/core/notification"
"dillmann.com.br/nginx-ignition/core/user"
)

func Test_categoriesHandler(t *testing.T) {
t.Run("handle", func(t *testing.T) {
t.Run("returns 200 OK with categories on success", func(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()

userID := uuid.New()
categories := []notification.CategoryInfo{
{ID: notification.CategoryCertificateRenewed},
}
commands := notification.NewMockedCommands(controller)
commands.EXPECT().
ListCategories(gomock.Any()).
Return(categories, nil)

handler := categoriesHandler{commands: commands}
engine := gin.New()
engine.Use(func(ginContext *gin.Context) {
ginContext.Set("ABAC:Subject", &authorization.Subject{User: &user.User{ID: userID}})
ginContext.Next()
})
engine.GET("/categories", handler.handle)

recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/categories", nil)
engine.ServeHTTP(recorder, request)

assert.Equal(t, http.StatusOK, recorder.Code)
var response []categoryResponse
json.Unmarshal(recorder.Body.Bytes(), &response)
assert.Len(t, response, 1)
assert.Equal(t, string(notification.CategoryCertificateRenewed), response[0].ID)
})

t.Run("returns 401 Unauthorized without subject", func(t *testing.T) {
handler := categoriesHandler{commands: nil}
engine := gin.New()
engine.GET("/categories", handler.handle)

recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/categories", nil)
engine.ServeHTTP(recorder, request)

assert.Equal(t, http.StatusUnauthorized, recorder.Code)
})
})
}
Loading
Loading