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",