[#7698] send system email alerts to superusers in case of an error with the automated backups
This commit is contained in:
+3
-1
@@ -1,9 +1,11 @@
|
|||||||
## v0.39.0 (WIP)
|
## v0.39.0 (WIP)
|
||||||
|
|
||||||
|
- Fixed logs bulk selection export error.
|
||||||
|
|
||||||
- Added new "SQL console" section under _Settings > Debug_ allowing executing any raw SQL query from the UI ([#2236](https://github.com/pocketbase/pocketbase/issues/2236); [#7638](https://github.com/pocketbase/pocketbase/discussions/7638)).
|
- Added new "SQL console" section under _Settings > Debug_ allowing executing any raw SQL query from the UI ([#2236](https://github.com/pocketbase/pocketbase/issues/2236); [#7638](https://github.com/pocketbase/pocketbase/discussions/7638)).
|
||||||
_Note that this is intended for one-off analytic queries, the occasional `VACUUM`/`PRAGMA optimize` or debug purposes and not as the primary interface for interacting with your PocketBase data because it can break your application if not used with proper care!_
|
_Note that this is intended for one-off analytic queries, the occasional `VACUUM`/`PRAGMA optimize` or debug purposes and not as the primary interface for interacting with your PocketBase data because it can break your application if not used with proper care!_
|
||||||
|
|
||||||
- Fixed logs bulk selection export error.
|
- Send system email alerts to superusers in case of an error with the automated backups ([#7698](https://github.com/pocketbase/pocketbase/issues/7698)).
|
||||||
|
|
||||||
- Other minor improvements (optimized logs and records list rendering, word breaking in labels, text contrast improvements, registered missing `oidc2` and `oidc3` option fields, updated default email template texts for consistency, etc.).
|
- Other minor improvements (optimized logs and records list rendering, word breaking in labels, text contrast improvements, registered missing `oidc2` and `oidc3` option fields, updated default email template texts for consistency, etc.).
|
||||||
|
|
||||||
|
|||||||
@@ -322,6 +322,19 @@ func (app *BaseApp) registerAutobackupHooks() {
|
|||||||
slog.String("name", name),
|
slog.String("name", name),
|
||||||
slog.String("error", err.Error()),
|
slog.String("error", err.Error()),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
alertError := sendSystemAlertToAllSuperusers(
|
||||||
|
app,
|
||||||
|
"Autobackup failure",
|
||||||
|
"Failed to create/upload automated backup. Raw error:\n"+err.Error(),
|
||||||
|
)
|
||||||
|
if alertError != nil {
|
||||||
|
app.Logger().Warn(
|
||||||
|
"[Backup cron] Failed to send backup error alerts",
|
||||||
|
slog.String("name", name),
|
||||||
|
slog.String("error", alertError.Error()),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
maxKeep := app.Settings().Backups.CronMaxKeep
|
maxKeep := app.Settings().Backups.CronMaxKeep
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"html"
|
||||||
|
"html/template"
|
||||||
|
"net/mail"
|
||||||
|
|
||||||
|
"github.com/pocketbase/pocketbase/tools/mailer"
|
||||||
|
)
|
||||||
|
|
||||||
|
const systemAlertHTML = `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<style>
|
||||||
|
body, html {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
color: #16161a;
|
||||||
|
background: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
font-weight: normal;
|
||||||
|
font-family: Source Sans Pro, sans-serif, emoji;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
padding: 20px 30px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
display: block;
|
||||||
|
margin: 10px 0;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
small {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
em, i {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
.alert {
|
||||||
|
padding: 15px;
|
||||||
|
background: #e4e8ec;
|
||||||
|
border-radius: 5px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>{{.AppName}} system alert occurred:</p>
|
||||||
|
<p class="alert"><strong>{{.AlertDetails}}</strong></p>
|
||||||
|
<p>For more information you could explore the logs in the dashboard of your application.</p>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
|
||||||
|
// sendSystemAlertToAllSuperusers sends a system error alert to all superusers.
|
||||||
|
//
|
||||||
|
// note: unexported for now until there is clarity around the planned log level alerts.
|
||||||
|
func sendSystemAlertToAllSuperusers(app App, subject string, details string) error {
|
||||||
|
superusers, err := app.FindAllRecords(CollectionNameSuperusers)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var alertErrors []error
|
||||||
|
for _, superuser := range superusers {
|
||||||
|
err := sendSystemAlert(app, superuser, subject, details)
|
||||||
|
if err != nil {
|
||||||
|
alertErrors = append(alertErrors, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Join(alertErrors...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendSystemAlert sends a system error alert to a single superuser.
|
||||||
|
//
|
||||||
|
// note: unexported for now until there is clarity around the planned log level alerts.
|
||||||
|
func sendSystemAlert(app App, superuser *Record, subject string, details string) error {
|
||||||
|
if !superuser.IsSuperuser() {
|
||||||
|
return errors.New("system alerts can be sent only to superusers")
|
||||||
|
}
|
||||||
|
|
||||||
|
if subject == "" || details == "" {
|
||||||
|
return errors.New("system alerts subject and details are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
data := struct {
|
||||||
|
AppName string
|
||||||
|
AlertDetails string
|
||||||
|
}{
|
||||||
|
AppName: app.Settings().Meta.AppName,
|
||||||
|
AlertDetails: details,
|
||||||
|
}
|
||||||
|
|
||||||
|
tpl := template.New("system_alert")
|
||||||
|
|
||||||
|
var parseErr error
|
||||||
|
tpl, parseErr = tpl.Parse(systemAlertHTML)
|
||||||
|
if parseErr != nil {
|
||||||
|
return parseErr
|
||||||
|
}
|
||||||
|
|
||||||
|
var buff bytes.Buffer
|
||||||
|
executeErr := tpl.Execute(&buff, data)
|
||||||
|
if executeErr != nil {
|
||||||
|
return executeErr
|
||||||
|
}
|
||||||
|
|
||||||
|
message := &mailer.Message{
|
||||||
|
From: mail.Address{
|
||||||
|
Name: app.Settings().Meta.SenderName,
|
||||||
|
Address: app.Settings().Meta.SenderAddress,
|
||||||
|
},
|
||||||
|
To: []mail.Address{{Address: superuser.Email()}},
|
||||||
|
Subject: "[" + app.Settings().Meta.AppName + " system alert] " + html.EscapeString(subject),
|
||||||
|
HTML: buff.String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.NewMailClient().Send(message)
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSendSystemAlert(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testDataDir, err := os.MkdirTemp("", "sendSystemAlert_pb_data")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(testDataDir)
|
||||||
|
|
||||||
|
testApp := NewBaseApp(BaseAppConfig{
|
||||||
|
DataDir: testDataDir,
|
||||||
|
})
|
||||||
|
defer testApp.ResetBootstrapState()
|
||||||
|
|
||||||
|
if err := testApp.Bootstrap(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := createTestSuperusers(testApp, 3); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
superuser, err := testApp.FindAuthRecordByEmail(CollectionNameSuperusers, "test1@example.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var sendCalls int
|
||||||
|
testApp.OnMailerSend().BindFunc(func(e *MailerEvent) error {
|
||||||
|
sendCalls++
|
||||||
|
|
||||||
|
if !strings.Contains(e.Message.Subject, "test_subject") {
|
||||||
|
t.Fatalf("Missing %q in Message.Subject:\n%s", "test_subject", e.Message.Subject)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(e.Message.HTML, "test_details") {
|
||||||
|
t.Fatalf("Missing %q in Message.HTML:\n%s", "test_details", e.Message.HTML)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(e.Message.To) != 1 || e.Message.To[0].Address != "test1@example.com" {
|
||||||
|
t.Fatalf("Expected To address %q, got %v", "test1@example.com", e.Message.To)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
sendSystemAlert(testApp, superuser, "test_subject", "test_details")
|
||||||
|
|
||||||
|
if sendCalls != 1 {
|
||||||
|
t.Fatalf("Expected 1 mail send call, got %d", sendCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendSystemAlertToAllSuperusers(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testDataDir, err := os.MkdirTemp("", "sendSystemAlertToAllSuperusers_pb_data")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(testDataDir)
|
||||||
|
|
||||||
|
testApp := NewBaseApp(BaseAppConfig{
|
||||||
|
DataDir: testDataDir,
|
||||||
|
})
|
||||||
|
defer testApp.ResetBootstrapState()
|
||||||
|
|
||||||
|
if err := testApp.Bootstrap(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := createTestSuperusers(testApp, 3); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var sendCalls int
|
||||||
|
testApp.OnMailerSend().BindFunc(func(e *MailerEvent) error {
|
||||||
|
sendCalls++
|
||||||
|
|
||||||
|
if !strings.Contains(e.Message.Subject, "test_subject") {
|
||||||
|
t.Fatalf("Missing %q in Message.Subject:\n%s", "test_subject", e.Message.Subject)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(e.Message.HTML, "test_details") {
|
||||||
|
t.Fatalf("Missing %q in Message.HTML:\n%s", "test_details", e.Message.HTML)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
sendSystemAlertToAllSuperusers(testApp, "test_subject", "test_details")
|
||||||
|
|
||||||
|
if sendCalls != 3 {
|
||||||
|
t.Fatalf("Expected 3 mail send calls, got %d", sendCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTestSuperusers(app App, total int) error {
|
||||||
|
superusersCollection, err := app.FindCollectionByNameOrId(CollectionNameSuperusers)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range total {
|
||||||
|
superuser := NewRecord(superusersCollection)
|
||||||
|
superuser.SetEmail("test" + strconv.Itoa(i+1) + "@example.com")
|
||||||
|
superuser.SetRandomPassword()
|
||||||
|
|
||||||
|
if err := app.Save(superuser); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user