diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go
index 538d67b81fc2d..6554372157207 100644
--- a/coderd/apidoc/docs.go
+++ b/coderd/apidoc/docs.go
@@ -1547,6 +1547,71 @@ const docTemplate = `{
}
}
},
+ "/notifications/settings": {
+ "get": {
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "General"
+ ],
+ "summary": "Get notifications settings",
+ "operationId": "get-notifications-settings",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.NotificationsSettings"
+ }
+ }
+ }
+ },
+ "put": {
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "General"
+ ],
+ "summary": "Update notifications settings",
+ "operationId": "update-notifications-settings",
+ "parameters": [
+ {
+ "description": "Notifications settings request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.NotificationsSettings"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.NotificationsSettings"
+ }
+ },
+ "304": {
+ "description": "Not Modified"
+ }
+ }
+ }
+ },
"/oauth2-provider/apps": {
"get": {
"security": [
@@ -10009,6 +10074,14 @@ const docTemplate = `{
}
}
},
+ "codersdk.NotificationsSettings": {
+ "type": "object",
+ "properties": {
+ "notifier_paused": {
+ "type": "boolean"
+ }
+ }
+ },
"codersdk.NotificationsWebhookConfig": {
"type": "object",
"properties": {
@@ -11036,6 +11109,7 @@ const docTemplate = `{
"license",
"convert_login",
"health_settings",
+ "notifications_settings",
"workspace_proxy",
"organization",
"oauth2_provider_app",
@@ -11054,6 +11128,7 @@ const docTemplate = `{
"ResourceTypeLicense",
"ResourceTypeConvertLogin",
"ResourceTypeHealthSettings",
+ "ResourceTypeNotificationsSettings",
"ResourceTypeWorkspaceProxy",
"ResourceTypeOrganization",
"ResourceTypeOAuth2ProviderApp",
diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json
index 49dfde7a6b651..03b0ba7716e2b 100644
--- a/coderd/apidoc/swagger.json
+++ b/coderd/apidoc/swagger.json
@@ -1344,6 +1344,61 @@
}
}
},
+ "/notifications/settings": {
+ "get": {
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ],
+ "produces": ["application/json"],
+ "tags": ["General"],
+ "summary": "Get notifications settings",
+ "operationId": "get-notifications-settings",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.NotificationsSettings"
+ }
+ }
+ }
+ },
+ "put": {
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ],
+ "consumes": ["application/json"],
+ "produces": ["application/json"],
+ "tags": ["General"],
+ "summary": "Update notifications settings",
+ "operationId": "update-notifications-settings",
+ "parameters": [
+ {
+ "description": "Notifications settings request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.NotificationsSettings"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/codersdk.NotificationsSettings"
+ }
+ },
+ "304": {
+ "description": "Not Modified"
+ }
+ }
+ }
+ },
"/oauth2-provider/apps": {
"get": {
"security": [
@@ -8978,6 +9033,14 @@
}
}
},
+ "codersdk.NotificationsSettings": {
+ "type": "object",
+ "properties": {
+ "notifier_paused": {
+ "type": "boolean"
+ }
+ }
+ },
"codersdk.NotificationsWebhookConfig": {
"type": "object",
"properties": {
@@ -9958,6 +10021,7 @@
"license",
"convert_login",
"health_settings",
+ "notifications_settings",
"workspace_proxy",
"organization",
"oauth2_provider_app",
@@ -9976,6 +10040,7 @@
"ResourceTypeLicense",
"ResourceTypeConvertLogin",
"ResourceTypeHealthSettings",
+ "ResourceTypeNotificationsSettings",
"ResourceTypeWorkspaceProxy",
"ResourceTypeOrganization",
"ResourceTypeOAuth2ProviderApp",
diff --git a/coderd/audit/diff.go b/coderd/audit/diff.go
index 09ae80c9ddf90..129b904c75b03 100644
--- a/coderd/audit/diff.go
+++ b/coderd/audit/diff.go
@@ -20,6 +20,7 @@ type Auditable interface {
database.WorkspaceProxy |
database.AuditOAuthConvertState |
database.HealthSettings |
+ database.NotificationsSettings |
database.OAuth2ProviderApp |
database.OAuth2ProviderAppSecret |
database.CustomRole |
diff --git a/coderd/audit/request.go b/coderd/audit/request.go
index 1c027fc85527f..403bb13ccf3f8 100644
--- a/coderd/audit/request.go
+++ b/coderd/audit/request.go
@@ -99,6 +99,8 @@ func ResourceTarget[T Auditable](tgt T) string {
return string(typed.ToLoginType)
case database.HealthSettings:
return "" // no target?
+ case database.NotificationsSettings:
+ return "" // no target?
case database.OAuth2ProviderApp:
return typed.Name
case database.OAuth2ProviderAppSecret:
@@ -142,6 +144,9 @@ func ResourceID[T Auditable](tgt T) uuid.UUID {
case database.HealthSettings:
// Artificial ID for auditing purposes
return typed.ID
+ case database.NotificationsSettings:
+ // Artificial ID for auditing purposes
+ return typed.ID
case database.OAuth2ProviderApp:
return typed.ID
case database.OAuth2ProviderAppSecret:
@@ -183,6 +188,8 @@ func ResourceType[T Auditable](tgt T) database.ResourceType {
return database.ResourceTypeConvertLogin
case database.HealthSettings:
return database.ResourceTypeHealthSettings
+ case database.NotificationsSettings:
+ return database.ResourceTypeNotificationsSettings
case database.OAuth2ProviderApp:
return database.ResourceTypeOauth2ProviderApp
case database.OAuth2ProviderAppSecret:
@@ -225,6 +232,9 @@ func ResourceRequiresOrgID[T Auditable]() bool {
case database.HealthSettings:
// Artificial ID for auditing purposes
return false
+ case database.NotificationsSettings:
+ // Artificial ID for auditing purposes
+ return false
case database.OAuth2ProviderApp:
return false
case database.OAuth2ProviderAppSecret:
diff --git a/coderd/coderd.go b/coderd/coderd.go
index 3e77490651e01..0a3414fdb984c 100644
--- a/coderd/coderd.go
+++ b/coderd/coderd.go
@@ -1243,6 +1243,11 @@ func New(options *Options) *API {
})
})
})
+ r.Route("/notifications", func(r chi.Router) {
+ r.Use(apiKeyMiddleware)
+ r.Get("/settings", api.notificationsSettings)
+ r.Put("/settings", api.putNotificationsSettings)
+ })
})
if options.SwaggerEndpoint {
diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go
index 67dadd5d74e19..1feea0c23bbe7 100644
--- a/coderd/database/dbauthz/dbauthz.go
+++ b/coderd/database/dbauthz/dbauthz.go
@@ -1479,6 +1479,11 @@ func (q *querier) GetNotificationMessagesByStatus(ctx context.Context, arg datab
return q.db.GetNotificationMessagesByStatus(ctx, arg)
}
+func (q *querier) GetNotificationsSettings(ctx context.Context) (string, error) {
+ // No authz checks
+ return q.db.GetNotificationsSettings(ctx)
+}
+
func (q *querier) GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (database.OAuth2ProviderApp, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceOauth2App); err != nil {
return database.OAuth2ProviderApp{}, err
@@ -3687,6 +3692,13 @@ func (q *querier) UpsertLogoURL(ctx context.Context, value string) error {
return q.db.UpsertLogoURL(ctx, value)
}
+func (q *querier) UpsertNotificationsSettings(ctx context.Context, value string) error {
+ if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil {
+ return err
+ }
+ return q.db.UpsertNotificationsSettings(ctx, value)
+}
+
func (q *querier) UpsertOAuthSigningKey(ctx context.Context, value string) error {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil {
return err
diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go
index e615fd3054d6e..52d375116e6a3 100644
--- a/coderd/database/dbauthz/dbauthz_test.go
+++ b/coderd/database/dbauthz/dbauthz_test.go
@@ -2350,6 +2350,12 @@ func (s *MethodTestSuite) TestSystemFunctions() {
s.Run("UpsertHealthSettings", s.Subtest(func(db database.Store, check *expects) {
check.Args("foo").Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate)
}))
+ s.Run("GetNotificationsSettings", s.Subtest(func(db database.Store, check *expects) {
+ check.Args().Asserts()
+ }))
+ s.Run("UpsertNotificationsSettings", s.Subtest(func(db database.Store, check *expects) {
+ check.Args("foo").Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate)
+ }))
s.Run("GetDeploymentWorkspaceAgentStats", s.Subtest(func(db database.Store, check *expects) {
check.Args(time.Time{}).Asserts()
}))
diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go
index 3954e47f43846..420d6bc466420 100644
--- a/coderd/database/dbmem/dbmem.go
+++ b/coderd/database/dbmem/dbmem.go
@@ -199,6 +199,7 @@ type data struct {
lastUpdateCheck []byte
announcementBanners []byte
healthSettings []byte
+ notificationsSettings []byte
applicationName string
logoURL string
appSecurityKey string
@@ -2771,6 +2772,17 @@ func (q *FakeQuerier) GetNotificationMessagesByStatus(_ context.Context, arg dat
return out, nil
}
+func (q *FakeQuerier) GetNotificationsSettings(_ context.Context) (string, error) {
+ q.mutex.RLock()
+ defer q.mutex.RUnlock()
+
+ if q.notificationsSettings == nil {
+ return "{}", nil
+ }
+
+ return string(q.notificationsSettings), nil
+}
+
func (q *FakeQuerier) GetOAuth2ProviderAppByID(_ context.Context, id uuid.UUID) (database.OAuth2ProviderApp, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
@@ -8668,8 +8680,8 @@ func (q *FakeQuerier) UpsertDefaultProxy(_ context.Context, arg database.UpsertD
}
func (q *FakeQuerier) UpsertHealthSettings(_ context.Context, data string) error {
- q.mutex.RLock()
- defer q.mutex.RUnlock()
+ q.mutex.Lock()
+ defer q.mutex.Unlock()
q.healthSettings = []byte(data)
return nil
@@ -8717,13 +8729,21 @@ func (q *FakeQuerier) UpsertLastUpdateCheck(_ context.Context, data string) erro
}
func (q *FakeQuerier) UpsertLogoURL(_ context.Context, data string) error {
- q.mutex.RLock()
- defer q.mutex.RUnlock()
+ q.mutex.Lock()
+ defer q.mutex.Unlock()
q.logoURL = data
return nil
}
+func (q *FakeQuerier) UpsertNotificationsSettings(_ context.Context, data string) error {
+ q.mutex.Lock()
+ defer q.mutex.Unlock()
+
+ q.notificationsSettings = []byte(data)
+ return nil
+}
+
func (q *FakeQuerier) UpsertOAuthSigningKey(_ context.Context, value string) error {
q.mutex.Lock()
defer q.mutex.Unlock()
diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go
index 0a7ecd4fb5f10..638aeaac14746 100644
--- a/coderd/database/dbmetrics/dbmetrics.go
+++ b/coderd/database/dbmetrics/dbmetrics.go
@@ -739,6 +739,13 @@ func (m metricsStore) GetNotificationMessagesByStatus(ctx context.Context, arg d
return r0, r1
}
+func (m metricsStore) GetNotificationsSettings(ctx context.Context) (string, error) {
+ start := time.Now()
+ r0, r1 := m.s.GetNotificationsSettings(ctx)
+ m.queryLatencies.WithLabelValues("GetNotificationsSettings").Observe(time.Since(start).Seconds())
+ return r0, r1
+}
+
func (m metricsStore) GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (database.OAuth2ProviderApp, error) {
start := time.Now()
r0, r1 := m.s.GetOAuth2ProviderAppByID(ctx, id)
@@ -2300,6 +2307,13 @@ func (m metricsStore) UpsertLogoURL(ctx context.Context, value string) error {
return r0
}
+func (m metricsStore) UpsertNotificationsSettings(ctx context.Context, value string) error {
+ start := time.Now()
+ r0 := m.s.UpsertNotificationsSettings(ctx, value)
+ m.queryLatencies.WithLabelValues("UpsertNotificationsSettings").Observe(time.Since(start).Seconds())
+ return r0
+}
+
func (m metricsStore) UpsertOAuthSigningKey(ctx context.Context, value string) error {
start := time.Now()
r0 := m.s.UpsertOAuthSigningKey(ctx, value)
diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go
index 982a6472ec16c..5fc5403a64f7f 100644
--- a/coderd/database/dbmock/dbmock.go
+++ b/coderd/database/dbmock/dbmock.go
@@ -1467,6 +1467,21 @@ func (mr *MockStoreMockRecorder) GetNotificationMessagesByStatus(arg0, arg1 any)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationMessagesByStatus", reflect.TypeOf((*MockStore)(nil).GetNotificationMessagesByStatus), arg0, arg1)
}
+// GetNotificationsSettings mocks base method.
+func (m *MockStore) GetNotificationsSettings(arg0 context.Context) (string, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetNotificationsSettings", arg0)
+ ret0, _ := ret[0].(string)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetNotificationsSettings indicates an expected call of GetNotificationsSettings.
+func (mr *MockStoreMockRecorder) GetNotificationsSettings(arg0 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationsSettings", reflect.TypeOf((*MockStore)(nil).GetNotificationsSettings), arg0)
+}
+
// GetOAuth2ProviderAppByID mocks base method.
func (m *MockStore) GetOAuth2ProviderAppByID(arg0 context.Context, arg1 uuid.UUID) (database.OAuth2ProviderApp, error) {
m.ctrl.T.Helper()
@@ -4813,6 +4828,20 @@ func (mr *MockStoreMockRecorder) UpsertLogoURL(arg0, arg1 any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertLogoURL", reflect.TypeOf((*MockStore)(nil).UpsertLogoURL), arg0, arg1)
}
+// UpsertNotificationsSettings mocks base method.
+func (m *MockStore) UpsertNotificationsSettings(arg0 context.Context, arg1 string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "UpsertNotificationsSettings", arg0, arg1)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// UpsertNotificationsSettings indicates an expected call of UpsertNotificationsSettings.
+func (mr *MockStoreMockRecorder) UpsertNotificationsSettings(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertNotificationsSettings", reflect.TypeOf((*MockStore)(nil).UpsertNotificationsSettings), arg0, arg1)
+}
+
// UpsertOAuthSigningKey mocks base method.
func (m *MockStore) UpsertOAuthSigningKey(arg0 context.Context, arg1 string) error {
m.ctrl.T.Helper()
diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql
index f0b9cb311606f..3f2da45155da1 100644
--- a/coderd/database/dump.sql
+++ b/coderd/database/dump.sql
@@ -163,7 +163,8 @@ CREATE TYPE resource_type AS ENUM (
'oauth2_provider_app',
'oauth2_provider_app_secret',
'custom_role',
- 'organization_member'
+ 'organization_member',
+ 'notifications_settings'
);
CREATE TYPE startup_script_behavior AS ENUM (
diff --git a/coderd/database/migrations/000223_notifications_settings_audit.down.sql b/coderd/database/migrations/000223_notifications_settings_audit.down.sql
new file mode 100644
index 0000000000000..de5e2cb77a38d
--- /dev/null
+++ b/coderd/database/migrations/000223_notifications_settings_audit.down.sql
@@ -0,0 +1,2 @@
+-- Nothing to do
+-- It's not possible to drop enum values from enum types, so the up migration has "IF NOT EXISTS".
diff --git a/coderd/database/migrations/000223_notifications_settings_audit.up.sql b/coderd/database/migrations/000223_notifications_settings_audit.up.sql
new file mode 100644
index 0000000000000..09afa99193166
--- /dev/null
+++ b/coderd/database/migrations/000223_notifications_settings_audit.up.sql
@@ -0,0 +1,2 @@
+-- This has to be outside a transaction
+ALTER TYPE resource_type ADD VALUE IF NOT EXISTS 'notifications_settings';
diff --git a/coderd/database/models.go b/coderd/database/models.go
index 7f34d7680abf2..4ff84ddc8891f 100644
--- a/coderd/database/models.go
+++ b/coderd/database/models.go
@@ -1352,6 +1352,7 @@ const (
ResourceTypeOauth2ProviderAppSecret ResourceType = "oauth2_provider_app_secret"
ResourceTypeCustomRole ResourceType = "custom_role"
ResourceTypeOrganizationMember ResourceType = "organization_member"
+ ResourceTypeNotificationsSettings ResourceType = "notifications_settings"
)
func (e *ResourceType) Scan(src interface{}) error {
@@ -1407,7 +1408,8 @@ func (e ResourceType) Valid() bool {
ResourceTypeOauth2ProviderApp,
ResourceTypeOauth2ProviderAppSecret,
ResourceTypeCustomRole,
- ResourceTypeOrganizationMember:
+ ResourceTypeOrganizationMember,
+ ResourceTypeNotificationsSettings:
return true
}
return false
@@ -1432,6 +1434,7 @@ func AllResourceTypeValues() []ResourceType {
ResourceTypeOauth2ProviderAppSecret,
ResourceTypeCustomRole,
ResourceTypeOrganizationMember,
+ ResourceTypeNotificationsSettings,
}
}
diff --git a/coderd/database/querier.go b/coderd/database/querier.go
index 75ade1dc12e5e..c4ce70cea28fe 100644
--- a/coderd/database/querier.go
+++ b/coderd/database/querier.go
@@ -161,6 +161,7 @@ type sqlcQuerier interface {
GetLicenses(ctx context.Context) ([]License, error)
GetLogoURL(ctx context.Context) (string, error)
GetNotificationMessagesByStatus(ctx context.Context, arg GetNotificationMessagesByStatusParams) ([]NotificationMessage, error)
+ GetNotificationsSettings(ctx context.Context) (string, error)
GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderApp, error)
GetOAuth2ProviderAppCodeByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderAppCode, error)
GetOAuth2ProviderAppCodeByPrefix(ctx context.Context, secretPrefix []byte) (OAuth2ProviderAppCode, error)
@@ -454,6 +455,7 @@ type sqlcQuerier interface {
UpsertJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg UpsertJFrogXrayScanByWorkspaceAndAgentIDParams) error
UpsertLastUpdateCheck(ctx context.Context, value string) error
UpsertLogoURL(ctx context.Context, value string) error
+ UpsertNotificationsSettings(ctx context.Context, value string) error
UpsertOAuthSigningKey(ctx context.Context, value string) error
UpsertProvisionerDaemon(ctx context.Context, arg UpsertProvisionerDaemonParams) (ProvisionerDaemon, error)
UpsertTailnetAgent(ctx context.Context, arg UpsertTailnetAgentParams) (TailnetAgent, error)
diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go
index 95f25ee1dbd11..83be6184706ce 100644
--- a/coderd/database/queries.sql.go
+++ b/coderd/database/queries.sql.go
@@ -6319,6 +6319,18 @@ func (q *sqlQuerier) GetLogoURL(ctx context.Context) (string, error) {
return value, err
}
+const getNotificationsSettings = `-- name: GetNotificationsSettings :one
+SELECT
+ COALESCE((SELECT value FROM site_configs WHERE key = 'notifications_settings'), '{}') :: text AS notifications_settings
+`
+
+func (q *sqlQuerier) GetNotificationsSettings(ctx context.Context) (string, error) {
+ row := q.db.QueryRowContext(ctx, getNotificationsSettings)
+ var notifications_settings string
+ err := row.Scan(¬ifications_settings)
+ return notifications_settings, err
+}
+
const getOAuthSigningKey = `-- name: GetOAuthSigningKey :one
SELECT value FROM site_configs WHERE key = 'oauth_signing_key'
`
@@ -6431,6 +6443,16 @@ func (q *sqlQuerier) UpsertLogoURL(ctx context.Context, value string) error {
return err
}
+const upsertNotificationsSettings = `-- name: UpsertNotificationsSettings :exec
+INSERT INTO site_configs (key, value) VALUES ('notifications_settings', $1)
+ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'notifications_settings'
+`
+
+func (q *sqlQuerier) UpsertNotificationsSettings(ctx context.Context, value string) error {
+ _, err := q.db.ExecContext(ctx, upsertNotificationsSettings, value)
+ return err
+}
+
const upsertOAuthSigningKey = `-- name: UpsertOAuthSigningKey :exec
INSERT INTO site_configs (key, value) VALUES ('oauth_signing_key', $1)
ON CONFLICT (key) DO UPDATE set value = $1 WHERE site_configs.key = 'oauth_signing_key'
diff --git a/coderd/database/queries/siteconfig.sql b/coderd/database/queries/siteconfig.sql
index 2b56a6d1455af..9287a4aee0b54 100644
--- a/coderd/database/queries/siteconfig.sql
+++ b/coderd/database/queries/siteconfig.sql
@@ -79,3 +79,13 @@ SELECT
-- name: UpsertHealthSettings :exec
INSERT INTO site_configs (key, value) VALUES ('health_settings', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'health_settings';
+
+-- name: GetNotificationsSettings :one
+SELECT
+ COALESCE((SELECT value FROM site_configs WHERE key = 'notifications_settings'), '{}') :: text AS notifications_settings
+;
+
+-- name: UpsertNotificationsSettings :exec
+INSERT INTO site_configs (key, value) VALUES ('notifications_settings', $1)
+ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'notifications_settings';
+
diff --git a/coderd/database/types.go b/coderd/database/types.go
index fd7a2fed82300..7113b09e14a70 100644
--- a/coderd/database/types.go
+++ b/coderd/database/types.go
@@ -30,6 +30,11 @@ type HealthSettings struct {
DismissedHealthchecks []healthsdk.HealthSection `db:"dismissed_healthchecks" json:"dismissed_healthchecks"`
}
+type NotificationsSettings struct {
+ ID uuid.UUID `db:"id" json:"id"`
+ NotifierPaused bool `db:"notifier_paused" json:"notifier_paused"`
+}
+
type Actions []policy.Action
func (a *Actions) Scan(src interface{}) error {
diff --git a/coderd/notifications.go b/coderd/notifications.go
new file mode 100644
index 0000000000000..f6bcbe0c7183d
--- /dev/null
+++ b/coderd/notifications.go
@@ -0,0 +1,122 @@
+package coderd
+
+import (
+ "bytes"
+ "encoding/json"
+ "net/http"
+
+ "github.com/google/uuid"
+
+ "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/rbac/policy"
+ "github.com/coder/coder/v2/codersdk"
+)
+
+// @Summary Get notifications settings
+// @ID get-notifications-settings
+// @Security CoderSessionToken
+// @Produce json
+// @Tags General
+// @Success 200 {object} codersdk.NotificationsSettings
+// @Router /notifications/settings [get]
+func (api *API) notificationsSettings(rw http.ResponseWriter, r *http.Request) {
+ settingsJSON, err := api.Database.GetNotificationsSettings(r.Context())
+ if err != nil {
+ httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
+ Message: "Failed to fetch current notifications settings.",
+ Detail: err.Error(),
+ })
+ return
+ }
+
+ var settings codersdk.NotificationsSettings
+ if len(settingsJSON) > 0 {
+ err = json.Unmarshal([]byte(settingsJSON), &settings)
+ if err != nil {
+ httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
+ Message: "Failed to unmarshal notifications settings.",
+ Detail: err.Error(),
+ })
+ return
+ }
+ }
+ httpapi.Write(r.Context(), rw, http.StatusOK, settings)
+}
+
+// @Summary Update notifications settings
+// @ID update-notifications-settings
+// @Security CoderSessionToken
+// @Accept json
+// @Produce json
+// @Tags General
+// @Param request body codersdk.NotificationsSettings true "Notifications settings request"
+// @Success 200 {object} codersdk.NotificationsSettings
+// @Success 304
+// @Router /notifications/settings [put]
+func (api *API) putNotificationsSettings(rw http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceDeploymentConfig) {
+ httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
+ Message: "Insufficient permissions to update notifications settings.",
+ })
+ return
+ }
+
+ var settings codersdk.NotificationsSettings
+ if !httpapi.Read(ctx, rw, r, &settings) {
+ return
+ }
+
+ settingsJSON, err := json.Marshal(&settings)
+ if err != nil {
+ httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
+ Message: "Failed to marshal notifications settings.",
+ Detail: err.Error(),
+ })
+ return
+ }
+
+ currentSettingsJSON, err := api.Database.GetNotificationsSettings(r.Context())
+ if err != nil {
+ httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
+ Message: "Failed to fetch current notifications settings.",
+ Detail: err.Error(),
+ })
+ return
+ }
+
+ if bytes.Equal(settingsJSON, []byte(currentSettingsJSON)) {
+ // See: https://www.rfc-editor.org/rfc/rfc7232#section-4.1
+ httpapi.Write(r.Context(), rw, http.StatusNotModified, nil)
+ return
+ }
+
+ auditor := api.Auditor.Load()
+ aReq, commitAudit := audit.InitRequest[database.NotificationsSettings](rw, &audit.RequestParams{
+ Audit: *auditor,
+ Log: api.Logger,
+ Request: r,
+ Action: database.AuditActionWrite,
+ })
+ defer commitAudit()
+
+ aReq.New = database.NotificationsSettings{
+ ID: uuid.New(),
+ NotifierPaused: settings.NotifierPaused,
+ }
+
+ err = api.Database.UpsertNotificationsSettings(ctx, string(settingsJSON))
+ if err != nil {
+ httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
+ Message: "Failed to update notifications settings.",
+ Detail: err.Error(),
+ })
+ return
+ }
+
+ httpapi.Write(r.Context(), rw, http.StatusOK, settings)
+}
diff --git a/coderd/notifications/manager_test.go b/coderd/notifications/manager_test.go
index ee07913c5dc87..fe161cc2cd8f6 100644
--- a/coderd/notifications/manager_test.go
+++ b/coderd/notifications/manager_test.go
@@ -12,13 +12,14 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
+ "github.com/coder/serpent"
+
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/notifications"
"github.com/coder/coder/v2/coderd/notifications/dispatch"
"github.com/coder/coder/v2/coderd/notifications/types"
"github.com/coder/coder/v2/testutil"
- "github.com/coder/serpent"
)
func TestBufferedUpdates(t *testing.T) {
diff --git a/coderd/notifications/notifications_test.go b/coderd/notifications/notifications_test.go
index a8cdf4c96e8a9..c38daa1531ecb 100644
--- a/coderd/notifications/notifications_test.go
+++ b/coderd/notifications/notifications_test.go
@@ -538,6 +538,71 @@ func TestInvalidConfig(t *testing.T) {
require.ErrorIs(t, err, notifications.ErrInvalidDispatchTimeout)
}
+func TestNotifierPaused(t *testing.T) {
+ t.Parallel()
+
+ // setup
+ ctx, logger, db := setupInMemory(t)
+
+ // Prepare the test
+ handler := &fakeHandler{}
+ method := database.NotificationMethodSmtp
+ user := createSampleUser(t, db)
+
+ cfg := defaultNotificationsConfig(method)
+ fetchInterval := time.Nanosecond // Let
+ cfg.FetchInterval = *serpent.DurationOf(&fetchInterval)
+ mgr, err := notifications.NewManager(cfg, db, logger.Named("manager"))
+ require.NoError(t, err)
+ mgr.WithHandlers(map[database.NotificationMethod]notifications.Handler{method: handler})
+ t.Cleanup(func() {
+ assert.NoError(t, mgr.Stop(ctx))
+ })
+ enq, err := notifications.NewStoreEnqueuer(cfg, db, defaultHelpers(), logger.Named("enqueuer"))
+ require.NoError(t, err)
+
+ mgr.Run(ctx)
+
+ // Notifier is on, enqueue the first message.
+ sid, err := enq.Enqueue(ctx, user.ID, notifications.TemplateWorkspaceDeleted, map[string]string{"type": "success"}, "test")
+ require.NoError(t, err)
+ require.Eventually(t, func() bool {
+ handler.mu.RLock()
+ defer handler.mu.RUnlock()
+ return slices.Contains(handler.succeeded, sid.String())
+ }, testutil.WaitShort, testutil.IntervalFast)
+
+ // Pause the notifier.
+ settingsJSON, err := json.Marshal(&codersdk.NotificationsSettings{NotifierPaused: true})
+ require.NoError(t, err)
+ err = db.UpsertNotificationsSettings(ctx, string(settingsJSON))
+ require.NoError(t, err)
+
+ // Notifier is paused, enqueue the next message.
+ sid, err = enq.Enqueue(ctx, user.ID, notifications.TemplateWorkspaceDeleted, map[string]string{"type": "success"}, "test")
+ require.NoError(t, err)
+ require.Eventually(t, func() bool {
+ pendingMessages, err := db.GetNotificationMessagesByStatus(ctx, database.GetNotificationMessagesByStatusParams{
+ Status: database.NotificationMessageStatusPending,
+ })
+ assert.NoError(t, err)
+ return len(pendingMessages) == 1
+ }, testutil.WaitShort, testutil.IntervalFast)
+
+ // Unpause the notifier.
+ settingsJSON, err = json.Marshal(&codersdk.NotificationsSettings{NotifierPaused: false})
+ require.NoError(t, err)
+ err = db.UpsertNotificationsSettings(ctx, string(settingsJSON))
+ require.NoError(t, err)
+
+ // Notifier is running again, message should be dequeued.
+ require.Eventually(t, func() bool {
+ handler.mu.RLock()
+ defer handler.mu.RUnlock()
+ return slices.Contains(handler.succeeded, sid.String())
+ }, testutil.WaitShort, testutil.IntervalFast)
+}
+
type fakeHandler struct {
mu sync.RWMutex
@@ -546,7 +611,7 @@ type fakeHandler struct {
}
func (f *fakeHandler) Dispatcher(payload types.MessagePayload, _, _ string) (dispatch.DeliveryFunc, error) {
- return func(ctx context.Context, msgID uuid.UUID) (retryable bool, err error) {
+ return func(_ context.Context, msgID uuid.UUID) (retryable bool, err error) {
f.mu.Lock()
defer f.mu.Unlock()
diff --git a/coderd/notifications/notifier.go b/coderd/notifications/notifier.go
index b214f8a77a070..d400b52166b78 100644
--- a/coderd/notifications/notifier.go
+++ b/coderd/notifications/notifier.go
@@ -71,10 +71,18 @@ func (n *notifier) run(ctx context.Context, success chan<- dispatchResult, failu
default:
}
- // Call process() immediately (i.e. don't wait an initial tick).
- err := n.process(ctx, success, failure)
+ // Check if notifier is not paused.
+ ok, err := n.ensureRunning(ctx)
if err != nil {
- n.log.Error(ctx, "failed to process messages", slog.Error(err))
+ n.log.Warn(ctx, "failed to check notifier state", slog.Error(err))
+ }
+
+ if ok {
+ // Call process() immediately (i.e. don't wait an initial tick).
+ err = n.process(ctx, success, failure)
+ if err != nil {
+ n.log.Error(ctx, "failed to process messages", slog.Error(err))
+ }
}
// Shortcut to bail out quickly if stop() has been called or the context canceled.
@@ -89,6 +97,31 @@ func (n *notifier) run(ctx context.Context, success chan<- dispatchResult, failu
}
}
+// ensureRunning checks if notifier is not paused.
+func (n *notifier) ensureRunning(ctx context.Context) (bool, error) {
+ n.log.Debug(ctx, "check if notifier is paused")
+
+ settingsJSON, err := n.store.GetNotificationsSettings(ctx)
+ if err != nil {
+ return false, xerrors.Errorf("get notifications settings: %w", err)
+ }
+
+ var settings codersdk.NotificationsSettings
+ if len(settingsJSON) == 0 {
+ return true, nil // settings.NotifierPaused is false by default
+ }
+
+ err = json.Unmarshal([]byte(settingsJSON), &settings)
+ if err != nil {
+ return false, xerrors.Errorf("unmarshal notifications settings")
+ }
+
+ if settings.NotifierPaused {
+ n.log.Debug(ctx, "notifier is paused, notifications will not be delivered")
+ }
+ return !settings.NotifierPaused, nil
+}
+
// process is responsible for coordinating the retrieval, processing, and delivery of messages.
// Messages are dispatched concurrently, but they may block when success/failure channels are full.
//
diff --git a/coderd/notifications/spec.go b/coderd/notifications/spec.go
index 63f6af7101d1b..bba0d4e183c5c 100644
--- a/coderd/notifications/spec.go
+++ b/coderd/notifications/spec.go
@@ -21,6 +21,7 @@ type Store interface {
EnqueueNotificationMessage(ctx context.Context, arg database.EnqueueNotificationMessageParams) (database.NotificationMessage, error)
FetchNewMessageMetadata(ctx context.Context, arg database.FetchNewMessageMetadataParams) (database.FetchNewMessageMetadataRow, error)
GetNotificationMessagesByStatus(ctx context.Context, arg database.GetNotificationMessagesByStatusParams) ([]database.NotificationMessage, error)
+ GetNotificationsSettings(ctx context.Context) (string, error)
}
// Handler is responsible for preparing and delivering a notification by a given method.
diff --git a/coderd/notifications_test.go b/coderd/notifications_test.go
new file mode 100644
index 0000000000000..7690154a0db80
--- /dev/null
+++ b/coderd/notifications_test.go
@@ -0,0 +1,95 @@
+package coderd_test
+
+import (
+ "net/http"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/testutil"
+)
+
+func TestUpdateNotificationsSettings(t *testing.T) {
+ t.Parallel()
+
+ t.Run("Permissions denied", func(t *testing.T) {
+ t.Parallel()
+
+ api := coderdtest.New(t, nil)
+ firstUser := coderdtest.CreateFirstUser(t, api)
+ anotherClient, _ := coderdtest.CreateAnotherUser(t, api, firstUser.OrganizationID)
+
+ // given
+ expected := codersdk.NotificationsSettings{
+ NotifierPaused: true,
+ }
+
+ ctx := testutil.Context(t, testutil.WaitShort)
+
+ // when
+ err := anotherClient.PutNotificationsSettings(ctx, expected)
+
+ // then
+ var sdkError *codersdk.Error
+ require.Error(t, err)
+ require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error")
+ require.Equal(t, http.StatusForbidden, sdkError.StatusCode())
+ })
+
+ t.Run("Settings modified", func(t *testing.T) {
+ t.Parallel()
+
+ client := coderdtest.New(t, nil)
+ _ = coderdtest.CreateFirstUser(t, client)
+
+ // given
+ expected := codersdk.NotificationsSettings{
+ NotifierPaused: true,
+ }
+
+ ctx := testutil.Context(t, testutil.WaitShort)
+
+ // when
+ err := client.PutNotificationsSettings(ctx, expected)
+ require.NoError(t, err)
+
+ // then
+ actual, err := client.GetNotificationsSettings(ctx)
+ require.NoError(t, err)
+ require.Equal(t, expected, actual)
+ })
+
+ t.Run("Settings not modified", func(t *testing.T) {
+ t.Parallel()
+
+ // Empty state: notifications Settings are undefined now (default).
+ client := coderdtest.New(t, nil)
+ _ = coderdtest.CreateFirstUser(t, client)
+ ctx := testutil.Context(t, testutil.WaitShort)
+
+ // Change the state: pause notifications
+ err := client.PutNotificationsSettings(ctx, codersdk.NotificationsSettings{
+ NotifierPaused: true,
+ })
+ require.NoError(t, err)
+
+ // Verify the state: notifications are paused.
+ actual, err := client.GetNotificationsSettings(ctx)
+ require.NoError(t, err)
+ require.True(t, actual.NotifierPaused)
+
+ // Change the stage again: notifications are paused.
+ expected := actual
+ err = client.PutNotificationsSettings(ctx, codersdk.NotificationsSettings{
+ NotifierPaused: true,
+ })
+ require.NoError(t, err)
+
+ // Verify the state: notifications are still paused, and there is no error returned.
+ actual, err = client.GetNotificationsSettings(ctx)
+ require.NoError(t, err)
+ require.Equal(t, expected.NotifierPaused, actual.NotifierPaused)
+ })
+}
diff --git a/codersdk/audit.go b/codersdk/audit.go
index 683db5406c13f..75bfe6204c607 100644
--- a/codersdk/audit.go
+++ b/codersdk/audit.go
@@ -14,20 +14,21 @@ import (
type ResourceType string
const (
- ResourceTypeTemplate ResourceType = "template"
- ResourceTypeTemplateVersion ResourceType = "template_version"
- ResourceTypeUser ResourceType = "user"
- ResourceTypeWorkspace ResourceType = "workspace"
- ResourceTypeWorkspaceBuild ResourceType = "workspace_build"
- ResourceTypeGitSSHKey ResourceType = "git_ssh_key"
- ResourceTypeAPIKey ResourceType = "api_key"
- ResourceTypeGroup ResourceType = "group"
- ResourceTypeLicense ResourceType = "license"
- ResourceTypeConvertLogin ResourceType = "convert_login"
- ResourceTypeHealthSettings ResourceType = "health_settings"
- ResourceTypeWorkspaceProxy ResourceType = "workspace_proxy"
- ResourceTypeOrganization ResourceType = "organization"
- ResourceTypeOAuth2ProviderApp ResourceType = "oauth2_provider_app"
+ ResourceTypeTemplate ResourceType = "template"
+ ResourceTypeTemplateVersion ResourceType = "template_version"
+ ResourceTypeUser ResourceType = "user"
+ ResourceTypeWorkspace ResourceType = "workspace"
+ ResourceTypeWorkspaceBuild ResourceType = "workspace_build"
+ ResourceTypeGitSSHKey ResourceType = "git_ssh_key"
+ ResourceTypeAPIKey ResourceType = "api_key"
+ ResourceTypeGroup ResourceType = "group"
+ ResourceTypeLicense ResourceType = "license"
+ ResourceTypeConvertLogin ResourceType = "convert_login"
+ ResourceTypeHealthSettings ResourceType = "health_settings"
+ ResourceTypeNotificationsSettings ResourceType = "notifications_settings"
+ ResourceTypeWorkspaceProxy ResourceType = "workspace_proxy"
+ ResourceTypeOrganization ResourceType = "organization"
+ ResourceTypeOAuth2ProviderApp ResourceType = "oauth2_provider_app"
// nolint:gosec // This is not a secret.
ResourceTypeOAuth2ProviderAppSecret ResourceType = "oauth2_provider_app_secret"
ResourceTypeCustomRole ResourceType = "custom_role"
@@ -64,6 +65,8 @@ func (r ResourceType) FriendlyString() string {
return "organization"
case ResourceTypeHealthSettings:
return "health_settings"
+ case ResourceTypeNotificationsSettings:
+ return "notifications_settings"
case ResourceTypeOAuth2ProviderApp:
return "oauth2 app"
case ResourceTypeOAuth2ProviderAppSecret:
diff --git a/codersdk/notifications.go b/codersdk/notifications.go
new file mode 100644
index 0000000000000..58829eed57891
--- /dev/null
+++ b/codersdk/notifications.go
@@ -0,0 +1,40 @@
+package codersdk
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+)
+
+type NotificationsSettings struct {
+ NotifierPaused bool `json:"notifier_paused"`
+}
+
+func (c *Client) GetNotificationsSettings(ctx context.Context) (NotificationsSettings, error) {
+ res, err := c.Request(ctx, http.MethodGet, "/api/v2/notifications/settings", nil)
+ if err != nil {
+ return NotificationsSettings{}, err
+ }
+ defer res.Body.Close()
+ if res.StatusCode != http.StatusOK {
+ return NotificationsSettings{}, ReadBodyAsError(res)
+ }
+ var settings NotificationsSettings
+ return settings, json.NewDecoder(res.Body).Decode(&settings)
+}
+
+func (c *Client) PutNotificationsSettings(ctx context.Context, settings NotificationsSettings) error {
+ res, err := c.Request(ctx, http.MethodPut, "/api/v2/notifications/settings", settings)
+ if err != nil {
+ return err
+ }
+ defer res.Body.Close()
+
+ if res.StatusCode == http.StatusNotModified {
+ return nil
+ }
+ if res.StatusCode != http.StatusOK {
+ return ReadBodyAsError(res)
+ }
+ return nil
+}
diff --git a/docs/admin/audit-logs.md b/docs/admin/audit-logs.md
index 5f34e6bf475c4..f239589f9700a 100644
--- a/docs/admin/audit-logs.md
+++ b/docs/admin/audit-logs.md
@@ -18,6 +18,7 @@ We track the following resources:
| GitSSHKey
create |
Field | Tracked |
---|
created_at | false |
private_key | true |
public_key | true |
updated_at | false |
user_id | true |
|
| HealthSettings
| Field | Tracked |
---|
dismissed_healthchecks | true |
id | false |
|
| License
create, delete | Field | Tracked |
---|
exp | true |
id | false |
jwt | false |
uploaded_at | true |
uuid | true |
|
+| NotificationsSettings
| Field | Tracked |
---|
id | false |
notifier_paused | true |
|
| OAuth2ProviderApp
| Field | Tracked |
---|
callback_url | true |
created_at | false |
icon | true |
id | false |
name | true |
updated_at | false |
|
| OAuth2ProviderAppSecret
| Field | Tracked |
---|
app_id | false |
created_at | false |
display_secret | false |
hashed_secret | false |
id | false |
last_used_at | false |
secret_prefix | false |
|
| Organization
| Field | Tracked |
---|
created_at | false |
description | true |
display_name | true |
icon | true |
id | false |
is_default | true |
name | true |
updated_at | true |
|
diff --git a/docs/api/general.md b/docs/api/general.md
index 8bd968c6b18ed..c628604b92123 100644
--- a/docs/api/general.md
+++ b/docs/api/general.md
@@ -651,6 +651,84 @@ Status Code **200**
To perform this operation, you must be authenticated. [Learn more](authentication.md).
+## Get notifications settings
+
+### Code samples
+
+```shell
+# Example request using curl
+curl -X GET http://coder-server:8080/api/v2/notifications/settings \
+ -H 'Accept: application/json' \
+ -H 'Coder-Session-Token: API_KEY'
+```
+
+`GET /notifications/settings`
+
+### Example responses
+
+> 200 Response
+
+```json
+{
+ "notifier_paused": true
+}
+```
+
+### Responses
+
+| Status | Meaning | Description | Schema |
+| ------ | ------------------------------------------------------- | ----------- | -------------------------------------------------------------------------- |
+| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.NotificationsSettings](schemas.md#codersdknotificationssettings) |
+
+To perform this operation, you must be authenticated. [Learn more](authentication.md).
+
+## Update notifications settings
+
+### Code samples
+
+```shell
+# Example request using curl
+curl -X PUT http://coder-server:8080/api/v2/notifications/settings \
+ -H 'Content-Type: application/json' \
+ -H 'Accept: application/json' \
+ -H 'Coder-Session-Token: API_KEY'
+```
+
+`PUT /notifications/settings`
+
+> Body parameter
+
+```json
+{
+ "notifier_paused": true
+}
+```
+
+### Parameters
+
+| Name | In | Type | Required | Description |
+| ------ | ---- | -------------------------------------------------------------------------- | -------- | ------------------------------ |
+| `body` | body | [codersdk.NotificationsSettings](schemas.md#codersdknotificationssettings) | true | Notifications settings request |
+
+### Example responses
+
+> 200 Response
+
+```json
+{
+ "notifier_paused": true
+}
+```
+
+### Responses
+
+| Status | Meaning | Description | Schema |
+| ------ | --------------------------------------------------------------- | ------------ | -------------------------------------------------------------------------- |
+| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.NotificationsSettings](schemas.md#codersdknotificationssettings) |
+| 304 | [Not Modified](https://tools.ietf.org/html/rfc7232#section-4.1) | Not Modified | |
+
+To perform this operation, you must be authenticated. [Learn more](authentication.md).
+
## Update check
### Code samples
diff --git a/docs/api/schemas.md b/docs/api/schemas.md
index 5e2eaf7b74784..fd0d4c87437d4 100644
--- a/docs/api/schemas.md
+++ b/docs/api/schemas.md
@@ -3122,6 +3122,20 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
| `hello` | string | false | | The hostname identifying the SMTP server. |
| `smarthost` | [serpent.HostPort](#serpenthostport) | false | | The intermediary SMTP host through which emails are sent (host:port). |
+## codersdk.NotificationsSettings
+
+```json
+{
+ "notifier_paused": true
+}
+```
+
+### Properties
+
+| Name | Type | Required | Restrictions | Description |
+| ----------------- | ------- | -------- | ------------ | ----------- |
+| `notifier_paused` | boolean | false | | |
+
## codersdk.NotificationsWebhookConfig
```json
@@ -4157,6 +4171,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
| `license` |
| `convert_login` |
| `health_settings` |
+| `notifications_settings` |
| `workspace_proxy` |
| `organization` |
| `oauth2_provider_app` |
diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go
index ed52b5e921560..f2ec06701904d 100644
--- a/enterprise/audit/table.go
+++ b/enterprise/audit/table.go
@@ -213,6 +213,10 @@ var auditableResourcesTypes = map[any]map[string]Action{
"id": ActionIgnore,
"dismissed_healthchecks": ActionTrack,
},
+ &database.NotificationsSettings{}: {
+ "id": ActionIgnore,
+ "notifier_paused": ActionTrack,
+ },
// TODO: track an ID here when the below ticket is completed:
// https://github.com/coder/coder/pull/6012
&database.License{}: {
diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts
index ad142b41392d0..6a3ce9adaae82 100644
--- a/site/src/api/typesGenerated.ts
+++ b/site/src/api/typesGenerated.ts
@@ -709,6 +709,11 @@ export interface NotificationsEmailConfig {
readonly hello: string;
}
+// From codersdk/notifications.go
+export interface NotificationsSettings {
+ readonly notifier_paused: boolean;
+}
+
// From codersdk/deployment.go
export interface NotificationsWebhookConfig {
readonly endpoint: string;
@@ -2242,6 +2247,7 @@ export type ResourceType =
| "group"
| "health_settings"
| "license"
+ | "notifications_settings"
| "oauth2_provider_app"
| "oauth2_provider_app_secret"
| "organization"
@@ -2259,6 +2265,7 @@ export const ResourceTypes: ResourceType[] = [
"group",
"health_settings",
"license",
+ "notifications_settings",
"oauth2_provider_app",
"oauth2_provider_app_secret",
"organization",
pFad - Phonifier reborn
Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy