mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: add notification preferences database & audit support (#14100)
This commit is contained in:
@@ -16,6 +16,16 @@ import (
|
|||||||
"github.com/coder/coder/v2/testutil"
|
"github.com/coder/coder/v2/testutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func createOpts(t *testing.T) *coderdtest.Options {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
dt := coderdtest.DeploymentValues(t)
|
||||||
|
dt.Experiments = []string{string(codersdk.ExperimentNotifications)}
|
||||||
|
return &coderdtest.Options{
|
||||||
|
DeploymentValues: dt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNotifications(t *testing.T) {
|
func TestNotifications(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -42,7 +52,7 @@ func TestNotifications(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
// given
|
// given
|
||||||
ownerClient, db := coderdtest.NewWithDatabase(t, nil)
|
ownerClient, db := coderdtest.NewWithDatabase(t, createOpts(t))
|
||||||
_ = coderdtest.CreateFirstUser(t, ownerClient)
|
_ = coderdtest.CreateFirstUser(t, ownerClient)
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@@ -72,7 +82,7 @@ func TestPauseNotifications_RegularUser(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
// given
|
// given
|
||||||
ownerClient, db := coderdtest.NewWithDatabase(t, nil)
|
ownerClient, db := coderdtest.NewWithDatabase(t, createOpts(t))
|
||||||
owner := coderdtest.CreateFirstUser(t, ownerClient)
|
owner := coderdtest.CreateFirstUser(t, ownerClient)
|
||||||
anotherClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
|
anotherClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
|
||||||
|
|
||||||
@@ -87,7 +97,7 @@ func TestPauseNotifications_RegularUser(t *testing.T) {
|
|||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error")
|
require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error")
|
||||||
assert.Equal(t, http.StatusForbidden, sdkError.StatusCode())
|
assert.Equal(t, http.StatusForbidden, sdkError.StatusCode())
|
||||||
assert.Contains(t, sdkError.Message, "Insufficient permissions to update notifications settings.")
|
assert.Contains(t, sdkError.Message, "Forbidden.")
|
||||||
|
|
||||||
// then
|
// then
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||||
|
|||||||
Generated
+251
-2
@@ -1547,6 +1547,34 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/notifications/dispatch-methods": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Notifications"
|
||||||
|
],
|
||||||
|
"summary": "Get notification dispatch methods",
|
||||||
|
"operationId": "get-notification-dispatch-methods",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/codersdk.NotificationMethodsResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/notifications/settings": {
|
"/notifications/settings": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -1558,7 +1586,7 @@ const docTemplate = `{
|
|||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"General"
|
"Notifications"
|
||||||
],
|
],
|
||||||
"summary": "Get notifications settings",
|
"summary": "Get notifications settings",
|
||||||
"operationId": "get-notifications-settings",
|
"operationId": "get-notifications-settings",
|
||||||
@@ -1584,7 +1612,7 @@ const docTemplate = `{
|
|||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"General"
|
"Notifications"
|
||||||
],
|
],
|
||||||
"summary": "Update notifications settings",
|
"summary": "Update notifications settings",
|
||||||
"operationId": "update-notifications-settings",
|
"operationId": "update-notifications-settings",
|
||||||
@@ -1612,6 +1640,68 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/notifications/templates/system": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Notifications"
|
||||||
|
],
|
||||||
|
"summary": "Get system notification templates",
|
||||||
|
"operationId": "get-system-notification-templates",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/codersdk.NotificationTemplate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/notifications/templates/{notification_template}/method": {
|
||||||
|
"put": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Enterprise"
|
||||||
|
],
|
||||||
|
"summary": "Update notification template dispatch method",
|
||||||
|
"operationId": "update-notification-template-dispatch-method",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Notification template UUID",
|
||||||
|
"name": "notification_template",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Success"
|
||||||
|
},
|
||||||
|
"304": {
|
||||||
|
"description": "Not modified"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/oauth2-provider/apps": {
|
"/oauth2-provider/apps": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -5354,6 +5444,90 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/users/{user}/notifications/preferences": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Notifications"
|
||||||
|
],
|
||||||
|
"summary": "Get user notification preferences",
|
||||||
|
"operationId": "get-user-notification-preferences",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "User ID, name, or me",
|
||||||
|
"name": "user",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/codersdk.NotificationPreference"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Notifications"
|
||||||
|
],
|
||||||
|
"summary": "Update user notification preferences",
|
||||||
|
"operationId": "update-user-notification-preferences",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Preferences",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.UpdateUserNotificationPreferences"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "User ID, name, or me",
|
||||||
|
"name": "user",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/codersdk.NotificationPreference"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/users/{user}/organizations": {
|
"/users/{user}/organizations": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -10202,6 +10376,66 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"codersdk.NotificationMethodsResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"available": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"codersdk.NotificationPreference": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"disabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"codersdk.NotificationTemplate": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"actions": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"body_template": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"group": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"kind": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"method": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"title_template": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"codersdk.NotificationsConfig": {
|
"codersdk.NotificationsConfig": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -11217,6 +11451,8 @@ const docTemplate = `{
|
|||||||
"file",
|
"file",
|
||||||
"group",
|
"group",
|
||||||
"license",
|
"license",
|
||||||
|
"notification_preference",
|
||||||
|
"notification_template",
|
||||||
"oauth2_app",
|
"oauth2_app",
|
||||||
"oauth2_app_code_token",
|
"oauth2_app_code_token",
|
||||||
"oauth2_app_secret",
|
"oauth2_app_secret",
|
||||||
@@ -11245,6 +11481,8 @@ const docTemplate = `{
|
|||||||
"ResourceFile",
|
"ResourceFile",
|
||||||
"ResourceGroup",
|
"ResourceGroup",
|
||||||
"ResourceLicense",
|
"ResourceLicense",
|
||||||
|
"ResourceNotificationPreference",
|
||||||
|
"ResourceNotificationTemplate",
|
||||||
"ResourceOauth2App",
|
"ResourceOauth2App",
|
||||||
"ResourceOauth2AppCodeToken",
|
"ResourceOauth2AppCodeToken",
|
||||||
"ResourceOauth2AppSecret",
|
"ResourceOauth2AppSecret",
|
||||||
@@ -12513,6 +12751,17 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"codersdk.UpdateUserNotificationPreferences": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"template_disabled_map": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"codersdk.UpdateUserPasswordRequest": {
|
"codersdk.UpdateUserPasswordRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|||||||
Generated
+229
-2
@@ -1344,6 +1344,30 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/notifications/dispatch-methods": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"produces": ["application/json"],
|
||||||
|
"tags": ["Notifications"],
|
||||||
|
"summary": "Get notification dispatch methods",
|
||||||
|
"operationId": "get-notification-dispatch-methods",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/codersdk.NotificationMethodsResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/notifications/settings": {
|
"/notifications/settings": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -1352,7 +1376,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"produces": ["application/json"],
|
"produces": ["application/json"],
|
||||||
"tags": ["General"],
|
"tags": ["Notifications"],
|
||||||
"summary": "Get notifications settings",
|
"summary": "Get notifications settings",
|
||||||
"operationId": "get-notifications-settings",
|
"operationId": "get-notifications-settings",
|
||||||
"responses": {
|
"responses": {
|
||||||
@@ -1372,7 +1396,7 @@
|
|||||||
],
|
],
|
||||||
"consumes": ["application/json"],
|
"consumes": ["application/json"],
|
||||||
"produces": ["application/json"],
|
"produces": ["application/json"],
|
||||||
"tags": ["General"],
|
"tags": ["Notifications"],
|
||||||
"summary": "Update notifications settings",
|
"summary": "Update notifications settings",
|
||||||
"operationId": "update-notifications-settings",
|
"operationId": "update-notifications-settings",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
@@ -1399,6 +1423,60 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/notifications/templates/system": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"produces": ["application/json"],
|
||||||
|
"tags": ["Notifications"],
|
||||||
|
"summary": "Get system notification templates",
|
||||||
|
"operationId": "get-system-notification-templates",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/codersdk.NotificationTemplate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/notifications/templates/{notification_template}/method": {
|
||||||
|
"put": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"produces": ["application/json"],
|
||||||
|
"tags": ["Enterprise"],
|
||||||
|
"summary": "Update notification template dispatch method",
|
||||||
|
"operationId": "update-notification-template-dispatch-method",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Notification template UUID",
|
||||||
|
"name": "notification_template",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Success"
|
||||||
|
},
|
||||||
|
"304": {
|
||||||
|
"description": "Not modified"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/oauth2-provider/apps": {
|
"/oauth2-provider/apps": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -4726,6 +4804,80 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/users/{user}/notifications/preferences": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"produces": ["application/json"],
|
||||||
|
"tags": ["Notifications"],
|
||||||
|
"summary": "Get user notification preferences",
|
||||||
|
"operationId": "get-user-notification-preferences",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "User ID, name, or me",
|
||||||
|
"name": "user",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/codersdk.NotificationPreference"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumes": ["application/json"],
|
||||||
|
"produces": ["application/json"],
|
||||||
|
"tags": ["Notifications"],
|
||||||
|
"summary": "Update user notification preferences",
|
||||||
|
"operationId": "update-user-notification-preferences",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Preferences",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.UpdateUserNotificationPreferences"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "User ID, name, or me",
|
||||||
|
"name": "user",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/codersdk.NotificationPreference"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/users/{user}/organizations": {
|
"/users/{user}/organizations": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -9143,6 +9295,66 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"codersdk.NotificationMethodsResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"available": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"codersdk.NotificationPreference": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"disabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"codersdk.NotificationTemplate": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"actions": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"body_template": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"group": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"kind": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"method": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"title_template": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"codersdk.NotificationsConfig": {
|
"codersdk.NotificationsConfig": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -10119,6 +10331,8 @@
|
|||||||
"file",
|
"file",
|
||||||
"group",
|
"group",
|
||||||
"license",
|
"license",
|
||||||
|
"notification_preference",
|
||||||
|
"notification_template",
|
||||||
"oauth2_app",
|
"oauth2_app",
|
||||||
"oauth2_app_code_token",
|
"oauth2_app_code_token",
|
||||||
"oauth2_app_secret",
|
"oauth2_app_secret",
|
||||||
@@ -10147,6 +10361,8 @@
|
|||||||
"ResourceFile",
|
"ResourceFile",
|
||||||
"ResourceGroup",
|
"ResourceGroup",
|
||||||
"ResourceLicense",
|
"ResourceLicense",
|
||||||
|
"ResourceNotificationPreference",
|
||||||
|
"ResourceNotificationTemplate",
|
||||||
"ResourceOauth2App",
|
"ResourceOauth2App",
|
||||||
"ResourceOauth2AppCodeToken",
|
"ResourceOauth2AppCodeToken",
|
||||||
"ResourceOauth2AppSecret",
|
"ResourceOauth2AppSecret",
|
||||||
@@ -11362,6 +11578,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"codersdk.UpdateUserNotificationPreferences": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"template_disabled_map": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"codersdk.UpdateUserPasswordRequest": {
|
"codersdk.UpdateUserPasswordRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["password"],
|
"required": ["password"],
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ type Auditable interface {
|
|||||||
database.OAuth2ProviderAppSecret |
|
database.OAuth2ProviderAppSecret |
|
||||||
database.CustomRole |
|
database.CustomRole |
|
||||||
database.AuditableOrganizationMember |
|
database.AuditableOrganizationMember |
|
||||||
database.Organization
|
database.Organization |
|
||||||
|
database.NotificationTemplate
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map is a map of changed fields in an audited resource. It maps field names to
|
// Map is a map of changed fields in an audited resource. It maps field names to
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
"cdr.dev/slog"
|
"cdr.dev/slog"
|
||||||
|
|
||||||
"github.com/coder/coder/v2/coderd/database"
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||||
"github.com/coder/coder/v2/coderd/httpmw"
|
"github.com/coder/coder/v2/coderd/httpmw"
|
||||||
@@ -117,6 +118,8 @@ func ResourceTarget[T Auditable](tgt T) string {
|
|||||||
return typed.Username
|
return typed.Username
|
||||||
case database.Organization:
|
case database.Organization:
|
||||||
return typed.Name
|
return typed.Name
|
||||||
|
case database.NotificationTemplate:
|
||||||
|
return typed.Name
|
||||||
default:
|
default:
|
||||||
panic(fmt.Sprintf("unknown resource %T for ResourceTarget", tgt))
|
panic(fmt.Sprintf("unknown resource %T for ResourceTarget", tgt))
|
||||||
}
|
}
|
||||||
@@ -163,6 +166,8 @@ func ResourceID[T Auditable](tgt T) uuid.UUID {
|
|||||||
return typed.UserID
|
return typed.UserID
|
||||||
case database.Organization:
|
case database.Organization:
|
||||||
return typed.ID
|
return typed.ID
|
||||||
|
case database.NotificationTemplate:
|
||||||
|
return typed.ID
|
||||||
default:
|
default:
|
||||||
panic(fmt.Sprintf("unknown resource %T for ResourceID", tgt))
|
panic(fmt.Sprintf("unknown resource %T for ResourceID", tgt))
|
||||||
}
|
}
|
||||||
@@ -206,6 +211,8 @@ func ResourceType[T Auditable](tgt T) database.ResourceType {
|
|||||||
return database.ResourceTypeOrganizationMember
|
return database.ResourceTypeOrganizationMember
|
||||||
case database.Organization:
|
case database.Organization:
|
||||||
return database.ResourceTypeOrganization
|
return database.ResourceTypeOrganization
|
||||||
|
case database.NotificationTemplate:
|
||||||
|
return database.ResourceTypeNotificationTemplate
|
||||||
default:
|
default:
|
||||||
panic(fmt.Sprintf("unknown resource %T for ResourceType", typed))
|
panic(fmt.Sprintf("unknown resource %T for ResourceType", typed))
|
||||||
}
|
}
|
||||||
@@ -251,6 +258,8 @@ func ResourceRequiresOrgID[T Auditable]() bool {
|
|||||||
return true
|
return true
|
||||||
case database.Organization:
|
case database.Organization:
|
||||||
return true
|
return true
|
||||||
|
case database.NotificationTemplate:
|
||||||
|
return false
|
||||||
default:
|
default:
|
||||||
panic(fmt.Sprintf("unknown resource %T for ResourceRequiresOrgID", tgt))
|
panic(fmt.Sprintf("unknown resource %T for ResourceRequiresOrgID", tgt))
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-1
@@ -1050,6 +1050,12 @@ func New(options *Options) *API {
|
|||||||
})
|
})
|
||||||
r.Get("/gitsshkey", api.gitSSHKey)
|
r.Get("/gitsshkey", api.gitSSHKey)
|
||||||
r.Put("/gitsshkey", api.regenerateGitSSHKey)
|
r.Put("/gitsshkey", api.regenerateGitSSHKey)
|
||||||
|
r.Route("/notifications", func(r chi.Router) {
|
||||||
|
r.Route("/preferences", func(r chi.Router) {
|
||||||
|
r.Get("/", api.userNotificationPreferences)
|
||||||
|
r.Put("/", api.putUserNotificationPreferences)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -1243,9 +1249,16 @@ func New(options *Options) *API {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
r.Route("/notifications", func(r chi.Router) {
|
r.Route("/notifications", func(r chi.Router) {
|
||||||
r.Use(apiKeyMiddleware)
|
r.Use(
|
||||||
|
apiKeyMiddleware,
|
||||||
|
httpmw.RequireExperiment(api.Experiments, codersdk.ExperimentNotifications),
|
||||||
|
)
|
||||||
r.Get("/settings", api.notificationsSettings)
|
r.Get("/settings", api.notificationsSettings)
|
||||||
r.Put("/settings", api.putNotificationsSettings)
|
r.Put("/settings", api.putNotificationsSettings)
|
||||||
|
r.Route("/templates", func(r chi.Router) {
|
||||||
|
r.Get("/system", api.systemNotificationTemplates)
|
||||||
|
})
|
||||||
|
r.Get("/dispatch-methods", api.notificationDispatchMethods)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1474,6 +1474,23 @@ func (q *querier) GetNotificationMessagesByStatus(ctx context.Context, arg datab
|
|||||||
return q.db.GetNotificationMessagesByStatus(ctx, arg)
|
return q.db.GetNotificationMessagesByStatus(ctx, arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (q *querier) GetNotificationTemplateByID(ctx context.Context, id uuid.UUID) (database.NotificationTemplate, error) {
|
||||||
|
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceNotificationTemplate); err != nil {
|
||||||
|
return database.NotificationTemplate{}, err
|
||||||
|
}
|
||||||
|
return q.db.GetNotificationTemplateByID(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *querier) GetNotificationTemplatesByKind(ctx context.Context, kind database.NotificationTemplateKind) ([]database.NotificationTemplate, error) {
|
||||||
|
// TODO: restrict 'system' kind to admins only?
|
||||||
|
// All notification templates share the same rbac.Object, so there is no need
|
||||||
|
// to authorize them individually. If this passes, all notification templates can be read.
|
||||||
|
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceNotificationTemplate); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return q.db.GetNotificationTemplatesByKind(ctx, kind)
|
||||||
|
}
|
||||||
|
|
||||||
func (q *querier) GetNotificationsSettings(ctx context.Context) (string, error) {
|
func (q *querier) GetNotificationsSettings(ctx context.Context) (string, error) {
|
||||||
// No authz checks
|
// No authz checks
|
||||||
return q.db.GetNotificationsSettings(ctx)
|
return q.db.GetNotificationsSettings(ctx)
|
||||||
@@ -2085,6 +2102,13 @@ func (q *querier) GetUserLinksByUserID(ctx context.Context, userID uuid.UUID) ([
|
|||||||
return q.db.GetUserLinksByUserID(ctx, userID)
|
return q.db.GetUserLinksByUserID(ctx, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (q *querier) GetUserNotificationPreferences(ctx context.Context, userID uuid.UUID) ([]database.NotificationPreference, error) {
|
||||||
|
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceNotificationPreference.WithOwner(userID.String())); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return q.db.GetUserNotificationPreferences(ctx, userID)
|
||||||
|
}
|
||||||
|
|
||||||
func (q *querier) GetUserWorkspaceBuildParameters(ctx context.Context, params database.GetUserWorkspaceBuildParametersParams) ([]database.GetUserWorkspaceBuildParametersRow, error) {
|
func (q *querier) GetUserWorkspaceBuildParameters(ctx context.Context, params database.GetUserWorkspaceBuildParametersParams) ([]database.GetUserWorkspaceBuildParametersRow, error) {
|
||||||
u, err := q.db.GetUserByID(ctx, params.OwnerID)
|
u, err := q.db.GetUserByID(ctx, params.OwnerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -3011,6 +3035,13 @@ func (q *querier) UpdateMemberRoles(ctx context.Context, arg database.UpdateMemb
|
|||||||
return q.db.UpdateMemberRoles(ctx, arg)
|
return q.db.UpdateMemberRoles(ctx, arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (q *querier) UpdateNotificationTemplateMethodByID(ctx context.Context, arg database.UpdateNotificationTemplateMethodByIDParams) (database.NotificationTemplate, error) {
|
||||||
|
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceNotificationTemplate); err != nil {
|
||||||
|
return database.NotificationTemplate{}, err
|
||||||
|
}
|
||||||
|
return q.db.UpdateNotificationTemplateMethodByID(ctx, arg)
|
||||||
|
}
|
||||||
|
|
||||||
func (q *querier) UpdateOAuth2ProviderAppByID(ctx context.Context, arg database.UpdateOAuth2ProviderAppByIDParams) (database.OAuth2ProviderApp, error) {
|
func (q *querier) UpdateOAuth2ProviderAppByID(ctx context.Context, arg database.UpdateOAuth2ProviderAppByIDParams) (database.OAuth2ProviderApp, error) {
|
||||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceOauth2App); err != nil {
|
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceOauth2App); err != nil {
|
||||||
return database.OAuth2ProviderApp{}, err
|
return database.OAuth2ProviderApp{}, err
|
||||||
@@ -3326,6 +3357,13 @@ func (q *querier) UpdateUserLoginType(ctx context.Context, arg database.UpdateUs
|
|||||||
return q.db.UpdateUserLoginType(ctx, arg)
|
return q.db.UpdateUserLoginType(ctx, arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (q *querier) UpdateUserNotificationPreferences(ctx context.Context, arg database.UpdateUserNotificationPreferencesParams) (int64, error) {
|
||||||
|
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceNotificationPreference.WithOwner(arg.UserID.String())); err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
return q.db.UpdateUserNotificationPreferences(ctx, arg)
|
||||||
|
}
|
||||||
|
|
||||||
func (q *querier) UpdateUserProfile(ctx context.Context, arg database.UpdateUserProfileParams) (database.User, error) {
|
func (q *querier) UpdateUserProfile(ctx context.Context, arg database.UpdateUserProfileParams) (database.User, error) {
|
||||||
u, err := q.db.GetUserByID(ctx, arg.ID)
|
u, err := q.db.GetUserByID(ctx, arg.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"cdr.dev/slog"
|
"cdr.dev/slog"
|
||||||
|
|
||||||
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
||||||
|
"github.com/coder/coder/v2/coderd/notifications"
|
||||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||||
"github.com/coder/coder/v2/codersdk"
|
"github.com/coder/coder/v2/codersdk"
|
||||||
|
|
||||||
@@ -2561,6 +2562,10 @@ func (s *MethodTestSuite) TestSystemFunctions() {
|
|||||||
AgentID: uuid.New(),
|
AgentID: uuid.New(),
|
||||||
}).Asserts(tpl, policy.ActionCreate)
|
}).Asserts(tpl, policy.ActionCreate)
|
||||||
}))
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MethodTestSuite) TestNotifications() {
|
||||||
|
// System functions
|
||||||
s.Run("AcquireNotificationMessages", s.Subtest(func(db database.Store, check *expects) {
|
s.Run("AcquireNotificationMessages", s.Subtest(func(db database.Store, check *expects) {
|
||||||
// TODO: update this test once we have a specific role for notifications
|
// TODO: update this test once we have a specific role for notifications
|
||||||
check.Args(database.AcquireNotificationMessagesParams{}).Asserts(rbac.ResourceSystem, policy.ActionUpdate)
|
check.Args(database.AcquireNotificationMessagesParams{}).Asserts(rbac.ResourceSystem, policy.ActionUpdate)
|
||||||
@@ -2596,6 +2601,40 @@ func (s *MethodTestSuite) TestSystemFunctions() {
|
|||||||
Limit: 10,
|
Limit: 10,
|
||||||
}).Asserts(rbac.ResourceSystem, policy.ActionRead)
|
}).Asserts(rbac.ResourceSystem, policy.ActionRead)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// Notification templates
|
||||||
|
s.Run("GetNotificationTemplateByID", s.Subtest(func(db database.Store, check *expects) {
|
||||||
|
user := dbgen.User(s.T(), db, database.User{})
|
||||||
|
check.Args(user.ID).Asserts(rbac.ResourceNotificationTemplate, policy.ActionRead).
|
||||||
|
Errors(dbmem.ErrUnimplemented)
|
||||||
|
}))
|
||||||
|
s.Run("GetNotificationTemplatesByKind", s.Subtest(func(db database.Store, check *expects) {
|
||||||
|
check.Args(database.NotificationTemplateKindSystem).
|
||||||
|
Asserts(rbac.ResourceNotificationTemplate, policy.ActionRead).
|
||||||
|
Errors(dbmem.ErrUnimplemented)
|
||||||
|
}))
|
||||||
|
s.Run("UpdateNotificationTemplateMethodByID", s.Subtest(func(db database.Store, check *expects) {
|
||||||
|
check.Args(database.UpdateNotificationTemplateMethodByIDParams{
|
||||||
|
Method: database.NullNotificationMethod{NotificationMethod: database.NotificationMethodWebhook, Valid: true},
|
||||||
|
ID: notifications.TemplateWorkspaceDormant,
|
||||||
|
}).Asserts(rbac.ResourceNotificationTemplate, policy.ActionUpdate).
|
||||||
|
Errors(dbmem.ErrUnimplemented)
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Notification preferences
|
||||||
|
s.Run("GetUserNotificationPreferences", s.Subtest(func(db database.Store, check *expects) {
|
||||||
|
user := dbgen.User(s.T(), db, database.User{})
|
||||||
|
check.Args(user.ID).
|
||||||
|
Asserts(rbac.ResourceNotificationPreference.WithOwner(user.ID.String()), policy.ActionRead)
|
||||||
|
}))
|
||||||
|
s.Run("UpdateUserNotificationPreferences", s.Subtest(func(db database.Store, check *expects) {
|
||||||
|
user := dbgen.User(s.T(), db, database.User{})
|
||||||
|
check.Args(database.UpdateUserNotificationPreferencesParams{
|
||||||
|
UserID: user.ID,
|
||||||
|
NotificationTemplateIds: []uuid.UUID{notifications.TemplateWorkspaceAutoUpdated, notifications.TemplateWorkspaceDeleted},
|
||||||
|
Disableds: []bool{true, false},
|
||||||
|
}).Asserts(rbac.ResourceNotificationPreference.WithOwner(user.ID.String()), policy.ActionUpdate)
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MethodTestSuite) TestOAuth2ProviderApps() {
|
func (s *MethodTestSuite) TestOAuth2ProviderApps() {
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ func New() database.Store {
|
|||||||
files: make([]database.File, 0),
|
files: make([]database.File, 0),
|
||||||
gitSSHKey: make([]database.GitSSHKey, 0),
|
gitSSHKey: make([]database.GitSSHKey, 0),
|
||||||
notificationMessages: make([]database.NotificationMessage, 0),
|
notificationMessages: make([]database.NotificationMessage, 0),
|
||||||
|
notificationPreferences: make([]database.NotificationPreference, 0),
|
||||||
parameterSchemas: make([]database.ParameterSchema, 0),
|
parameterSchemas: make([]database.ParameterSchema, 0),
|
||||||
provisionerDaemons: make([]database.ProvisionerDaemon, 0),
|
provisionerDaemons: make([]database.ProvisionerDaemon, 0),
|
||||||
workspaceAgents: make([]database.WorkspaceAgent, 0),
|
workspaceAgents: make([]database.WorkspaceAgent, 0),
|
||||||
@@ -160,6 +161,7 @@ type data struct {
|
|||||||
jfrogXRayScans []database.JfrogXrayScan
|
jfrogXRayScans []database.JfrogXrayScan
|
||||||
licenses []database.License
|
licenses []database.License
|
||||||
notificationMessages []database.NotificationMessage
|
notificationMessages []database.NotificationMessage
|
||||||
|
notificationPreferences []database.NotificationPreference
|
||||||
oauth2ProviderApps []database.OAuth2ProviderApp
|
oauth2ProviderApps []database.OAuth2ProviderApp
|
||||||
oauth2ProviderAppSecrets []database.OAuth2ProviderAppSecret
|
oauth2ProviderAppSecrets []database.OAuth2ProviderAppSecret
|
||||||
oauth2ProviderAppCodes []database.OAuth2ProviderAppCode
|
oauth2ProviderAppCodes []database.OAuth2ProviderAppCode
|
||||||
@@ -2708,6 +2710,18 @@ func (q *FakeQuerier) GetNotificationMessagesByStatus(_ context.Context, arg dat
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (*FakeQuerier) GetNotificationTemplateByID(_ context.Context, _ uuid.UUID) (database.NotificationTemplate, error) {
|
||||||
|
// Not implementing this function because it relies on state in the database which is created with migrations.
|
||||||
|
// We could consider using code-generation to align the database state and dbmem, but it's not worth it right now.
|
||||||
|
return database.NotificationTemplate{}, ErrUnimplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*FakeQuerier) GetNotificationTemplatesByKind(_ context.Context, _ database.NotificationTemplateKind) ([]database.NotificationTemplate, error) {
|
||||||
|
// Not implementing this function because it relies on state in the database which is created with migrations.
|
||||||
|
// We could consider using code-generation to align the database state and dbmem, but it's not worth it right now.
|
||||||
|
return nil, ErrUnimplemented
|
||||||
|
}
|
||||||
|
|
||||||
func (q *FakeQuerier) GetNotificationsSettings(_ context.Context) (string, error) {
|
func (q *FakeQuerier) GetNotificationsSettings(_ context.Context) (string, error) {
|
||||||
q.mutex.RLock()
|
q.mutex.RLock()
|
||||||
defer q.mutex.RUnlock()
|
defer q.mutex.RUnlock()
|
||||||
@@ -4853,6 +4867,22 @@ func (q *FakeQuerier) GetUserLinksByUserID(_ context.Context, userID uuid.UUID)
|
|||||||
return uls, nil
|
return uls, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (q *FakeQuerier) GetUserNotificationPreferences(_ context.Context, userID uuid.UUID) ([]database.NotificationPreference, error) {
|
||||||
|
q.mutex.RLock()
|
||||||
|
defer q.mutex.RUnlock()
|
||||||
|
|
||||||
|
out := make([]database.NotificationPreference, 0, len(q.notificationPreferences))
|
||||||
|
for _, np := range q.notificationPreferences {
|
||||||
|
if np.UserID != userID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
out = append(out, np)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (q *FakeQuerier) GetUserWorkspaceBuildParameters(_ context.Context, params database.GetUserWorkspaceBuildParametersParams) ([]database.GetUserWorkspaceBuildParametersRow, error) {
|
func (q *FakeQuerier) GetUserWorkspaceBuildParameters(_ context.Context, params database.GetUserWorkspaceBuildParametersParams) ([]database.GetUserWorkspaceBuildParametersRow, error) {
|
||||||
q.mutex.RLock()
|
q.mutex.RLock()
|
||||||
defer q.mutex.RUnlock()
|
defer q.mutex.RUnlock()
|
||||||
@@ -7520,6 +7550,12 @@ func (q *FakeQuerier) UpdateMemberRoles(_ context.Context, arg database.UpdateMe
|
|||||||
return database.OrganizationMember{}, sql.ErrNoRows
|
return database.OrganizationMember{}, sql.ErrNoRows
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (*FakeQuerier) UpdateNotificationTemplateMethodByID(_ context.Context, _ database.UpdateNotificationTemplateMethodByIDParams) (database.NotificationTemplate, error) {
|
||||||
|
// Not implementing this function because it relies on state in the database which is created with migrations.
|
||||||
|
// We could consider using code-generation to align the database state and dbmem, but it's not worth it right now.
|
||||||
|
return database.NotificationTemplate{}, ErrUnimplemented
|
||||||
|
}
|
||||||
|
|
||||||
func (q *FakeQuerier) UpdateOAuth2ProviderAppByID(_ context.Context, arg database.UpdateOAuth2ProviderAppByIDParams) (database.OAuth2ProviderApp, error) {
|
func (q *FakeQuerier) UpdateOAuth2ProviderAppByID(_ context.Context, arg database.UpdateOAuth2ProviderAppByIDParams) (database.OAuth2ProviderApp, error) {
|
||||||
err := validateDatabaseType(arg)
|
err := validateDatabaseType(arg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -8114,6 +8150,57 @@ func (q *FakeQuerier) UpdateUserLoginType(_ context.Context, arg database.Update
|
|||||||
return database.User{}, sql.ErrNoRows
|
return database.User{}, sql.ErrNoRows
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (q *FakeQuerier) UpdateUserNotificationPreferences(_ context.Context, arg database.UpdateUserNotificationPreferencesParams) (int64, error) {
|
||||||
|
err := validateDatabaseType(arg)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
q.mutex.Lock()
|
||||||
|
defer q.mutex.Unlock()
|
||||||
|
|
||||||
|
var upserted int64
|
||||||
|
for i := range arg.NotificationTemplateIds {
|
||||||
|
var (
|
||||||
|
found bool
|
||||||
|
templateID = arg.NotificationTemplateIds[i]
|
||||||
|
disabled = arg.Disableds[i]
|
||||||
|
)
|
||||||
|
|
||||||
|
for j, np := range q.notificationPreferences {
|
||||||
|
if np.UserID != arg.UserID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if np.NotificationTemplateID != templateID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
np.Disabled = disabled
|
||||||
|
np.UpdatedAt = dbtime.Now()
|
||||||
|
q.notificationPreferences[j] = np
|
||||||
|
|
||||||
|
upserted++
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
np := database.NotificationPreference{
|
||||||
|
Disabled: disabled,
|
||||||
|
UserID: arg.UserID,
|
||||||
|
NotificationTemplateID: templateID,
|
||||||
|
CreatedAt: dbtime.Now(),
|
||||||
|
UpdatedAt: dbtime.Now(),
|
||||||
|
}
|
||||||
|
q.notificationPreferences = append(q.notificationPreferences, np)
|
||||||
|
upserted++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return upserted, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (q *FakeQuerier) UpdateUserProfile(_ context.Context, arg database.UpdateUserProfileParams) (database.User, error) {
|
func (q *FakeQuerier) UpdateUserProfile(_ context.Context, arg database.UpdateUserProfileParams) (database.User, error) {
|
||||||
if err := validateDatabaseType(arg); err != nil {
|
if err := validateDatabaseType(arg); err != nil {
|
||||||
return database.User{}, err
|
return database.User{}, err
|
||||||
|
|||||||
@@ -746,6 +746,20 @@ func (m metricsStore) GetNotificationMessagesByStatus(ctx context.Context, arg d
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m metricsStore) GetNotificationTemplateByID(ctx context.Context, id uuid.UUID) (database.NotificationTemplate, error) {
|
||||||
|
start := time.Now()
|
||||||
|
r0, r1 := m.s.GetNotificationTemplateByID(ctx, id)
|
||||||
|
m.queryLatencies.WithLabelValues("GetNotificationTemplateByID").Observe(time.Since(start).Seconds())
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m metricsStore) GetNotificationTemplatesByKind(ctx context.Context, kind database.NotificationTemplateKind) ([]database.NotificationTemplate, error) {
|
||||||
|
start := time.Now()
|
||||||
|
r0, r1 := m.s.GetNotificationTemplatesByKind(ctx, kind)
|
||||||
|
m.queryLatencies.WithLabelValues("GetNotificationTemplatesByKind").Observe(time.Since(start).Seconds())
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
func (m metricsStore) GetNotificationsSettings(ctx context.Context) (string, error) {
|
func (m metricsStore) GetNotificationsSettings(ctx context.Context) (string, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
r0, r1 := m.s.GetNotificationsSettings(ctx)
|
r0, r1 := m.s.GetNotificationsSettings(ctx)
|
||||||
@@ -1222,6 +1236,13 @@ func (m metricsStore) GetUserLinksByUserID(ctx context.Context, userID uuid.UUID
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m metricsStore) GetUserNotificationPreferences(ctx context.Context, userID uuid.UUID) ([]database.NotificationPreference, error) {
|
||||||
|
start := time.Now()
|
||||||
|
r0, r1 := m.s.GetUserNotificationPreferences(ctx, userID)
|
||||||
|
m.queryLatencies.WithLabelValues("GetUserNotificationPreferences").Observe(time.Since(start).Seconds())
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
func (m metricsStore) GetUserWorkspaceBuildParameters(ctx context.Context, ownerID database.GetUserWorkspaceBuildParametersParams) ([]database.GetUserWorkspaceBuildParametersRow, error) {
|
func (m metricsStore) GetUserWorkspaceBuildParameters(ctx context.Context, ownerID database.GetUserWorkspaceBuildParametersParams) ([]database.GetUserWorkspaceBuildParametersRow, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
r0, r1 := m.s.GetUserWorkspaceBuildParameters(ctx, ownerID)
|
r0, r1 := m.s.GetUserWorkspaceBuildParameters(ctx, ownerID)
|
||||||
@@ -1957,6 +1978,13 @@ func (m metricsStore) UpdateMemberRoles(ctx context.Context, arg database.Update
|
|||||||
return member, err
|
return member, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m metricsStore) UpdateNotificationTemplateMethodByID(ctx context.Context, arg database.UpdateNotificationTemplateMethodByIDParams) (database.NotificationTemplate, error) {
|
||||||
|
start := time.Now()
|
||||||
|
r0, r1 := m.s.UpdateNotificationTemplateMethodByID(ctx, arg)
|
||||||
|
m.queryLatencies.WithLabelValues("UpdateNotificationTemplateMethodByID").Observe(time.Since(start).Seconds())
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
func (m metricsStore) UpdateOAuth2ProviderAppByID(ctx context.Context, arg database.UpdateOAuth2ProviderAppByIDParams) (database.OAuth2ProviderApp, error) {
|
func (m metricsStore) UpdateOAuth2ProviderAppByID(ctx context.Context, arg database.UpdateOAuth2ProviderAppByIDParams) (database.OAuth2ProviderApp, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
r0, r1 := m.s.UpdateOAuth2ProviderAppByID(ctx, arg)
|
r0, r1 := m.s.UpdateOAuth2ProviderAppByID(ctx, arg)
|
||||||
@@ -2139,6 +2167,13 @@ func (m metricsStore) UpdateUserLoginType(ctx context.Context, arg database.Upda
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m metricsStore) UpdateUserNotificationPreferences(ctx context.Context, arg database.UpdateUserNotificationPreferencesParams) (int64, error) {
|
||||||
|
start := time.Now()
|
||||||
|
r0, r1 := m.s.UpdateUserNotificationPreferences(ctx, arg)
|
||||||
|
m.queryLatencies.WithLabelValues("UpdateUserNotificationPreferences").Observe(time.Since(start).Seconds())
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
func (m metricsStore) UpdateUserProfile(ctx context.Context, arg database.UpdateUserProfileParams) (database.User, error) {
|
func (m metricsStore) UpdateUserProfile(ctx context.Context, arg database.UpdateUserProfileParams) (database.User, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
user, err := m.s.UpdateUserProfile(ctx, arg)
|
user, err := m.s.UpdateUserProfile(ctx, arg)
|
||||||
|
|||||||
@@ -1495,6 +1495,36 @@ func (mr *MockStoreMockRecorder) GetNotificationMessagesByStatus(arg0, arg1 any)
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationMessagesByStatus", reflect.TypeOf((*MockStore)(nil).GetNotificationMessagesByStatus), arg0, arg1)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationMessagesByStatus", reflect.TypeOf((*MockStore)(nil).GetNotificationMessagesByStatus), arg0, arg1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetNotificationTemplateByID mocks base method.
|
||||||
|
func (m *MockStore) GetNotificationTemplateByID(arg0 context.Context, arg1 uuid.UUID) (database.NotificationTemplate, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetNotificationTemplateByID", arg0, arg1)
|
||||||
|
ret0, _ := ret[0].(database.NotificationTemplate)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNotificationTemplateByID indicates an expected call of GetNotificationTemplateByID.
|
||||||
|
func (mr *MockStoreMockRecorder) GetNotificationTemplateByID(arg0, arg1 any) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationTemplateByID", reflect.TypeOf((*MockStore)(nil).GetNotificationTemplateByID), arg0, arg1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNotificationTemplatesByKind mocks base method.
|
||||||
|
func (m *MockStore) GetNotificationTemplatesByKind(arg0 context.Context, arg1 database.NotificationTemplateKind) ([]database.NotificationTemplate, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetNotificationTemplatesByKind", arg0, arg1)
|
||||||
|
ret0, _ := ret[0].([]database.NotificationTemplate)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNotificationTemplatesByKind indicates an expected call of GetNotificationTemplatesByKind.
|
||||||
|
func (mr *MockStoreMockRecorder) GetNotificationTemplatesByKind(arg0, arg1 any) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationTemplatesByKind", reflect.TypeOf((*MockStore)(nil).GetNotificationTemplatesByKind), arg0, arg1)
|
||||||
|
}
|
||||||
|
|
||||||
// GetNotificationsSettings mocks base method.
|
// GetNotificationsSettings mocks base method.
|
||||||
func (m *MockStore) GetNotificationsSettings(arg0 context.Context) (string, error) {
|
func (m *MockStore) GetNotificationsSettings(arg0 context.Context) (string, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@@ -2545,6 +2575,21 @@ func (mr *MockStoreMockRecorder) GetUserLinksByUserID(arg0, arg1 any) *gomock.Ca
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserLinksByUserID", reflect.TypeOf((*MockStore)(nil).GetUserLinksByUserID), arg0, arg1)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserLinksByUserID", reflect.TypeOf((*MockStore)(nil).GetUserLinksByUserID), arg0, arg1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserNotificationPreferences mocks base method.
|
||||||
|
func (m *MockStore) GetUserNotificationPreferences(arg0 context.Context, arg1 uuid.UUID) ([]database.NotificationPreference, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetUserNotificationPreferences", arg0, arg1)
|
||||||
|
ret0, _ := ret[0].([]database.NotificationPreference)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserNotificationPreferences indicates an expected call of GetUserNotificationPreferences.
|
||||||
|
func (mr *MockStoreMockRecorder) GetUserNotificationPreferences(arg0, arg1 any) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserNotificationPreferences", reflect.TypeOf((*MockStore)(nil).GetUserNotificationPreferences), arg0, arg1)
|
||||||
|
}
|
||||||
|
|
||||||
// GetUserWorkspaceBuildParameters mocks base method.
|
// GetUserWorkspaceBuildParameters mocks base method.
|
||||||
func (m *MockStore) GetUserWorkspaceBuildParameters(arg0 context.Context, arg1 database.GetUserWorkspaceBuildParametersParams) ([]database.GetUserWorkspaceBuildParametersRow, error) {
|
func (m *MockStore) GetUserWorkspaceBuildParameters(arg0 context.Context, arg1 database.GetUserWorkspaceBuildParametersParams) ([]database.GetUserWorkspaceBuildParametersRow, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@@ -4131,6 +4176,21 @@ func (mr *MockStoreMockRecorder) UpdateMemberRoles(arg0, arg1 any) *gomock.Call
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateMemberRoles", reflect.TypeOf((*MockStore)(nil).UpdateMemberRoles), arg0, arg1)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateMemberRoles", reflect.TypeOf((*MockStore)(nil).UpdateMemberRoles), arg0, arg1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateNotificationTemplateMethodByID mocks base method.
|
||||||
|
func (m *MockStore) UpdateNotificationTemplateMethodByID(arg0 context.Context, arg1 database.UpdateNotificationTemplateMethodByIDParams) (database.NotificationTemplate, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "UpdateNotificationTemplateMethodByID", arg0, arg1)
|
||||||
|
ret0, _ := ret[0].(database.NotificationTemplate)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateNotificationTemplateMethodByID indicates an expected call of UpdateNotificationTemplateMethodByID.
|
||||||
|
func (mr *MockStoreMockRecorder) UpdateNotificationTemplateMethodByID(arg0, arg1 any) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateNotificationTemplateMethodByID", reflect.TypeOf((*MockStore)(nil).UpdateNotificationTemplateMethodByID), arg0, arg1)
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateOAuth2ProviderAppByID mocks base method.
|
// UpdateOAuth2ProviderAppByID mocks base method.
|
||||||
func (m *MockStore) UpdateOAuth2ProviderAppByID(arg0 context.Context, arg1 database.UpdateOAuth2ProviderAppByIDParams) (database.OAuth2ProviderApp, error) {
|
func (m *MockStore) UpdateOAuth2ProviderAppByID(arg0 context.Context, arg1 database.UpdateOAuth2ProviderAppByIDParams) (database.OAuth2ProviderApp, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@@ -4504,6 +4564,21 @@ func (mr *MockStoreMockRecorder) UpdateUserLoginType(arg0, arg1 any) *gomock.Cal
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserLoginType", reflect.TypeOf((*MockStore)(nil).UpdateUserLoginType), arg0, arg1)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserLoginType", reflect.TypeOf((*MockStore)(nil).UpdateUserLoginType), arg0, arg1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateUserNotificationPreferences mocks base method.
|
||||||
|
func (m *MockStore) UpdateUserNotificationPreferences(arg0 context.Context, arg1 database.UpdateUserNotificationPreferencesParams) (int64, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "UpdateUserNotificationPreferences", arg0, arg1)
|
||||||
|
ret0, _ := ret[0].(int64)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUserNotificationPreferences indicates an expected call of UpdateUserNotificationPreferences.
|
||||||
|
func (mr *MockStoreMockRecorder) UpdateUserNotificationPreferences(arg0, arg1 any) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserNotificationPreferences", reflect.TypeOf((*MockStore)(nil).UpdateUserNotificationPreferences), arg0, arg1)
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateUserProfile mocks base method.
|
// UpdateUserProfile mocks base method.
|
||||||
func (m *MockStore) UpdateUserProfile(arg0 context.Context, arg1 database.UpdateUserProfileParams) (database.User, error) {
|
func (m *MockStore) UpdateUserProfile(arg0 context.Context, arg1 database.UpdateUserProfileParams) (database.User, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
|||||||
Generated
+49
-3
@@ -84,7 +84,8 @@ CREATE TYPE notification_message_status AS ENUM (
|
|||||||
'sent',
|
'sent',
|
||||||
'permanent_failure',
|
'permanent_failure',
|
||||||
'temporary_failure',
|
'temporary_failure',
|
||||||
'unknown'
|
'unknown',
|
||||||
|
'inhibited'
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TYPE notification_method AS ENUM (
|
CREATE TYPE notification_method AS ENUM (
|
||||||
@@ -92,6 +93,10 @@ CREATE TYPE notification_method AS ENUM (
|
|||||||
'webhook'
|
'webhook'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TYPE notification_template_kind AS ENUM (
|
||||||
|
'system'
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TYPE parameter_destination_scheme AS ENUM (
|
CREATE TYPE parameter_destination_scheme AS ENUM (
|
||||||
'none',
|
'none',
|
||||||
'environment_variable',
|
'environment_variable',
|
||||||
@@ -164,7 +169,8 @@ CREATE TYPE resource_type AS ENUM (
|
|||||||
'oauth2_provider_app_secret',
|
'oauth2_provider_app_secret',
|
||||||
'custom_role',
|
'custom_role',
|
||||||
'organization_member',
|
'organization_member',
|
||||||
'notifications_settings'
|
'notifications_settings',
|
||||||
|
'notification_template'
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TYPE startup_script_behavior AS ENUM (
|
CREATE TYPE startup_script_behavior AS ENUM (
|
||||||
@@ -249,6 +255,23 @@ BEGIN
|
|||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
|
CREATE FUNCTION inhibit_enqueue_if_disabled() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
-- Fail the insertion if the user has disabled this notification.
|
||||||
|
IF EXISTS (SELECT 1
|
||||||
|
FROM notification_preferences
|
||||||
|
WHERE disabled = TRUE
|
||||||
|
AND user_id = NEW.user_id
|
||||||
|
AND notification_template_id = NEW.notification_template_id) THEN
|
||||||
|
RAISE EXCEPTION 'cannot enqueue message: user has disabled this notification';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
CREATE FUNCTION insert_apikey_fail_if_user_deleted() RETURNS trigger
|
CREATE FUNCTION insert_apikey_fail_if_user_deleted() RETURNS trigger
|
||||||
LANGUAGE plpgsql
|
LANGUAGE plpgsql
|
||||||
AS $$
|
AS $$
|
||||||
@@ -567,17 +590,29 @@ CREATE TABLE notification_messages (
|
|||||||
queued_seconds double precision
|
queued_seconds double precision
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE notification_preferences (
|
||||||
|
user_id uuid NOT NULL,
|
||||||
|
notification_template_id uuid NOT NULL,
|
||||||
|
disabled boolean DEFAULT false NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE notification_templates (
|
CREATE TABLE notification_templates (
|
||||||
id uuid NOT NULL,
|
id uuid NOT NULL,
|
||||||
name text NOT NULL,
|
name text NOT NULL,
|
||||||
title_template text NOT NULL,
|
title_template text NOT NULL,
|
||||||
body_template text NOT NULL,
|
body_template text NOT NULL,
|
||||||
actions jsonb,
|
actions jsonb,
|
||||||
"group" text
|
"group" text,
|
||||||
|
method notification_method,
|
||||||
|
kind notification_template_kind DEFAULT 'system'::notification_template_kind NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
COMMENT ON TABLE notification_templates IS 'Templates from which to create notification messages.';
|
COMMENT ON TABLE notification_templates IS 'Templates from which to create notification messages.';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN notification_templates.method IS 'NULL defers to the deployment-level method';
|
||||||
|
|
||||||
CREATE TABLE oauth2_provider_app_codes (
|
CREATE TABLE oauth2_provider_app_codes (
|
||||||
id uuid NOT NULL,
|
id uuid NOT NULL,
|
||||||
created_at timestamp with time zone NOT NULL,
|
created_at timestamp with time zone NOT NULL,
|
||||||
@@ -1536,6 +1571,9 @@ ALTER TABLE ONLY licenses
|
|||||||
ALTER TABLE ONLY notification_messages
|
ALTER TABLE ONLY notification_messages
|
||||||
ADD CONSTRAINT notification_messages_pkey PRIMARY KEY (id);
|
ADD CONSTRAINT notification_messages_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
ALTER TABLE ONLY notification_preferences
|
||||||
|
ADD CONSTRAINT notification_preferences_pkey PRIMARY KEY (user_id, notification_template_id);
|
||||||
|
|
||||||
ALTER TABLE ONLY notification_templates
|
ALTER TABLE ONLY notification_templates
|
||||||
ADD CONSTRAINT notification_templates_name_key UNIQUE (name);
|
ADD CONSTRAINT notification_templates_name_key UNIQUE (name);
|
||||||
|
|
||||||
@@ -1798,6 +1836,8 @@ CREATE INDEX workspace_resources_job_id_idx ON workspace_resources USING btree (
|
|||||||
|
|
||||||
CREATE UNIQUE INDEX workspaces_owner_id_lower_idx ON workspaces USING btree (owner_id, lower((name)::text)) WHERE (deleted = false);
|
CREATE UNIQUE INDEX workspaces_owner_id_lower_idx ON workspaces USING btree (owner_id, lower((name)::text)) WHERE (deleted = false);
|
||||||
|
|
||||||
|
CREATE TRIGGER inhibit_enqueue_if_disabled BEFORE INSERT ON notification_messages FOR EACH ROW EXECUTE FUNCTION inhibit_enqueue_if_disabled();
|
||||||
|
|
||||||
CREATE TRIGGER tailnet_notify_agent_change AFTER INSERT OR DELETE OR UPDATE ON tailnet_agents FOR EACH ROW EXECUTE FUNCTION tailnet_notify_agent_change();
|
CREATE TRIGGER tailnet_notify_agent_change AFTER INSERT OR DELETE OR UPDATE ON tailnet_agents FOR EACH ROW EXECUTE FUNCTION tailnet_notify_agent_change();
|
||||||
|
|
||||||
CREATE TRIGGER tailnet_notify_client_change AFTER INSERT OR DELETE OR UPDATE ON tailnet_clients FOR EACH ROW EXECUTE FUNCTION tailnet_notify_client_change();
|
CREATE TRIGGER tailnet_notify_client_change AFTER INSERT OR DELETE OR UPDATE ON tailnet_clients FOR EACH ROW EXECUTE FUNCTION tailnet_notify_client_change();
|
||||||
@@ -1851,6 +1891,12 @@ ALTER TABLE ONLY notification_messages
|
|||||||
ALTER TABLE ONLY notification_messages
|
ALTER TABLE ONLY notification_messages
|
||||||
ADD CONSTRAINT notification_messages_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
ADD CONSTRAINT notification_messages_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE ONLY notification_preferences
|
||||||
|
ADD CONSTRAINT notification_preferences_notification_template_id_fkey FOREIGN KEY (notification_template_id) REFERENCES notification_templates(id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE ONLY notification_preferences
|
||||||
|
ADD CONSTRAINT notification_preferences_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||||
|
|
||||||
ALTER TABLE ONLY oauth2_provider_app_codes
|
ALTER TABLE ONLY oauth2_provider_app_codes
|
||||||
ADD CONSTRAINT oauth2_provider_app_codes_app_id_fkey FOREIGN KEY (app_id) REFERENCES oauth2_provider_apps(id) ON DELETE CASCADE;
|
ADD CONSTRAINT oauth2_provider_app_codes_app_id_fkey FOREIGN KEY (app_id) REFERENCES oauth2_provider_apps(id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ const (
|
|||||||
ForeignKeyJfrogXrayScansWorkspaceID ForeignKeyConstraint = "jfrog_xray_scans_workspace_id_fkey" // ALTER TABLE ONLY jfrog_xray_scans ADD CONSTRAINT jfrog_xray_scans_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE;
|
ForeignKeyJfrogXrayScansWorkspaceID ForeignKeyConstraint = "jfrog_xray_scans_workspace_id_fkey" // ALTER TABLE ONLY jfrog_xray_scans ADD CONSTRAINT jfrog_xray_scans_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE;
|
||||||
ForeignKeyNotificationMessagesNotificationTemplateID ForeignKeyConstraint = "notification_messages_notification_template_id_fkey" // ALTER TABLE ONLY notification_messages ADD CONSTRAINT notification_messages_notification_template_id_fkey FOREIGN KEY (notification_template_id) REFERENCES notification_templates(id) ON DELETE CASCADE;
|
ForeignKeyNotificationMessagesNotificationTemplateID ForeignKeyConstraint = "notification_messages_notification_template_id_fkey" // ALTER TABLE ONLY notification_messages ADD CONSTRAINT notification_messages_notification_template_id_fkey FOREIGN KEY (notification_template_id) REFERENCES notification_templates(id) ON DELETE CASCADE;
|
||||||
ForeignKeyNotificationMessagesUserID ForeignKeyConstraint = "notification_messages_user_id_fkey" // ALTER TABLE ONLY notification_messages ADD CONSTRAINT notification_messages_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
ForeignKeyNotificationMessagesUserID ForeignKeyConstraint = "notification_messages_user_id_fkey" // ALTER TABLE ONLY notification_messages ADD CONSTRAINT notification_messages_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||||
|
ForeignKeyNotificationPreferencesNotificationTemplateID ForeignKeyConstraint = "notification_preferences_notification_template_id_fkey" // ALTER TABLE ONLY notification_preferences ADD CONSTRAINT notification_preferences_notification_template_id_fkey FOREIGN KEY (notification_template_id) REFERENCES notification_templates(id) ON DELETE CASCADE;
|
||||||
|
ForeignKeyNotificationPreferencesUserID ForeignKeyConstraint = "notification_preferences_user_id_fkey" // ALTER TABLE ONLY notification_preferences ADD CONSTRAINT notification_preferences_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||||
ForeignKeyOauth2ProviderAppCodesAppID ForeignKeyConstraint = "oauth2_provider_app_codes_app_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_codes ADD CONSTRAINT oauth2_provider_app_codes_app_id_fkey FOREIGN KEY (app_id) REFERENCES oauth2_provider_apps(id) ON DELETE CASCADE;
|
ForeignKeyOauth2ProviderAppCodesAppID ForeignKeyConstraint = "oauth2_provider_app_codes_app_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_codes ADD CONSTRAINT oauth2_provider_app_codes_app_id_fkey FOREIGN KEY (app_id) REFERENCES oauth2_provider_apps(id) ON DELETE CASCADE;
|
||||||
ForeignKeyOauth2ProviderAppCodesUserID ForeignKeyConstraint = "oauth2_provider_app_codes_user_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_codes ADD CONSTRAINT oauth2_provider_app_codes_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
ForeignKeyOauth2ProviderAppCodesUserID ForeignKeyConstraint = "oauth2_provider_app_codes_user_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_codes ADD CONSTRAINT oauth2_provider_app_codes_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||||
ForeignKeyOauth2ProviderAppSecretsAppID ForeignKeyConstraint = "oauth2_provider_app_secrets_app_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_secrets ADD CONSTRAINT oauth2_provider_app_secrets_app_id_fkey FOREIGN KEY (app_id) REFERENCES oauth2_provider_apps(id) ON DELETE CASCADE;
|
ForeignKeyOauth2ProviderAppSecretsAppID ForeignKeyConstraint = "oauth2_provider_app_secrets_app_id_fkey" // ALTER TABLE ONLY oauth2_provider_app_secrets ADD CONSTRAINT oauth2_provider_app_secrets_app_id_fkey FOREIGN KEY (app_id) REFERENCES oauth2_provider_apps(id) ON DELETE CASCADE;
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
ALTER TABLE notification_templates
|
||||||
|
DROP COLUMN IF EXISTS method,
|
||||||
|
DROP COLUMN IF EXISTS kind;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS notification_preferences;
|
||||||
|
DROP TYPE IF EXISTS notification_template_kind;
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS inhibit_enqueue_if_disabled ON notification_messages;
|
||||||
|
DROP FUNCTION IF EXISTS inhibit_enqueue_if_disabled;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
CREATE TABLE notification_preferences
|
||||||
|
(
|
||||||
|
user_id uuid REFERENCES users ON DELETE CASCADE NOT NULL,
|
||||||
|
notification_template_id uuid REFERENCES notification_templates ON DELETE CASCADE NOT NULL,
|
||||||
|
disabled bool NOT NULL DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (user_id, notification_template_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Add a new type (to be expanded upon later) which specifies the kind of notification template.
|
||||||
|
CREATE TYPE notification_template_kind AS ENUM (
|
||||||
|
'system'
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE notification_templates
|
||||||
|
-- Allow per-template notification method (enterprise only).
|
||||||
|
ADD COLUMN method notification_method,
|
||||||
|
-- Update all existing notification templates to be system templates.
|
||||||
|
ADD COLUMN kind notification_template_kind DEFAULT 'system'::notification_template_kind NOT NULL;
|
||||||
|
COMMENT ON COLUMN notification_templates.method IS 'NULL defers to the deployment-level method';
|
||||||
|
|
||||||
|
-- No equivalent in down migration because ENUM values cannot be deleted.
|
||||||
|
ALTER TYPE notification_message_status ADD VALUE IF NOT EXISTS 'inhibited';
|
||||||
|
|
||||||
|
-- Function to prevent enqueuing notifications unnecessarily.
|
||||||
|
CREATE OR REPLACE FUNCTION inhibit_enqueue_if_disabled()
|
||||||
|
RETURNS TRIGGER AS
|
||||||
|
$$
|
||||||
|
BEGIN
|
||||||
|
-- Fail the insertion if the user has disabled this notification.
|
||||||
|
IF EXISTS (SELECT 1
|
||||||
|
FROM notification_preferences
|
||||||
|
WHERE disabled = TRUE
|
||||||
|
AND user_id = NEW.user_id
|
||||||
|
AND notification_template_id = NEW.notification_template_id) THEN
|
||||||
|
RAISE EXCEPTION 'cannot enqueue message: user has disabled this notification';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- Trigger to execute above function on insertion.
|
||||||
|
CREATE TRIGGER inhibit_enqueue_if_disabled
|
||||||
|
BEFORE INSERT
|
||||||
|
ON notification_messages
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION inhibit_enqueue_if_disabled();
|
||||||
|
|
||||||
|
-- Allow modifications to notification templates to be audited.
|
||||||
|
ALTER TYPE resource_type ADD VALUE IF NOT EXISTS 'notification_template';
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
INSERT INTO notification_templates (id, name, title_template, body_template, "group")
|
||||||
|
VALUES ('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'A', 'title', 'body', 'Group 1') ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO notification_preferences (user_id, notification_template_id, disabled, created_at, updated_at)
|
||||||
|
VALUES ('a0061a8e-7db7-4585-838c-3116a003dd21', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', FALSE, '2024-07-15 10:30:00+00', '2024-07-15 10:30:00+00');
|
||||||
@@ -669,6 +669,7 @@ const (
|
|||||||
NotificationMessageStatusPermanentFailure NotificationMessageStatus = "permanent_failure"
|
NotificationMessageStatusPermanentFailure NotificationMessageStatus = "permanent_failure"
|
||||||
NotificationMessageStatusTemporaryFailure NotificationMessageStatus = "temporary_failure"
|
NotificationMessageStatusTemporaryFailure NotificationMessageStatus = "temporary_failure"
|
||||||
NotificationMessageStatusUnknown NotificationMessageStatus = "unknown"
|
NotificationMessageStatusUnknown NotificationMessageStatus = "unknown"
|
||||||
|
NotificationMessageStatusInhibited NotificationMessageStatus = "inhibited"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (e *NotificationMessageStatus) Scan(src interface{}) error {
|
func (e *NotificationMessageStatus) Scan(src interface{}) error {
|
||||||
@@ -713,7 +714,8 @@ func (e NotificationMessageStatus) Valid() bool {
|
|||||||
NotificationMessageStatusSent,
|
NotificationMessageStatusSent,
|
||||||
NotificationMessageStatusPermanentFailure,
|
NotificationMessageStatusPermanentFailure,
|
||||||
NotificationMessageStatusTemporaryFailure,
|
NotificationMessageStatusTemporaryFailure,
|
||||||
NotificationMessageStatusUnknown:
|
NotificationMessageStatusUnknown,
|
||||||
|
NotificationMessageStatusInhibited:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -727,6 +729,7 @@ func AllNotificationMessageStatusValues() []NotificationMessageStatus {
|
|||||||
NotificationMessageStatusPermanentFailure,
|
NotificationMessageStatusPermanentFailure,
|
||||||
NotificationMessageStatusTemporaryFailure,
|
NotificationMessageStatusTemporaryFailure,
|
||||||
NotificationMessageStatusUnknown,
|
NotificationMessageStatusUnknown,
|
||||||
|
NotificationMessageStatusInhibited,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -788,6 +791,61 @@ func AllNotificationMethodValues() []NotificationMethod {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NotificationTemplateKind string
|
||||||
|
|
||||||
|
const (
|
||||||
|
NotificationTemplateKindSystem NotificationTemplateKind = "system"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e *NotificationTemplateKind) Scan(src interface{}) error {
|
||||||
|
switch s := src.(type) {
|
||||||
|
case []byte:
|
||||||
|
*e = NotificationTemplateKind(s)
|
||||||
|
case string:
|
||||||
|
*e = NotificationTemplateKind(s)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported scan type for NotificationTemplateKind: %T", src)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type NullNotificationTemplateKind struct {
|
||||||
|
NotificationTemplateKind NotificationTemplateKind `json:"notification_template_kind"`
|
||||||
|
Valid bool `json:"valid"` // Valid is true if NotificationTemplateKind is not NULL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan implements the Scanner interface.
|
||||||
|
func (ns *NullNotificationTemplateKind) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
ns.NotificationTemplateKind, ns.Valid = "", false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ns.Valid = true
|
||||||
|
return ns.NotificationTemplateKind.Scan(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value implements the driver Valuer interface.
|
||||||
|
func (ns NullNotificationTemplateKind) Value() (driver.Value, error) {
|
||||||
|
if !ns.Valid {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return string(ns.NotificationTemplateKind), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e NotificationTemplateKind) Valid() bool {
|
||||||
|
switch e {
|
||||||
|
case NotificationTemplateKindSystem:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func AllNotificationTemplateKindValues() []NotificationTemplateKind {
|
||||||
|
return []NotificationTemplateKind{
|
||||||
|
NotificationTemplateKindSystem,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type ParameterDestinationScheme string
|
type ParameterDestinationScheme string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -1353,6 +1411,7 @@ const (
|
|||||||
ResourceTypeCustomRole ResourceType = "custom_role"
|
ResourceTypeCustomRole ResourceType = "custom_role"
|
||||||
ResourceTypeOrganizationMember ResourceType = "organization_member"
|
ResourceTypeOrganizationMember ResourceType = "organization_member"
|
||||||
ResourceTypeNotificationsSettings ResourceType = "notifications_settings"
|
ResourceTypeNotificationsSettings ResourceType = "notifications_settings"
|
||||||
|
ResourceTypeNotificationTemplate ResourceType = "notification_template"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (e *ResourceType) Scan(src interface{}) error {
|
func (e *ResourceType) Scan(src interface{}) error {
|
||||||
@@ -1409,7 +1468,8 @@ func (e ResourceType) Valid() bool {
|
|||||||
ResourceTypeOauth2ProviderAppSecret,
|
ResourceTypeOauth2ProviderAppSecret,
|
||||||
ResourceTypeCustomRole,
|
ResourceTypeCustomRole,
|
||||||
ResourceTypeOrganizationMember,
|
ResourceTypeOrganizationMember,
|
||||||
ResourceTypeNotificationsSettings:
|
ResourceTypeNotificationsSettings,
|
||||||
|
ResourceTypeNotificationTemplate:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -1435,6 +1495,7 @@ func AllResourceTypeValues() []ResourceType {
|
|||||||
ResourceTypeCustomRole,
|
ResourceTypeCustomRole,
|
||||||
ResourceTypeOrganizationMember,
|
ResourceTypeOrganizationMember,
|
||||||
ResourceTypeNotificationsSettings,
|
ResourceTypeNotificationsSettings,
|
||||||
|
ResourceTypeNotificationTemplate,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2034,6 +2095,14 @@ type NotificationMessage struct {
|
|||||||
QueuedSeconds sql.NullFloat64 `db:"queued_seconds" json:"queued_seconds"`
|
QueuedSeconds sql.NullFloat64 `db:"queued_seconds" json:"queued_seconds"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NotificationPreference struct {
|
||||||
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||||
|
NotificationTemplateID uuid.UUID `db:"notification_template_id" json:"notification_template_id"`
|
||||||
|
Disabled bool `db:"disabled" json:"disabled"`
|
||||||
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
// Templates from which to create notification messages.
|
// Templates from which to create notification messages.
|
||||||
type NotificationTemplate struct {
|
type NotificationTemplate struct {
|
||||||
ID uuid.UUID `db:"id" json:"id"`
|
ID uuid.UUID `db:"id" json:"id"`
|
||||||
@@ -2042,6 +2111,9 @@ type NotificationTemplate struct {
|
|||||||
BodyTemplate string `db:"body_template" json:"body_template"`
|
BodyTemplate string `db:"body_template" json:"body_template"`
|
||||||
Actions []byte `db:"actions" json:"actions"`
|
Actions []byte `db:"actions" json:"actions"`
|
||||||
Group sql.NullString `db:"group" json:"group"`
|
Group sql.NullString `db:"group" json:"group"`
|
||||||
|
// NULL defers to the deployment-level method
|
||||||
|
Method NullNotificationMethod `db:"method" json:"method"`
|
||||||
|
Kind NotificationTemplateKind `db:"kind" json:"kind"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// A table used to configure apps that can use Coder as an OAuth2 provider, the reverse of what we are calling external authentication.
|
// A table used to configure apps that can use Coder as an OAuth2 provider, the reverse of what we are calling external authentication.
|
||||||
|
|||||||
@@ -162,6 +162,8 @@ type sqlcQuerier interface {
|
|||||||
GetLicenses(ctx context.Context) ([]License, error)
|
GetLicenses(ctx context.Context) ([]License, error)
|
||||||
GetLogoURL(ctx context.Context) (string, error)
|
GetLogoURL(ctx context.Context) (string, error)
|
||||||
GetNotificationMessagesByStatus(ctx context.Context, arg GetNotificationMessagesByStatusParams) ([]NotificationMessage, error)
|
GetNotificationMessagesByStatus(ctx context.Context, arg GetNotificationMessagesByStatusParams) ([]NotificationMessage, error)
|
||||||
|
GetNotificationTemplateByID(ctx context.Context, id uuid.UUID) (NotificationTemplate, error)
|
||||||
|
GetNotificationTemplatesByKind(ctx context.Context, kind NotificationTemplateKind) ([]NotificationTemplate, error)
|
||||||
GetNotificationsSettings(ctx context.Context) (string, error)
|
GetNotificationsSettings(ctx context.Context) (string, error)
|
||||||
GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderApp, error)
|
GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderApp, error)
|
||||||
GetOAuth2ProviderAppCodeByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderAppCode, error)
|
GetOAuth2ProviderAppCodeByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderAppCode, error)
|
||||||
@@ -265,6 +267,7 @@ type sqlcQuerier interface {
|
|||||||
GetUserLinkByLinkedID(ctx context.Context, linkedID string) (UserLink, error)
|
GetUserLinkByLinkedID(ctx context.Context, linkedID string) (UserLink, error)
|
||||||
GetUserLinkByUserIDLoginType(ctx context.Context, arg GetUserLinkByUserIDLoginTypeParams) (UserLink, error)
|
GetUserLinkByUserIDLoginType(ctx context.Context, arg GetUserLinkByUserIDLoginTypeParams) (UserLink, error)
|
||||||
GetUserLinksByUserID(ctx context.Context, userID uuid.UUID) ([]UserLink, error)
|
GetUserLinksByUserID(ctx context.Context, userID uuid.UUID) ([]UserLink, error)
|
||||||
|
GetUserNotificationPreferences(ctx context.Context, userID uuid.UUID) ([]NotificationPreference, error)
|
||||||
GetUserWorkspaceBuildParameters(ctx context.Context, arg GetUserWorkspaceBuildParametersParams) ([]GetUserWorkspaceBuildParametersRow, error)
|
GetUserWorkspaceBuildParameters(ctx context.Context, arg GetUserWorkspaceBuildParametersParams) ([]GetUserWorkspaceBuildParametersRow, error)
|
||||||
// This will never return deleted users.
|
// This will never return deleted users.
|
||||||
GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUsersRow, error)
|
GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUsersRow, error)
|
||||||
@@ -401,6 +404,7 @@ type sqlcQuerier interface {
|
|||||||
UpdateGroupByID(ctx context.Context, arg UpdateGroupByIDParams) (Group, error)
|
UpdateGroupByID(ctx context.Context, arg UpdateGroupByIDParams) (Group, error)
|
||||||
UpdateInactiveUsersToDormant(ctx context.Context, arg UpdateInactiveUsersToDormantParams) ([]UpdateInactiveUsersToDormantRow, error)
|
UpdateInactiveUsersToDormant(ctx context.Context, arg UpdateInactiveUsersToDormantParams) ([]UpdateInactiveUsersToDormantRow, error)
|
||||||
UpdateMemberRoles(ctx context.Context, arg UpdateMemberRolesParams) (OrganizationMember, error)
|
UpdateMemberRoles(ctx context.Context, arg UpdateMemberRolesParams) (OrganizationMember, error)
|
||||||
|
UpdateNotificationTemplateMethodByID(ctx context.Context, arg UpdateNotificationTemplateMethodByIDParams) (NotificationTemplate, error)
|
||||||
UpdateOAuth2ProviderAppByID(ctx context.Context, arg UpdateOAuth2ProviderAppByIDParams) (OAuth2ProviderApp, error)
|
UpdateOAuth2ProviderAppByID(ctx context.Context, arg UpdateOAuth2ProviderAppByIDParams) (OAuth2ProviderApp, error)
|
||||||
UpdateOAuth2ProviderAppSecretByID(ctx context.Context, arg UpdateOAuth2ProviderAppSecretByIDParams) (OAuth2ProviderAppSecret, error)
|
UpdateOAuth2ProviderAppSecretByID(ctx context.Context, arg UpdateOAuth2ProviderAppSecretByIDParams) (OAuth2ProviderAppSecret, error)
|
||||||
UpdateOrganization(ctx context.Context, arg UpdateOrganizationParams) (Organization, error)
|
UpdateOrganization(ctx context.Context, arg UpdateOrganizationParams) (Organization, error)
|
||||||
@@ -427,6 +431,7 @@ type sqlcQuerier interface {
|
|||||||
UpdateUserLink(ctx context.Context, arg UpdateUserLinkParams) (UserLink, error)
|
UpdateUserLink(ctx context.Context, arg UpdateUserLinkParams) (UserLink, error)
|
||||||
UpdateUserLinkedID(ctx context.Context, arg UpdateUserLinkedIDParams) (UserLink, error)
|
UpdateUserLinkedID(ctx context.Context, arg UpdateUserLinkedIDParams) (UserLink, error)
|
||||||
UpdateUserLoginType(ctx context.Context, arg UpdateUserLoginTypeParams) (User, error)
|
UpdateUserLoginType(ctx context.Context, arg UpdateUserLoginTypeParams) (User, error)
|
||||||
|
UpdateUserNotificationPreferences(ctx context.Context, arg UpdateUserNotificationPreferencesParams) (int64, error)
|
||||||
UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) (User, error)
|
UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) (User, error)
|
||||||
UpdateUserQuietHoursSchedule(ctx context.Context, arg UpdateUserQuietHoursScheduleParams) (User, error)
|
UpdateUserQuietHoursSchedule(ctx context.Context, arg UpdateUserQuietHoursScheduleParams) (User, error)
|
||||||
UpdateUserRoles(ctx context.Context, arg UpdateUserRolesParams) (User, error)
|
UpdateUserRoles(ctx context.Context, arg UpdateUserRolesParams) (User, error)
|
||||||
|
|||||||
+172
-12
@@ -3335,14 +3335,18 @@ SELECT
|
|||||||
nm.id,
|
nm.id,
|
||||||
nm.payload,
|
nm.payload,
|
||||||
nm.method,
|
nm.method,
|
||||||
nm.attempt_count::int AS attempt_count,
|
nm.attempt_count::int AS attempt_count,
|
||||||
nm.queued_seconds::float AS queued_seconds,
|
nm.queued_seconds::float AS queued_seconds,
|
||||||
-- template
|
-- template
|
||||||
nt.id AS template_id,
|
nt.id AS template_id,
|
||||||
nt.title_template,
|
nt.title_template,
|
||||||
nt.body_template
|
nt.body_template,
|
||||||
|
-- preferences
|
||||||
|
(CASE WHEN np.disabled IS NULL THEN false ELSE np.disabled END)::bool AS disabled
|
||||||
FROM acquired nm
|
FROM acquired nm
|
||||||
JOIN notification_templates nt ON nm.notification_template_id = nt.id
|
JOIN notification_templates nt ON nm.notification_template_id = nt.id
|
||||||
|
LEFT JOIN notification_preferences AS np
|
||||||
|
ON (np.user_id = nm.user_id AND np.notification_template_id = nm.notification_template_id)
|
||||||
`
|
`
|
||||||
|
|
||||||
type AcquireNotificationMessagesParams struct {
|
type AcquireNotificationMessagesParams struct {
|
||||||
@@ -3361,6 +3365,7 @@ type AcquireNotificationMessagesRow struct {
|
|||||||
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
|
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
|
||||||
TitleTemplate string `db:"title_template" json:"title_template"`
|
TitleTemplate string `db:"title_template" json:"title_template"`
|
||||||
BodyTemplate string `db:"body_template" json:"body_template"`
|
BodyTemplate string `db:"body_template" json:"body_template"`
|
||||||
|
Disabled bool `db:"disabled" json:"disabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Acquires the lease for a given count of notification messages, to enable concurrent dequeuing and subsequent sending.
|
// Acquires the lease for a given count of notification messages, to enable concurrent dequeuing and subsequent sending.
|
||||||
@@ -3396,6 +3401,7 @@ func (q *sqlQuerier) AcquireNotificationMessages(ctx context.Context, arg Acquir
|
|||||||
&i.TemplateID,
|
&i.TemplateID,
|
||||||
&i.TitleTemplate,
|
&i.TitleTemplate,
|
||||||
&i.BodyTemplate,
|
&i.BodyTemplate,
|
||||||
|
&i.Disabled,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -3534,10 +3540,11 @@ func (q *sqlQuerier) EnqueueNotificationMessage(ctx context.Context, arg Enqueue
|
|||||||
const fetchNewMessageMetadata = `-- name: FetchNewMessageMetadata :one
|
const fetchNewMessageMetadata = `-- name: FetchNewMessageMetadata :one
|
||||||
SELECT nt.name AS notification_name,
|
SELECT nt.name AS notification_name,
|
||||||
nt.actions AS actions,
|
nt.actions AS actions,
|
||||||
|
nt.method AS custom_method,
|
||||||
u.id AS user_id,
|
u.id AS user_id,
|
||||||
u.email AS user_email,
|
u.email AS user_email,
|
||||||
COALESCE(NULLIF(u.name, ''), NULLIF(u.username, ''))::text AS user_name,
|
COALESCE(NULLIF(u.name, ''), NULLIF(u.username, ''))::text AS user_name,
|
||||||
COALESCE(u.username, '') AS user_username
|
u.username AS user_username
|
||||||
FROM notification_templates nt,
|
FROM notification_templates nt,
|
||||||
users u
|
users u
|
||||||
WHERE nt.id = $1
|
WHERE nt.id = $1
|
||||||
@@ -3550,12 +3557,13 @@ type FetchNewMessageMetadataParams struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FetchNewMessageMetadataRow struct {
|
type FetchNewMessageMetadataRow struct {
|
||||||
NotificationName string `db:"notification_name" json:"notification_name"`
|
NotificationName string `db:"notification_name" json:"notification_name"`
|
||||||
Actions []byte `db:"actions" json:"actions"`
|
Actions []byte `db:"actions" json:"actions"`
|
||||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
CustomMethod NullNotificationMethod `db:"custom_method" json:"custom_method"`
|
||||||
UserEmail string `db:"user_email" json:"user_email"`
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||||
UserName string `db:"user_name" json:"user_name"`
|
UserEmail string `db:"user_email" json:"user_email"`
|
||||||
UserUsername string `db:"user_username" json:"user_username"`
|
UserName string `db:"user_name" json:"user_name"`
|
||||||
|
UserUsername string `db:"user_username" json:"user_username"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is used to build up the notification_message's JSON payload.
|
// This is used to build up the notification_message's JSON payload.
|
||||||
@@ -3565,6 +3573,7 @@ func (q *sqlQuerier) FetchNewMessageMetadata(ctx context.Context, arg FetchNewMe
|
|||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.NotificationName,
|
&i.NotificationName,
|
||||||
&i.Actions,
|
&i.Actions,
|
||||||
|
&i.CustomMethod,
|
||||||
&i.UserID,
|
&i.UserID,
|
||||||
&i.UserEmail,
|
&i.UserEmail,
|
||||||
&i.UserName,
|
&i.UserName,
|
||||||
@@ -3574,7 +3583,10 @@ func (q *sqlQuerier) FetchNewMessageMetadata(ctx context.Context, arg FetchNewMe
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getNotificationMessagesByStatus = `-- name: GetNotificationMessagesByStatus :many
|
const getNotificationMessagesByStatus = `-- name: GetNotificationMessagesByStatus :many
|
||||||
SELECT id, notification_template_id, user_id, method, status, status_reason, created_by, payload, attempt_count, targets, created_at, updated_at, leased_until, next_retry_after, queued_seconds FROM notification_messages WHERE status = $1 LIMIT $2::int
|
SELECT id, notification_template_id, user_id, method, status, status_reason, created_by, payload, attempt_count, targets, created_at, updated_at, leased_until, next_retry_after, queued_seconds
|
||||||
|
FROM notification_messages
|
||||||
|
WHERE status = $1
|
||||||
|
LIMIT $2::int
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetNotificationMessagesByStatusParams struct {
|
type GetNotificationMessagesByStatusParams struct {
|
||||||
@@ -3621,6 +3633,154 @@ func (q *sqlQuerier) GetNotificationMessagesByStatus(ctx context.Context, arg Ge
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getNotificationTemplateByID = `-- name: GetNotificationTemplateByID :one
|
||||||
|
SELECT id, name, title_template, body_template, actions, "group", method, kind
|
||||||
|
FROM notification_templates
|
||||||
|
WHERE id = $1::uuid
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *sqlQuerier) GetNotificationTemplateByID(ctx context.Context, id uuid.UUID) (NotificationTemplate, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getNotificationTemplateByID, id)
|
||||||
|
var i NotificationTemplate
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.TitleTemplate,
|
||||||
|
&i.BodyTemplate,
|
||||||
|
&i.Actions,
|
||||||
|
&i.Group,
|
||||||
|
&i.Method,
|
||||||
|
&i.Kind,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getNotificationTemplatesByKind = `-- name: GetNotificationTemplatesByKind :many
|
||||||
|
SELECT id, name, title_template, body_template, actions, "group", method, kind
|
||||||
|
FROM notification_templates
|
||||||
|
WHERE kind = $1::notification_template_kind
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *sqlQuerier) GetNotificationTemplatesByKind(ctx context.Context, kind NotificationTemplateKind) ([]NotificationTemplate, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getNotificationTemplatesByKind, kind)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []NotificationTemplate
|
||||||
|
for rows.Next() {
|
||||||
|
var i NotificationTemplate
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.TitleTemplate,
|
||||||
|
&i.BodyTemplate,
|
||||||
|
&i.Actions,
|
||||||
|
&i.Group,
|
||||||
|
&i.Method,
|
||||||
|
&i.Kind,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUserNotificationPreferences = `-- name: GetUserNotificationPreferences :many
|
||||||
|
SELECT user_id, notification_template_id, disabled, created_at, updated_at
|
||||||
|
FROM notification_preferences
|
||||||
|
WHERE user_id = $1::uuid
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *sqlQuerier) GetUserNotificationPreferences(ctx context.Context, userID uuid.UUID) ([]NotificationPreference, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getUserNotificationPreferences, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []NotificationPreference
|
||||||
|
for rows.Next() {
|
||||||
|
var i NotificationPreference
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.UserID,
|
||||||
|
&i.NotificationTemplateID,
|
||||||
|
&i.Disabled,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateNotificationTemplateMethodByID = `-- name: UpdateNotificationTemplateMethodByID :one
|
||||||
|
UPDATE notification_templates
|
||||||
|
SET method = $1::notification_method
|
||||||
|
WHERE id = $2::uuid
|
||||||
|
RETURNING id, name, title_template, body_template, actions, "group", method, kind
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateNotificationTemplateMethodByIDParams struct {
|
||||||
|
Method NullNotificationMethod `db:"method" json:"method"`
|
||||||
|
ID uuid.UUID `db:"id" json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *sqlQuerier) UpdateNotificationTemplateMethodByID(ctx context.Context, arg UpdateNotificationTemplateMethodByIDParams) (NotificationTemplate, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, updateNotificationTemplateMethodByID, arg.Method, arg.ID)
|
||||||
|
var i NotificationTemplate
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.TitleTemplate,
|
||||||
|
&i.BodyTemplate,
|
||||||
|
&i.Actions,
|
||||||
|
&i.Group,
|
||||||
|
&i.Method,
|
||||||
|
&i.Kind,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateUserNotificationPreferences = `-- name: UpdateUserNotificationPreferences :execrows
|
||||||
|
INSERT
|
||||||
|
INTO notification_preferences (user_id, notification_template_id, disabled)
|
||||||
|
SELECT $1::uuid, new_values.notification_template_id, new_values.disabled
|
||||||
|
FROM (SELECT UNNEST($2::uuid[]) AS notification_template_id,
|
||||||
|
UNNEST($3::bool[]) AS disabled) AS new_values
|
||||||
|
ON CONFLICT (user_id, notification_template_id) DO UPDATE
|
||||||
|
SET disabled = EXCLUDED.disabled,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateUserNotificationPreferencesParams struct {
|
||||||
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||||
|
NotificationTemplateIds []uuid.UUID `db:"notification_template_ids" json:"notification_template_ids"`
|
||||||
|
Disableds []bool `db:"disableds" json:"disableds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *sqlQuerier) UpdateUserNotificationPreferences(ctx context.Context, arg UpdateUserNotificationPreferencesParams) (int64, error) {
|
||||||
|
result, err := q.db.ExecContext(ctx, updateUserNotificationPreferences, arg.UserID, pq.Array(arg.NotificationTemplateIds), pq.Array(arg.Disableds))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return result.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
const deleteOAuth2ProviderAppByID = `-- name: DeleteOAuth2ProviderAppByID :exec
|
const deleteOAuth2ProviderAppByID = `-- name: DeleteOAuth2ProviderAppByID :exec
|
||||||
DELETE FROM oauth2_provider_apps WHERE id = $1
|
DELETE FROM oauth2_provider_apps WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
-- This is used to build up the notification_message's JSON payload.
|
-- This is used to build up the notification_message's JSON payload.
|
||||||
SELECT nt.name AS notification_name,
|
SELECT nt.name AS notification_name,
|
||||||
nt.actions AS actions,
|
nt.actions AS actions,
|
||||||
|
nt.method AS custom_method,
|
||||||
u.id AS user_id,
|
u.id AS user_id,
|
||||||
u.email AS user_email,
|
u.email AS user_email,
|
||||||
COALESCE(NULLIF(u.name, ''), NULLIF(u.username, ''))::text AS user_name,
|
COALESCE(NULLIF(u.name, ''), NULLIF(u.username, ''))::text AS user_name,
|
||||||
COALESCE(u.username, '') AS user_username
|
u.username AS user_username
|
||||||
FROM notification_templates nt,
|
FROM notification_templates nt,
|
||||||
users u
|
users u
|
||||||
WHERE nt.id = @notification_template_id
|
WHERE nt.id = @notification_template_id
|
||||||
@@ -79,14 +80,18 @@ SELECT
|
|||||||
nm.id,
|
nm.id,
|
||||||
nm.payload,
|
nm.payload,
|
||||||
nm.method,
|
nm.method,
|
||||||
nm.attempt_count::int AS attempt_count,
|
nm.attempt_count::int AS attempt_count,
|
||||||
nm.queued_seconds::float AS queued_seconds,
|
nm.queued_seconds::float AS queued_seconds,
|
||||||
-- template
|
-- template
|
||||||
nt.id AS template_id,
|
nt.id AS template_id,
|
||||||
nt.title_template,
|
nt.title_template,
|
||||||
nt.body_template
|
nt.body_template,
|
||||||
|
-- preferences
|
||||||
|
(CASE WHEN np.disabled IS NULL THEN false ELSE np.disabled END)::bool AS disabled
|
||||||
FROM acquired nm
|
FROM acquired nm
|
||||||
JOIN notification_templates nt ON nm.notification_template_id = nt.id;
|
JOIN notification_templates nt ON nm.notification_template_id = nt.id
|
||||||
|
LEFT JOIN notification_preferences AS np
|
||||||
|
ON (np.user_id = nm.user_id AND np.notification_template_id = nm.notification_template_id);
|
||||||
|
|
||||||
-- name: BulkMarkNotificationMessagesFailed :execrows
|
-- name: BulkMarkNotificationMessagesFailed :execrows
|
||||||
UPDATE notification_messages
|
UPDATE notification_messages
|
||||||
@@ -131,4 +136,38 @@ WHERE id IN
|
|||||||
WHERE nested.updated_at < NOW() - INTERVAL '7 days');
|
WHERE nested.updated_at < NOW() - INTERVAL '7 days');
|
||||||
|
|
||||||
-- name: GetNotificationMessagesByStatus :many
|
-- name: GetNotificationMessagesByStatus :many
|
||||||
SELECT * FROM notification_messages WHERE status = @status LIMIT sqlc.arg('limit')::int;
|
SELECT *
|
||||||
|
FROM notification_messages
|
||||||
|
WHERE status = @status
|
||||||
|
LIMIT sqlc.arg('limit')::int;
|
||||||
|
|
||||||
|
-- name: GetUserNotificationPreferences :many
|
||||||
|
SELECT *
|
||||||
|
FROM notification_preferences
|
||||||
|
WHERE user_id = @user_id::uuid;
|
||||||
|
|
||||||
|
-- name: UpdateUserNotificationPreferences :execrows
|
||||||
|
INSERT
|
||||||
|
INTO notification_preferences (user_id, notification_template_id, disabled)
|
||||||
|
SELECT @user_id::uuid, new_values.notification_template_id, new_values.disabled
|
||||||
|
FROM (SELECT UNNEST(@notification_template_ids::uuid[]) AS notification_template_id,
|
||||||
|
UNNEST(@disableds::bool[]) AS disabled) AS new_values
|
||||||
|
ON CONFLICT (user_id, notification_template_id) DO UPDATE
|
||||||
|
SET disabled = EXCLUDED.disabled,
|
||||||
|
updated_at = CURRENT_TIMESTAMP;
|
||||||
|
|
||||||
|
-- name: UpdateNotificationTemplateMethodByID :one
|
||||||
|
UPDATE notification_templates
|
||||||
|
SET method = sqlc.narg('method')::notification_method
|
||||||
|
WHERE id = @id::uuid
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: GetNotificationTemplateByID :one
|
||||||
|
SELECT *
|
||||||
|
FROM notification_templates
|
||||||
|
WHERE id = @id::uuid;
|
||||||
|
|
||||||
|
-- name: GetNotificationTemplatesByKind :many
|
||||||
|
SELECT *
|
||||||
|
FROM notification_templates
|
||||||
|
WHERE kind = @kind::notification_template_kind;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const (
|
|||||||
UniqueLicensesJWTKey UniqueConstraint = "licenses_jwt_key" // ALTER TABLE ONLY licenses ADD CONSTRAINT licenses_jwt_key UNIQUE (jwt);
|
UniqueLicensesJWTKey UniqueConstraint = "licenses_jwt_key" // ALTER TABLE ONLY licenses ADD CONSTRAINT licenses_jwt_key UNIQUE (jwt);
|
||||||
UniqueLicensesPkey UniqueConstraint = "licenses_pkey" // ALTER TABLE ONLY licenses ADD CONSTRAINT licenses_pkey PRIMARY KEY (id);
|
UniqueLicensesPkey UniqueConstraint = "licenses_pkey" // ALTER TABLE ONLY licenses ADD CONSTRAINT licenses_pkey PRIMARY KEY (id);
|
||||||
UniqueNotificationMessagesPkey UniqueConstraint = "notification_messages_pkey" // ALTER TABLE ONLY notification_messages ADD CONSTRAINT notification_messages_pkey PRIMARY KEY (id);
|
UniqueNotificationMessagesPkey UniqueConstraint = "notification_messages_pkey" // ALTER TABLE ONLY notification_messages ADD CONSTRAINT notification_messages_pkey PRIMARY KEY (id);
|
||||||
|
UniqueNotificationPreferencesPkey UniqueConstraint = "notification_preferences_pkey" // ALTER TABLE ONLY notification_preferences ADD CONSTRAINT notification_preferences_pkey PRIMARY KEY (user_id, notification_template_id);
|
||||||
UniqueNotificationTemplatesNameKey UniqueConstraint = "notification_templates_name_key" // ALTER TABLE ONLY notification_templates ADD CONSTRAINT notification_templates_name_key UNIQUE (name);
|
UniqueNotificationTemplatesNameKey UniqueConstraint = "notification_templates_name_key" // ALTER TABLE ONLY notification_templates ADD CONSTRAINT notification_templates_name_key UNIQUE (name);
|
||||||
UniqueNotificationTemplatesPkey UniqueConstraint = "notification_templates_pkey" // ALTER TABLE ONLY notification_templates ADD CONSTRAINT notification_templates_pkey PRIMARY KEY (id);
|
UniqueNotificationTemplatesPkey UniqueConstraint = "notification_templates_pkey" // ALTER TABLE ONLY notification_templates ADD CONSTRAINT notification_templates_pkey PRIMARY KEY (id);
|
||||||
UniqueOauth2ProviderAppCodesPkey UniqueConstraint = "oauth2_provider_app_codes_pkey" // ALTER TABLE ONLY oauth2_provider_app_codes ADD CONSTRAINT oauth2_provider_app_codes_pkey PRIMARY KEY (id);
|
UniqueOauth2ProviderAppCodesPkey UniqueConstraint = "oauth2_provider_app_codes_pkey" // ALTER TABLE ONLY oauth2_provider_app_codes ADD CONSTRAINT oauth2_provider_app_codes_pkey PRIMARY KEY (id);
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package httpmw
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
|
"github.com/coder/coder/v2/coderd/httpapi"
|
||||||
|
"github.com/coder/coder/v2/codersdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
type notificationTemplateParamContextKey struct{}
|
||||||
|
|
||||||
|
// NotificationTemplateParam returns the template from the ExtractNotificationTemplateParam handler.
|
||||||
|
func NotificationTemplateParam(r *http.Request) database.NotificationTemplate {
|
||||||
|
template, ok := r.Context().Value(notificationTemplateParamContextKey{}).(database.NotificationTemplate)
|
||||||
|
if !ok {
|
||||||
|
panic("developer error: notification template middleware not used")
|
||||||
|
}
|
||||||
|
return template
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractNotificationTemplateParam grabs a notification template from the "notification_template" URL parameter.
|
||||||
|
func ExtractNotificationTemplateParam(db database.Store) func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
notifTemplateID, parsed := ParseUUIDParam(rw, r, "notification_template")
|
||||||
|
if !parsed {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nt, err := db.GetNotificationTemplateByID(r.Context(), notifTemplateID)
|
||||||
|
if httpapi.Is404Error(err) {
|
||||||
|
httpapi.ResourceNotFound(rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||||
|
Message: "Internal error fetching notification template.",
|
||||||
|
Detail: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = context.WithValue(ctx, notificationTemplateParamContextKey{}, nt)
|
||||||
|
next.ServeHTTP(rw, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+190
-14
@@ -7,11 +7,13 @@ import (
|
|||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
|
||||||
|
"cdr.dev/slog"
|
||||||
|
|
||||||
"github.com/coder/coder/v2/coderd/audit"
|
"github.com/coder/coder/v2/coderd/audit"
|
||||||
"github.com/coder/coder/v2/coderd/database"
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
"github.com/coder/coder/v2/coderd/httpapi"
|
"github.com/coder/coder/v2/coderd/httpapi"
|
||||||
|
"github.com/coder/coder/v2/coderd/httpmw"
|
||||||
"github.com/coder/coder/v2/coderd/rbac"
|
"github.com/coder/coder/v2/coderd/rbac"
|
||||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
|
||||||
"github.com/coder/coder/v2/codersdk"
|
"github.com/coder/coder/v2/codersdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,7 +21,7 @@ import (
|
|||||||
// @ID get-notifications-settings
|
// @ID get-notifications-settings
|
||||||
// @Security CoderSessionToken
|
// @Security CoderSessionToken
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Tags General
|
// @Tags Notifications
|
||||||
// @Success 200 {object} codersdk.NotificationsSettings
|
// @Success 200 {object} codersdk.NotificationsSettings
|
||||||
// @Router /notifications/settings [get]
|
// @Router /notifications/settings [get]
|
||||||
func (api *API) notificationsSettings(rw http.ResponseWriter, r *http.Request) {
|
func (api *API) notificationsSettings(rw http.ResponseWriter, r *http.Request) {
|
||||||
@@ -51,7 +53,7 @@ func (api *API) notificationsSettings(rw http.ResponseWriter, r *http.Request) {
|
|||||||
// @Security CoderSessionToken
|
// @Security CoderSessionToken
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Tags General
|
// @Tags Notifications
|
||||||
// @Param request body codersdk.NotificationsSettings true "Notifications settings request"
|
// @Param request body codersdk.NotificationsSettings true "Notifications settings request"
|
||||||
// @Success 200 {object} codersdk.NotificationsSettings
|
// @Success 200 {object} codersdk.NotificationsSettings
|
||||||
// @Success 304
|
// @Success 304
|
||||||
@@ -59,13 +61,6 @@ func (api *API) notificationsSettings(rw http.ResponseWriter, r *http.Request) {
|
|||||||
func (api *API) putNotificationsSettings(rw http.ResponseWriter, r *http.Request) {
|
func (api *API) putNotificationsSettings(rw http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
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
|
var settings codersdk.NotificationsSettings
|
||||||
if !httpapi.Read(ctx, rw, r, &settings) {
|
if !httpapi.Read(ctx, rw, r, &settings) {
|
||||||
return
|
return
|
||||||
@@ -80,9 +75,9 @@ func (api *API) putNotificationsSettings(rw http.ResponseWriter, r *http.Request
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
currentSettingsJSON, err := api.Database.GetNotificationsSettings(r.Context())
|
currentSettingsJSON, err := api.Database.GetNotificationsSettings(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||||
Message: "Failed to fetch current notifications settings.",
|
Message: "Failed to fetch current notifications settings.",
|
||||||
Detail: err.Error(),
|
Detail: err.Error(),
|
||||||
})
|
})
|
||||||
@@ -91,7 +86,7 @@ func (api *API) putNotificationsSettings(rw http.ResponseWriter, r *http.Request
|
|||||||
|
|
||||||
if bytes.Equal(settingsJSON, []byte(currentSettingsJSON)) {
|
if bytes.Equal(settingsJSON, []byte(currentSettingsJSON)) {
|
||||||
// See: https://www.rfc-editor.org/rfc/rfc7232#section-4.1
|
// See: https://www.rfc-editor.org/rfc/rfc7232#section-4.1
|
||||||
httpapi.Write(r.Context(), rw, http.StatusNotModified, nil)
|
httpapi.Write(ctx, rw, http.StatusNotModified, nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,12 +106,193 @@ func (api *API) putNotificationsSettings(rw http.ResponseWriter, r *http.Request
|
|||||||
|
|
||||||
err = api.Database.UpsertNotificationsSettings(ctx, string(settingsJSON))
|
err = api.Database.UpsertNotificationsSettings(ctx, string(settingsJSON))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
|
if rbac.IsUnauthorizedError(err) {
|
||||||
|
httpapi.Forbidden(rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||||
Message: "Failed to update notifications settings.",
|
Message: "Failed to update notifications settings.",
|
||||||
Detail: err.Error(),
|
Detail: err.Error(),
|
||||||
})
|
})
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
httpapi.Write(r.Context(), rw, http.StatusOK, settings)
|
httpapi.Write(r.Context(), rw, http.StatusOK, settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary Get system notification templates
|
||||||
|
// @ID get-system-notification-templates
|
||||||
|
// @Security CoderSessionToken
|
||||||
|
// @Produce json
|
||||||
|
// @Tags Notifications
|
||||||
|
// @Success 200 {array} codersdk.NotificationTemplate
|
||||||
|
// @Router /notifications/templates/system [get]
|
||||||
|
func (api *API) systemNotificationTemplates(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
templates, err := api.Database.GetNotificationTemplatesByKind(ctx, database.NotificationTemplateKindSystem)
|
||||||
|
if err != nil {
|
||||||
|
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
|
||||||
|
Message: "Failed to retrieve system notifications templates.",
|
||||||
|
Detail: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out := convertNotificationTemplates(templates)
|
||||||
|
httpapi.Write(r.Context(), rw, http.StatusOK, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Get notification dispatch methods
|
||||||
|
// @ID get-notification-dispatch-methods
|
||||||
|
// @Security CoderSessionToken
|
||||||
|
// @Produce json
|
||||||
|
// @Tags Notifications
|
||||||
|
// @Success 200 {array} codersdk.NotificationMethodsResponse
|
||||||
|
// @Router /notifications/dispatch-methods [get]
|
||||||
|
func (api *API) notificationDispatchMethods(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
var methods []string
|
||||||
|
for _, nm := range database.AllNotificationMethodValues() {
|
||||||
|
methods = append(methods, string(nm))
|
||||||
|
}
|
||||||
|
|
||||||
|
httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.NotificationMethodsResponse{
|
||||||
|
AvailableNotificationMethods: methods,
|
||||||
|
DefaultNotificationMethod: api.DeploymentValues.Notifications.Method.Value(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Get user notification preferences
|
||||||
|
// @ID get-user-notification-preferences
|
||||||
|
// @Security CoderSessionToken
|
||||||
|
// @Produce json
|
||||||
|
// @Tags Notifications
|
||||||
|
// @Param user path string true "User ID, name, or me"
|
||||||
|
// @Success 200 {array} codersdk.NotificationPreference
|
||||||
|
// @Router /users/{user}/notifications/preferences [get]
|
||||||
|
func (api *API) userNotificationPreferences(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
ctx = r.Context()
|
||||||
|
user = httpmw.UserParam(r)
|
||||||
|
logger = api.Logger.Named("notifications.preferences").With(slog.F("user_id", user.ID))
|
||||||
|
)
|
||||||
|
|
||||||
|
prefs, err := api.Database.GetUserNotificationPreferences(ctx, user.ID)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(ctx, "failed to retrieve preferences", slog.Error(err))
|
||||||
|
|
||||||
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||||
|
Message: "Failed to retrieve user notification preferences.",
|
||||||
|
Detail: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out := convertNotificationPreferences(prefs)
|
||||||
|
httpapi.Write(ctx, rw, http.StatusOK, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Update user notification preferences
|
||||||
|
// @ID update-user-notification-preferences
|
||||||
|
// @Security CoderSessionToken
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Tags Notifications
|
||||||
|
// @Param request body codersdk.UpdateUserNotificationPreferences true "Preferences"
|
||||||
|
// @Param user path string true "User ID, name, or me"
|
||||||
|
// @Success 200 {array} codersdk.NotificationPreference
|
||||||
|
// @Router /users/{user}/notifications/preferences [put]
|
||||||
|
func (api *API) putUserNotificationPreferences(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
ctx = r.Context()
|
||||||
|
user = httpmw.UserParam(r)
|
||||||
|
logger = api.Logger.Named("notifications.preferences").With(slog.F("user_id", user.ID))
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parse request.
|
||||||
|
var prefs codersdk.UpdateUserNotificationPreferences
|
||||||
|
if !httpapi.Read(ctx, rw, r, &prefs) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build query params.
|
||||||
|
input := database.UpdateUserNotificationPreferencesParams{
|
||||||
|
UserID: user.ID,
|
||||||
|
NotificationTemplateIds: make([]uuid.UUID, 0, len(prefs.TemplateDisabledMap)),
|
||||||
|
Disableds: make([]bool, 0, len(prefs.TemplateDisabledMap)),
|
||||||
|
}
|
||||||
|
for tmplID, disabled := range prefs.TemplateDisabledMap {
|
||||||
|
id, err := uuid.Parse(tmplID)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn(ctx, "failed to parse notification template UUID", slog.F("input", tmplID), slog.Error(err))
|
||||||
|
|
||||||
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||||
|
Message: "Unable to parse notification template UUID.",
|
||||||
|
Detail: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
input.NotificationTemplateIds = append(input.NotificationTemplateIds, id)
|
||||||
|
input.Disableds = append(input.Disableds, disabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update preferences with params.
|
||||||
|
updated, err := api.Database.UpdateUserNotificationPreferences(ctx, input)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(ctx, "failed to update preferences", slog.Error(err))
|
||||||
|
|
||||||
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||||
|
Message: "Failed to update user notifications preferences.",
|
||||||
|
Detail: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preferences updated, now fetch all preferences belonging to this user.
|
||||||
|
logger.Info(ctx, "updated preferences", slog.F("count", updated))
|
||||||
|
|
||||||
|
userPrefs, err := api.Database.GetUserNotificationPreferences(ctx, user.ID)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(ctx, "failed to retrieve preferences", slog.Error(err))
|
||||||
|
|
||||||
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||||
|
Message: "Failed to retrieve user notifications preferences.",
|
||||||
|
Detail: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out := convertNotificationPreferences(userPrefs)
|
||||||
|
httpapi.Write(ctx, rw, http.StatusOK, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertNotificationTemplates(in []database.NotificationTemplate) (out []codersdk.NotificationTemplate) {
|
||||||
|
for _, tmpl := range in {
|
||||||
|
out = append(out, codersdk.NotificationTemplate{
|
||||||
|
ID: tmpl.ID,
|
||||||
|
Name: tmpl.Name,
|
||||||
|
TitleTemplate: tmpl.TitleTemplate,
|
||||||
|
BodyTemplate: tmpl.BodyTemplate,
|
||||||
|
Actions: string(tmpl.Actions),
|
||||||
|
Group: tmpl.Group.String,
|
||||||
|
Method: string(tmpl.Method.NotificationMethod),
|
||||||
|
Kind: string(tmpl.Kind),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertNotificationPreferences(in []database.NotificationPreference) (out []codersdk.NotificationPreference) {
|
||||||
|
for _, pref := range in {
|
||||||
|
out = append(out, codersdk.NotificationPreference{
|
||||||
|
NotificationTemplateID: pref.NotificationTemplateID,
|
||||||
|
Disabled: pref.Disabled,
|
||||||
|
UpdatedAt: pref.UpdatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package notifications
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -16,14 +17,13 @@ import (
|
|||||||
"github.com/coder/coder/v2/codersdk"
|
"github.com/coder/coder/v2/codersdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ErrCannotEnqueueDisabledNotification = xerrors.New("user has disabled this notification")
|
||||||
|
|
||||||
type StoreEnqueuer struct {
|
type StoreEnqueuer struct {
|
||||||
store Store
|
store Store
|
||||||
log slog.Logger
|
log slog.Logger
|
||||||
|
|
||||||
// TODO: expand this to allow for each notification to have custom delivery methods, or multiple, or none.
|
defaultMethod database.NotificationMethod
|
||||||
// For example, Larry might want email notifications for "workspace deleted" notifications, but Harry wants
|
|
||||||
// Slack notifications, and Mary doesn't want any.
|
|
||||||
method database.NotificationMethod
|
|
||||||
// helpers holds a map of template funcs which are used when rendering templates. These need to be passed in because
|
// helpers holds a map of template funcs which are used when rendering templates. These need to be passed in because
|
||||||
// the template funcs will return values which are inappropriately encapsulated in this struct.
|
// the template funcs will return values which are inappropriately encapsulated in this struct.
|
||||||
helpers template.FuncMap
|
helpers template.FuncMap
|
||||||
@@ -37,17 +37,31 @@ func NewStoreEnqueuer(cfg codersdk.NotificationsConfig, store Store, helpers tem
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &StoreEnqueuer{
|
return &StoreEnqueuer{
|
||||||
store: store,
|
store: store,
|
||||||
log: log,
|
log: log,
|
||||||
method: method,
|
defaultMethod: method,
|
||||||
helpers: helpers,
|
helpers: helpers,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enqueue queues a notification message for later delivery.
|
// Enqueue queues a notification message for later delivery.
|
||||||
// Messages will be dequeued by a notifier later and dispatched.
|
// Messages will be dequeued by a notifier later and dispatched.
|
||||||
func (s *StoreEnqueuer) Enqueue(ctx context.Context, userID, templateID uuid.UUID, labels map[string]string, createdBy string, targets ...uuid.UUID) (*uuid.UUID, error) {
|
func (s *StoreEnqueuer) Enqueue(ctx context.Context, userID, templateID uuid.UUID, labels map[string]string, createdBy string, targets ...uuid.UUID) (*uuid.UUID, error) {
|
||||||
payload, err := s.buildPayload(ctx, userID, templateID, labels)
|
metadata, err := s.store.FetchNewMessageMetadata(ctx, database.FetchNewMessageMetadataParams{
|
||||||
|
UserID: userID,
|
||||||
|
NotificationTemplateID: templateID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.log.Warn(ctx, "failed to fetch message metadata", slog.F("template_id", templateID), slog.F("user_id", userID), slog.Error(err))
|
||||||
|
return nil, xerrors.Errorf("new message metadata: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchMethod := s.defaultMethod
|
||||||
|
if metadata.CustomMethod.Valid {
|
||||||
|
dispatchMethod = metadata.CustomMethod.NotificationMethod
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := s.buildPayload(metadata, labels)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.Warn(ctx, "failed to build payload", slog.F("template_id", templateID), slog.F("user_id", userID), slog.Error(err))
|
s.log.Warn(ctx, "failed to build payload", slog.F("template_id", templateID), slog.F("user_id", userID), slog.Error(err))
|
||||||
return nil, xerrors.Errorf("enqueue notification (payload build): %w", err)
|
return nil, xerrors.Errorf("enqueue notification (payload build): %w", err)
|
||||||
@@ -63,12 +77,21 @@ func (s *StoreEnqueuer) Enqueue(ctx context.Context, userID, templateID uuid.UUI
|
|||||||
ID: id,
|
ID: id,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
NotificationTemplateID: templateID,
|
NotificationTemplateID: templateID,
|
||||||
Method: s.method,
|
Method: dispatchMethod,
|
||||||
Payload: input,
|
Payload: input,
|
||||||
Targets: targets,
|
Targets: targets,
|
||||||
CreatedBy: createdBy,
|
CreatedBy: createdBy,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// We have a trigger on the notification_messages table named `inhibit_enqueue_if_disabled` which prevents messages
|
||||||
|
// from being enqueued if the user has disabled them via notification_preferences. The trigger will fail the insertion
|
||||||
|
// with the message "cannot enqueue message: user has disabled this notification".
|
||||||
|
//
|
||||||
|
// This is more efficient than fetching the user's preferences for each enqueue, and centralizes the business logic.
|
||||||
|
if strings.Contains(err.Error(), ErrCannotEnqueueDisabledNotification.Error()) {
|
||||||
|
return nil, ErrCannotEnqueueDisabledNotification
|
||||||
|
}
|
||||||
|
|
||||||
s.log.Warn(ctx, "failed to enqueue notification", slog.F("template_id", templateID), slog.F("input", input), slog.Error(err))
|
s.log.Warn(ctx, "failed to enqueue notification", slog.F("template_id", templateID), slog.F("input", input), slog.Error(err))
|
||||||
return nil, xerrors.Errorf("enqueue notification: %w", err)
|
return nil, xerrors.Errorf("enqueue notification: %w", err)
|
||||||
}
|
}
|
||||||
@@ -80,15 +103,7 @@ func (s *StoreEnqueuer) Enqueue(ctx context.Context, userID, templateID uuid.UUI
|
|||||||
// buildPayload creates the payload that the notification will for variable substitution and/or routing.
|
// buildPayload creates the payload that the notification will for variable substitution and/or routing.
|
||||||
// The payload contains information about the recipient, the event that triggered the notification, and any subsequent
|
// The payload contains information about the recipient, the event that triggered the notification, and any subsequent
|
||||||
// actions which can be taken by the recipient.
|
// actions which can be taken by the recipient.
|
||||||
func (s *StoreEnqueuer) buildPayload(ctx context.Context, userID, templateID uuid.UUID, labels map[string]string) (*types.MessagePayload, error) {
|
func (s *StoreEnqueuer) buildPayload(metadata database.FetchNewMessageMetadataRow, labels map[string]string) (*types.MessagePayload, error) {
|
||||||
metadata, err := s.store.FetchNewMessageMetadata(ctx, database.FetchNewMessageMetadataParams{
|
|
||||||
UserID: userID,
|
|
||||||
NotificationTemplateID: templateID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, xerrors.Errorf("new message metadata: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
payload := types.MessagePayload{
|
payload := types.MessagePayload{
|
||||||
Version: "1.0",
|
Version: "1.0",
|
||||||
|
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ func (m *Manager) loop(ctx context.Context) error {
|
|||||||
var eg errgroup.Group
|
var eg errgroup.Group
|
||||||
|
|
||||||
// Create a notifier to run concurrently, which will handle dequeueing and dispatching notifications.
|
// Create a notifier to run concurrently, which will handle dequeueing and dispatching notifications.
|
||||||
m.notifier = newNotifier(m.cfg, uuid.New(), m.log, m.store, m.handlers, m.method, m.metrics)
|
m.notifier = newNotifier(m.cfg, uuid.New(), m.log, m.store, m.handlers, m.metrics)
|
||||||
eg.Go(func() error {
|
eg.Go(func() error {
|
||||||
return m.notifier.run(ctx, m.success, m.failure)
|
return m.notifier.run(ctx, m.success, m.failure)
|
||||||
})
|
})
|
||||||
@@ -249,15 +249,24 @@ func (m *Manager) syncUpdates(ctx context.Context) {
|
|||||||
for i := 0; i < nFailure; i++ {
|
for i := 0; i < nFailure; i++ {
|
||||||
res := <-m.failure
|
res := <-m.failure
|
||||||
|
|
||||||
status := database.NotificationMessageStatusPermanentFailure
|
var (
|
||||||
if res.retryable {
|
reason string
|
||||||
|
status database.NotificationMessageStatus
|
||||||
|
)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case res.retryable:
|
||||||
status = database.NotificationMessageStatusTemporaryFailure
|
status = database.NotificationMessageStatusTemporaryFailure
|
||||||
|
case res.inhibited:
|
||||||
|
status = database.NotificationMessageStatusInhibited
|
||||||
|
reason = "disabled by user"
|
||||||
|
default:
|
||||||
|
status = database.NotificationMessageStatusPermanentFailure
|
||||||
}
|
}
|
||||||
|
|
||||||
failureParams.IDs = append(failureParams.IDs, res.msg)
|
failureParams.IDs = append(failureParams.IDs, res.msg)
|
||||||
failureParams.FailedAts = append(failureParams.FailedAts, res.ts)
|
failureParams.FailedAts = append(failureParams.FailedAts, res.ts)
|
||||||
failureParams.Statuses = append(failureParams.Statuses, status)
|
failureParams.Statuses = append(failureParams.Statuses, status)
|
||||||
var reason string
|
|
||||||
if res.err != nil {
|
if res.err != nil {
|
||||||
reason = res.err.Error()
|
reason = res.err.Error()
|
||||||
}
|
}
|
||||||
@@ -367,4 +376,5 @@ type dispatchResult struct {
|
|||||||
ts time.Time
|
ts time.Time
|
||||||
err error
|
err error
|
||||||
retryable bool
|
retryable bool
|
||||||
|
inhibited bool
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -339,6 +339,81 @@ func TestInflightDispatchesMetric(t *testing.T) {
|
|||||||
}, testutil.WaitShort, testutil.IntervalFast)
|
}, testutil.WaitShort, testutil.IntervalFast)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCustomMethodMetricCollection(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// SETUP
|
||||||
|
if !dbtestutil.WillUsePostgres() {
|
||||||
|
// UpdateNotificationTemplateMethodByID only makes sense with a real database.
|
||||||
|
t.Skip("This test requires postgres; it relies on business-logic only implemented in the database")
|
||||||
|
}
|
||||||
|
ctx, logger, store := setup(t)
|
||||||
|
|
||||||
|
var (
|
||||||
|
reg = prometheus.NewRegistry()
|
||||||
|
metrics = notifications.NewMetrics(reg)
|
||||||
|
template = notifications.TemplateWorkspaceDeleted
|
||||||
|
anotherTemplate = notifications.TemplateWorkspaceDormant
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
customMethod = database.NotificationMethodWebhook
|
||||||
|
defaultMethod = database.NotificationMethodSmtp
|
||||||
|
)
|
||||||
|
|
||||||
|
// GIVEN: a template whose notification method differs from the default.
|
||||||
|
out, err := store.UpdateNotificationTemplateMethodByID(ctx, database.UpdateNotificationTemplateMethodByIDParams{
|
||||||
|
ID: template,
|
||||||
|
Method: database.NullNotificationMethod{NotificationMethod: customMethod, Valid: true},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, customMethod, out.Method.NotificationMethod)
|
||||||
|
|
||||||
|
// WHEN: two notifications (each with different templates) are enqueued.
|
||||||
|
cfg := defaultNotificationsConfig(defaultMethod)
|
||||||
|
mgr, err := notifications.NewManager(cfg, store, metrics, logger.Named("manager"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
assert.NoError(t, mgr.Stop(ctx))
|
||||||
|
})
|
||||||
|
|
||||||
|
smtpHandler := &fakeHandler{}
|
||||||
|
webhookHandler := &fakeHandler{}
|
||||||
|
mgr.WithHandlers(map[database.NotificationMethod]notifications.Handler{
|
||||||
|
defaultMethod: smtpHandler,
|
||||||
|
customMethod: webhookHandler,
|
||||||
|
})
|
||||||
|
|
||||||
|
enq, err := notifications.NewStoreEnqueuer(cfg, store, defaultHelpers(), logger.Named("enqueuer"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
user := createSampleUser(t, store)
|
||||||
|
|
||||||
|
_, err = enq.Enqueue(ctx, user.ID, template, map[string]string{"type": "success"}, "test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = enq.Enqueue(ctx, user.ID, anotherTemplate, map[string]string{"type": "success"}, "test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
mgr.Run(ctx)
|
||||||
|
|
||||||
|
// THEN: the fake handlers to "dispatch" the notifications.
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
smtpHandler.mu.RLock()
|
||||||
|
webhookHandler.mu.RLock()
|
||||||
|
defer smtpHandler.mu.RUnlock()
|
||||||
|
defer webhookHandler.mu.RUnlock()
|
||||||
|
|
||||||
|
return len(smtpHandler.succeeded) == 1 && len(smtpHandler.failed) == 0 &&
|
||||||
|
len(webhookHandler.succeeded) == 1 && len(webhookHandler.failed) == 0
|
||||||
|
}, testutil.WaitShort, testutil.IntervalFast)
|
||||||
|
|
||||||
|
// THEN: we should have metric series for both the default and custom notification methods.
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
return promtest.ToFloat64(metrics.DispatchAttempts.WithLabelValues(string(defaultMethod), anotherTemplate.String(), notifications.ResultSuccess)) > 0 &&
|
||||||
|
promtest.ToFloat64(metrics.DispatchAttempts.WithLabelValues(string(customMethod), template.String(), notifications.ResultSuccess)) > 0
|
||||||
|
}, testutil.WaitShort, testutil.IntervalFast)
|
||||||
|
}
|
||||||
|
|
||||||
// hasMatchingFingerprint checks if the given metric's series fingerprint matches the reference fingerprint.
|
// hasMatchingFingerprint checks if the given metric's series fingerprint matches the reference fingerprint.
|
||||||
func hasMatchingFingerprint(metric *dto.Metric, fp model.Fingerprint) bool {
|
func hasMatchingFingerprint(metric *dto.Metric, fp model.Fingerprint) bool {
|
||||||
return fingerprintLabelPairs(metric.Label) == fp
|
return fingerprintLabelPairs(metric.Label) == fp
|
||||||
|
|||||||
@@ -604,7 +604,7 @@ func TestNotifierPaused(t *testing.T) {
|
|||||||
}, testutil.WaitShort, testutil.IntervalFast)
|
}, testutil.WaitShort, testutil.IntervalFast)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNotifcationTemplatesBody(t *testing.T) {
|
func TestNotificationTemplatesBody(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
if !dbtestutil.WillUsePostgres() {
|
if !dbtestutil.WillUsePostgres() {
|
||||||
@@ -705,6 +705,194 @@ func TestNotifcationTemplatesBody(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestDisabledBeforeEnqueue ensures that notifications cannot be enqueued once a user has disabled that notification template
|
||||||
|
func TestDisabledBeforeEnqueue(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// SETUP
|
||||||
|
if !dbtestutil.WillUsePostgres() {
|
||||||
|
t.Skip("This test requires postgres; it is testing business-logic implemented in the database")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, logger, db := setup(t)
|
||||||
|
|
||||||
|
// GIVEN: an enqueuer & a sample user
|
||||||
|
cfg := defaultNotificationsConfig(database.NotificationMethodSmtp)
|
||||||
|
enq, err := notifications.NewStoreEnqueuer(cfg, db, defaultHelpers(), logger.Named("enqueuer"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
user := createSampleUser(t, db)
|
||||||
|
|
||||||
|
// WHEN: the user has a preference set to not receive the "workspace deleted" notification
|
||||||
|
templateID := notifications.TemplateWorkspaceDeleted
|
||||||
|
n, err := db.UpdateUserNotificationPreferences(ctx, database.UpdateUserNotificationPreferencesParams{
|
||||||
|
UserID: user.ID,
|
||||||
|
NotificationTemplateIds: []uuid.UUID{templateID},
|
||||||
|
Disableds: []bool{true},
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "failed to set preferences")
|
||||||
|
require.EqualValues(t, 1, n, "unexpected number of affected rows")
|
||||||
|
|
||||||
|
// THEN: enqueuing the "workspace deleted" notification should fail with an error
|
||||||
|
_, err = enq.Enqueue(ctx, user.ID, templateID, map[string]string{}, "test")
|
||||||
|
require.ErrorIs(t, err, notifications.ErrCannotEnqueueDisabledNotification, "enqueueing did not fail with expected error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDisabledAfterEnqueue ensures that notifications enqueued before a notification template was disabled will not be
|
||||||
|
// sent, and will instead be marked as "inhibited".
|
||||||
|
func TestDisabledAfterEnqueue(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// SETUP
|
||||||
|
if !dbtestutil.WillUsePostgres() {
|
||||||
|
t.Skip("This test requires postgres; it is testing business-logic implemented in the database")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, logger, db := setup(t)
|
||||||
|
|
||||||
|
method := database.NotificationMethodSmtp
|
||||||
|
cfg := defaultNotificationsConfig(method)
|
||||||
|
|
||||||
|
mgr, err := notifications.NewManager(cfg, db, createMetrics(), logger.Named("manager"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
assert.NoError(t, mgr.Stop(ctx))
|
||||||
|
})
|
||||||
|
|
||||||
|
enq, err := notifications.NewStoreEnqueuer(cfg, db, defaultHelpers(), logger.Named("enqueuer"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
user := createSampleUser(t, db)
|
||||||
|
|
||||||
|
// GIVEN: a notification is enqueued which has not (yet) been disabled
|
||||||
|
templateID := notifications.TemplateWorkspaceDeleted
|
||||||
|
msgID, err := enq.Enqueue(ctx, user.ID, templateID, map[string]string{}, "test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Disable the notification template.
|
||||||
|
n, err := db.UpdateUserNotificationPreferences(ctx, database.UpdateUserNotificationPreferencesParams{
|
||||||
|
UserID: user.ID,
|
||||||
|
NotificationTemplateIds: []uuid.UUID{templateID},
|
||||||
|
Disableds: []bool{true},
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "failed to set preferences")
|
||||||
|
require.EqualValues(t, 1, n, "unexpected number of affected rows")
|
||||||
|
|
||||||
|
// WHEN: running the manager to trigger dequeueing of (now-disabled) messages
|
||||||
|
mgr.Run(ctx)
|
||||||
|
|
||||||
|
// THEN: the message should not be sent, and must be set to "inhibited"
|
||||||
|
require.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||||
|
m, err := db.GetNotificationMessagesByStatus(ctx, database.GetNotificationMessagesByStatusParams{
|
||||||
|
Status: database.NotificationMessageStatusInhibited,
|
||||||
|
Limit: 10,
|
||||||
|
})
|
||||||
|
assert.NoError(ct, err)
|
||||||
|
if assert.Equal(ct, len(m), 1) {
|
||||||
|
assert.Equal(ct, m[0].ID.String(), msgID.String())
|
||||||
|
assert.Contains(ct, m[0].StatusReason.String, "disabled by user")
|
||||||
|
}
|
||||||
|
}, testutil.WaitLong, testutil.IntervalFast, "did not find the expected inhibited message")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCustomNotificationMethod(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// SETUP
|
||||||
|
if !dbtestutil.WillUsePostgres() {
|
||||||
|
t.Skip("This test requires postgres; it relies on business-logic only implemented in the database")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, logger, db := setup(t)
|
||||||
|
|
||||||
|
received := make(chan uuid.UUID, 1)
|
||||||
|
|
||||||
|
// SETUP:
|
||||||
|
// Start mock server to simulate webhook endpoint.
|
||||||
|
mockWebhookSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var payload dispatch.WebhookPayload
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&payload)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
received <- payload.MsgID
|
||||||
|
close(received)
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, err = w.Write([]byte("noted."))
|
||||||
|
require.NoError(t, err)
|
||||||
|
}))
|
||||||
|
defer mockWebhookSrv.Close()
|
||||||
|
|
||||||
|
// Start mock SMTP server.
|
||||||
|
mockSMTPSrv := smtpmock.New(smtpmock.ConfigurationAttr{
|
||||||
|
LogToStdout: false,
|
||||||
|
LogServerActivity: true,
|
||||||
|
})
|
||||||
|
require.NoError(t, mockSMTPSrv.Start())
|
||||||
|
t.Cleanup(func() {
|
||||||
|
assert.NoError(t, mockSMTPSrv.Stop())
|
||||||
|
})
|
||||||
|
|
||||||
|
endpoint, err := url.Parse(mockWebhookSrv.URL)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// GIVEN: a notification template which has a method explicitly set
|
||||||
|
var (
|
||||||
|
template = notifications.TemplateWorkspaceDormant
|
||||||
|
defaultMethod = database.NotificationMethodSmtp
|
||||||
|
customMethod = database.NotificationMethodWebhook
|
||||||
|
)
|
||||||
|
out, err := db.UpdateNotificationTemplateMethodByID(ctx, database.UpdateNotificationTemplateMethodByIDParams{
|
||||||
|
ID: template,
|
||||||
|
Method: database.NullNotificationMethod{NotificationMethod: customMethod, Valid: true},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, customMethod, out.Method.NotificationMethod)
|
||||||
|
|
||||||
|
// GIVEN: a manager configured with multiple dispatch methods
|
||||||
|
cfg := defaultNotificationsConfig(defaultMethod)
|
||||||
|
cfg.SMTP = codersdk.NotificationsEmailConfig{
|
||||||
|
From: "danny@coder.com",
|
||||||
|
Hello: "localhost",
|
||||||
|
Smarthost: serpent.HostPort{Host: "localhost", Port: fmt.Sprintf("%d", mockSMTPSrv.PortNumber())},
|
||||||
|
}
|
||||||
|
cfg.Webhook = codersdk.NotificationsWebhookConfig{
|
||||||
|
Endpoint: *serpent.URLOf(endpoint),
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr, err := notifications.NewManager(cfg, db, createMetrics(), logger.Named("manager"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_ = mgr.Stop(ctx)
|
||||||
|
})
|
||||||
|
|
||||||
|
enq, err := notifications.NewStoreEnqueuer(cfg, db, defaultHelpers(), logger)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// WHEN: a notification of that template is enqueued, it should be delivered with the configured method - not the default.
|
||||||
|
user := createSampleUser(t, db)
|
||||||
|
msgID, err := enq.Enqueue(ctx, user.ID, template, map[string]string{}, "test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// THEN: the notification should be received by the custom dispatch method
|
||||||
|
mgr.Run(ctx)
|
||||||
|
|
||||||
|
receivedMsgID := testutil.RequireRecvCtx(ctx, t, received)
|
||||||
|
require.Equal(t, msgID.String(), receivedMsgID.String())
|
||||||
|
|
||||||
|
// Ensure no messages received by default method (SMTP):
|
||||||
|
msgs := mockSMTPSrv.MessagesAndPurge()
|
||||||
|
require.Len(t, msgs, 0)
|
||||||
|
|
||||||
|
// Enqueue a notification which does not have a custom method set to ensure default works correctly.
|
||||||
|
msgID, err = enq.Enqueue(ctx, user.ID, notifications.TemplateWorkspaceDeleted, map[string]string{}, "test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||||
|
msgs := mockSMTPSrv.MessagesAndPurge()
|
||||||
|
if assert.Len(ct, msgs, 1) {
|
||||||
|
assert.Contains(ct, msgs[0].MsgRequest(), fmt.Sprintf("Message-Id: %s", msgID))
|
||||||
|
}
|
||||||
|
}, testutil.WaitLong, testutil.IntervalFast)
|
||||||
|
}
|
||||||
|
|
||||||
type fakeHandler struct {
|
type fakeHandler struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
succeeded, failed []string
|
succeeded, failed []string
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
|
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||||
"github.com/coder/coder/v2/coderd/notifications/dispatch"
|
"github.com/coder/coder/v2/coderd/notifications/dispatch"
|
||||||
"github.com/coder/coder/v2/coderd/notifications/render"
|
"github.com/coder/coder/v2/coderd/notifications/render"
|
||||||
"github.com/coder/coder/v2/coderd/notifications/types"
|
"github.com/coder/coder/v2/coderd/notifications/types"
|
||||||
@@ -33,12 +34,11 @@ type notifier struct {
|
|||||||
quit chan any
|
quit chan any
|
||||||
done chan any
|
done chan any
|
||||||
|
|
||||||
method database.NotificationMethod
|
|
||||||
handlers map[database.NotificationMethod]Handler
|
handlers map[database.NotificationMethod]Handler
|
||||||
metrics *Metrics
|
metrics *Metrics
|
||||||
}
|
}
|
||||||
|
|
||||||
func newNotifier(cfg codersdk.NotificationsConfig, id uuid.UUID, log slog.Logger, db Store, hr map[database.NotificationMethod]Handler, method database.NotificationMethod, metrics *Metrics) *notifier {
|
func newNotifier(cfg codersdk.NotificationsConfig, id uuid.UUID, log slog.Logger, db Store, hr map[database.NotificationMethod]Handler, metrics *Metrics) *notifier {
|
||||||
return ¬ifier{
|
return ¬ifier{
|
||||||
id: id,
|
id: id,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
@@ -48,7 +48,6 @@ func newNotifier(cfg codersdk.NotificationsConfig, id uuid.UUID, log slog.Logger
|
|||||||
tick: time.NewTicker(cfg.FetchInterval.Value()),
|
tick: time.NewTicker(cfg.FetchInterval.Value()),
|
||||||
store: db,
|
store: db,
|
||||||
handlers: hr,
|
handlers: hr,
|
||||||
method: method,
|
|
||||||
metrics: metrics,
|
metrics: metrics,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -144,6 +143,12 @@ func (n *notifier) process(ctx context.Context, success chan<- dispatchResult, f
|
|||||||
|
|
||||||
var eg errgroup.Group
|
var eg errgroup.Group
|
||||||
for _, msg := range msgs {
|
for _, msg := range msgs {
|
||||||
|
// If a notification template has been disabled by the user after a notification was enqueued, mark it as inhibited
|
||||||
|
if msg.Disabled {
|
||||||
|
failure <- n.newInhibitedDispatch(msg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// A message failing to be prepared correctly should not affect other messages.
|
// A message failing to be prepared correctly should not affect other messages.
|
||||||
deliverFn, err := n.prepare(ctx, msg)
|
deliverFn, err := n.prepare(ctx, msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -234,17 +239,17 @@ func (n *notifier) deliver(ctx context.Context, msg database.AcquireNotification
|
|||||||
logger := n.log.With(slog.F("msg_id", msg.ID), slog.F("method", msg.Method), slog.F("attempt", msg.AttemptCount+1))
|
logger := n.log.With(slog.F("msg_id", msg.ID), slog.F("method", msg.Method), slog.F("attempt", msg.AttemptCount+1))
|
||||||
|
|
||||||
if msg.AttemptCount > 0 {
|
if msg.AttemptCount > 0 {
|
||||||
n.metrics.RetryCount.WithLabelValues(string(n.method), msg.TemplateID.String()).Inc()
|
n.metrics.RetryCount.WithLabelValues(string(msg.Method), msg.TemplateID.String()).Inc()
|
||||||
}
|
}
|
||||||
|
|
||||||
n.metrics.InflightDispatches.WithLabelValues(string(n.method), msg.TemplateID.String()).Inc()
|
n.metrics.InflightDispatches.WithLabelValues(string(msg.Method), msg.TemplateID.String()).Inc()
|
||||||
n.metrics.QueuedSeconds.WithLabelValues(string(n.method)).Observe(msg.QueuedSeconds)
|
n.metrics.QueuedSeconds.WithLabelValues(string(msg.Method)).Observe(msg.QueuedSeconds)
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
retryable, err := deliver(ctx, msg.ID)
|
retryable, err := deliver(ctx, msg.ID)
|
||||||
|
|
||||||
n.metrics.DispatcherSendSeconds.WithLabelValues(string(n.method)).Observe(time.Since(start).Seconds())
|
n.metrics.DispatcherSendSeconds.WithLabelValues(string(msg.Method)).Observe(time.Since(start).Seconds())
|
||||||
n.metrics.InflightDispatches.WithLabelValues(string(n.method), msg.TemplateID.String()).Dec()
|
n.metrics.InflightDispatches.WithLabelValues(string(msg.Method), msg.TemplateID.String()).Dec()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Don't try to accumulate message responses if the context has been canceled.
|
// Don't try to accumulate message responses if the context has been canceled.
|
||||||
@@ -281,12 +286,12 @@ func (n *notifier) deliver(ctx context.Context, msg database.AcquireNotification
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (n *notifier) newSuccessfulDispatch(msg database.AcquireNotificationMessagesRow) dispatchResult {
|
func (n *notifier) newSuccessfulDispatch(msg database.AcquireNotificationMessagesRow) dispatchResult {
|
||||||
n.metrics.DispatchAttempts.WithLabelValues(string(n.method), msg.TemplateID.String(), ResultSuccess).Inc()
|
n.metrics.DispatchAttempts.WithLabelValues(string(msg.Method), msg.TemplateID.String(), ResultSuccess).Inc()
|
||||||
|
|
||||||
return dispatchResult{
|
return dispatchResult{
|
||||||
notifier: n.id,
|
notifier: n.id,
|
||||||
msg: msg.ID,
|
msg: msg.ID,
|
||||||
ts: time.Now(),
|
ts: dbtime.Now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,17 +306,27 @@ func (n *notifier) newFailedDispatch(msg database.AcquireNotificationMessagesRow
|
|||||||
result = ResultPermFail
|
result = ResultPermFail
|
||||||
}
|
}
|
||||||
|
|
||||||
n.metrics.DispatchAttempts.WithLabelValues(string(n.method), msg.TemplateID.String(), result).Inc()
|
n.metrics.DispatchAttempts.WithLabelValues(string(msg.Method), msg.TemplateID.String(), result).Inc()
|
||||||
|
|
||||||
return dispatchResult{
|
return dispatchResult{
|
||||||
notifier: n.id,
|
notifier: n.id,
|
||||||
msg: msg.ID,
|
msg: msg.ID,
|
||||||
ts: time.Now(),
|
ts: dbtime.Now(),
|
||||||
err: err,
|
err: err,
|
||||||
retryable: retryable,
|
retryable: retryable,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *notifier) newInhibitedDispatch(msg database.AcquireNotificationMessagesRow) dispatchResult {
|
||||||
|
return dispatchResult{
|
||||||
|
notifier: n.id,
|
||||||
|
msg: msg.ID,
|
||||||
|
ts: dbtime.Now(),
|
||||||
|
retryable: false,
|
||||||
|
inhibited: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// stop stops the notifier from processing any new notifications.
|
// stop stops the notifier from processing any new notifications.
|
||||||
// This is a graceful stop, so any in-flight notifications will be completed before the notifier stops.
|
// This is a graceful stop, so any in-flight notifications will be completed before the notifier stops.
|
||||||
// Once a notifier has stopped, it cannot be restarted.
|
// Once a notifier has stopped, it cannot be restarted.
|
||||||
|
|||||||
@@ -5,19 +5,34 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
|
||||||
|
"github.com/coder/serpent"
|
||||||
|
|
||||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||||
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
|
"github.com/coder/coder/v2/coderd/notifications"
|
||||||
"github.com/coder/coder/v2/codersdk"
|
"github.com/coder/coder/v2/codersdk"
|
||||||
"github.com/coder/coder/v2/testutil"
|
"github.com/coder/coder/v2/testutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func createOpts(t *testing.T) *coderdtest.Options {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
dt := coderdtest.DeploymentValues(t)
|
||||||
|
dt.Experiments = []string{string(codersdk.ExperimentNotifications)}
|
||||||
|
return &coderdtest.Options{
|
||||||
|
DeploymentValues: dt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUpdateNotificationsSettings(t *testing.T) {
|
func TestUpdateNotificationsSettings(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
t.Run("Permissions denied", func(t *testing.T) {
|
t.Run("Permissions denied", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
api := coderdtest.New(t, nil)
|
api := coderdtest.New(t, createOpts(t))
|
||||||
firstUser := coderdtest.CreateFirstUser(t, api)
|
firstUser := coderdtest.CreateFirstUser(t, api)
|
||||||
anotherClient, _ := coderdtest.CreateAnotherUser(t, api, firstUser.OrganizationID)
|
anotherClient, _ := coderdtest.CreateAnotherUser(t, api, firstUser.OrganizationID)
|
||||||
|
|
||||||
@@ -41,7 +56,7 @@ func TestUpdateNotificationsSettings(t *testing.T) {
|
|||||||
t.Run("Settings modified", func(t *testing.T) {
|
t.Run("Settings modified", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
client := coderdtest.New(t, nil)
|
client := coderdtest.New(t, createOpts(t))
|
||||||
_ = coderdtest.CreateFirstUser(t, client)
|
_ = coderdtest.CreateFirstUser(t, client)
|
||||||
|
|
||||||
// given
|
// given
|
||||||
@@ -65,7 +80,7 @@ func TestUpdateNotificationsSettings(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
// Empty state: notifications Settings are undefined now (default).
|
// Empty state: notifications Settings are undefined now (default).
|
||||||
client := coderdtest.New(t, nil)
|
client := coderdtest.New(t, createOpts(t))
|
||||||
_ = coderdtest.CreateFirstUser(t, client)
|
_ = coderdtest.CreateFirstUser(t, client)
|
||||||
ctx := testutil.Context(t, testutil.WaitShort)
|
ctx := testutil.Context(t, testutil.WaitShort)
|
||||||
|
|
||||||
@@ -93,3 +108,213 @@ func TestUpdateNotificationsSettings(t *testing.T) {
|
|||||||
require.Equal(t, expected.NotifierPaused, actual.NotifierPaused)
|
require.Equal(t, expected.NotifierPaused, actual.NotifierPaused)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNotificationPreferences(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("Initial state", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
|
api := coderdtest.New(t, createOpts(t))
|
||||||
|
firstUser := coderdtest.CreateFirstUser(t, api)
|
||||||
|
|
||||||
|
// Given: a member in its initial state.
|
||||||
|
memberClient, member := coderdtest.CreateAnotherUser(t, api, firstUser.OrganizationID)
|
||||||
|
|
||||||
|
// When: calling the API.
|
||||||
|
prefs, err := memberClient.GetUserNotificationPreferences(ctx, member.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Then: no preferences will be returned.
|
||||||
|
require.Len(t, prefs, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Insufficient permissions", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
|
api := coderdtest.New(t, createOpts(t))
|
||||||
|
firstUser := coderdtest.CreateFirstUser(t, api)
|
||||||
|
|
||||||
|
// Given: 2 members.
|
||||||
|
_, member1 := coderdtest.CreateAnotherUser(t, api, firstUser.OrganizationID)
|
||||||
|
member2Client, _ := coderdtest.CreateAnotherUser(t, api, firstUser.OrganizationID)
|
||||||
|
|
||||||
|
// When: attempting to retrieve the preferences of another member.
|
||||||
|
_, err := member2Client.GetUserNotificationPreferences(ctx, member1.ID)
|
||||||
|
|
||||||
|
// Then: the API should reject the request.
|
||||||
|
var sdkError *codersdk.Error
|
||||||
|
require.Error(t, err)
|
||||||
|
require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error")
|
||||||
|
// NOTE: ExtractUserParam gets in the way here, and returns a 400 Bad Request instead of a 403 Forbidden.
|
||||||
|
// This is not ideal, and we should probably change this behavior.
|
||||||
|
require.Equal(t, http.StatusBadRequest, sdkError.StatusCode())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Admin may read any users' preferences", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
|
api := coderdtest.New(t, createOpts(t))
|
||||||
|
firstUser := coderdtest.CreateFirstUser(t, api)
|
||||||
|
|
||||||
|
// Given: a member.
|
||||||
|
_, member := coderdtest.CreateAnotherUser(t, api, firstUser.OrganizationID)
|
||||||
|
|
||||||
|
// When: attempting to retrieve the preferences of another member as an admin.
|
||||||
|
prefs, err := api.GetUserNotificationPreferences(ctx, member.ID)
|
||||||
|
|
||||||
|
// Then: the API should not reject the request.
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, prefs, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Admin may update any users' preferences", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
|
api := coderdtest.New(t, createOpts(t))
|
||||||
|
firstUser := coderdtest.CreateFirstUser(t, api)
|
||||||
|
|
||||||
|
// Given: a member.
|
||||||
|
memberClient, member := coderdtest.CreateAnotherUser(t, api, firstUser.OrganizationID)
|
||||||
|
|
||||||
|
// When: attempting to modify and subsequently retrieve the preferences of another member as an admin.
|
||||||
|
prefs, err := api.UpdateUserNotificationPreferences(ctx, member.ID, codersdk.UpdateUserNotificationPreferences{
|
||||||
|
TemplateDisabledMap: map[string]bool{
|
||||||
|
notifications.TemplateWorkspaceMarkedForDeletion.String(): true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Then: the request should succeed and the user should be able to query their own preferences to see the same result.
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, prefs, 1)
|
||||||
|
|
||||||
|
memberPrefs, err := memberClient.GetUserNotificationPreferences(ctx, member.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, memberPrefs, 1)
|
||||||
|
require.Equal(t, prefs[0].NotificationTemplateID, memberPrefs[0].NotificationTemplateID)
|
||||||
|
require.Equal(t, prefs[0].Disabled, memberPrefs[0].Disabled)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Add preferences", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
|
api := coderdtest.New(t, createOpts(t))
|
||||||
|
firstUser := coderdtest.CreateFirstUser(t, api)
|
||||||
|
|
||||||
|
// Given: a member with no preferences.
|
||||||
|
memberClient, member := coderdtest.CreateAnotherUser(t, api, firstUser.OrganizationID)
|
||||||
|
prefs, err := memberClient.GetUserNotificationPreferences(ctx, member.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, prefs, 0)
|
||||||
|
|
||||||
|
// When: attempting to add new preferences.
|
||||||
|
template := notifications.TemplateWorkspaceDeleted
|
||||||
|
prefs, err = memberClient.UpdateUserNotificationPreferences(ctx, member.ID, codersdk.UpdateUserNotificationPreferences{
|
||||||
|
TemplateDisabledMap: map[string]bool{
|
||||||
|
template.String(): true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Then: the returning preferences should be set as expected.
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, prefs, 1)
|
||||||
|
require.Equal(t, prefs[0].NotificationTemplateID, template)
|
||||||
|
require.True(t, prefs[0].Disabled)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Modify preferences", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
|
api := coderdtest.New(t, createOpts(t))
|
||||||
|
firstUser := coderdtest.CreateFirstUser(t, api)
|
||||||
|
|
||||||
|
// Given: a member with preferences.
|
||||||
|
memberClient, member := coderdtest.CreateAnotherUser(t, api, firstUser.OrganizationID)
|
||||||
|
prefs, err := memberClient.UpdateUserNotificationPreferences(ctx, member.ID, codersdk.UpdateUserNotificationPreferences{
|
||||||
|
TemplateDisabledMap: map[string]bool{
|
||||||
|
notifications.TemplateWorkspaceDeleted.String(): true,
|
||||||
|
notifications.TemplateWorkspaceDormant.String(): true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, prefs, 2)
|
||||||
|
|
||||||
|
// When: attempting to modify their preferences.
|
||||||
|
prefs, err = memberClient.UpdateUserNotificationPreferences(ctx, member.ID, codersdk.UpdateUserNotificationPreferences{
|
||||||
|
TemplateDisabledMap: map[string]bool{
|
||||||
|
notifications.TemplateWorkspaceDeleted.String(): true,
|
||||||
|
notifications.TemplateWorkspaceDormant.String(): false, // <--- this one was changed
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, prefs, 2)
|
||||||
|
|
||||||
|
// Then: the modified preferences should be set as expected.
|
||||||
|
var found bool
|
||||||
|
for _, p := range prefs {
|
||||||
|
switch p.NotificationTemplateID {
|
||||||
|
case notifications.TemplateWorkspaceDormant:
|
||||||
|
found = true
|
||||||
|
require.False(t, p.Disabled)
|
||||||
|
case notifications.TemplateWorkspaceDeleted:
|
||||||
|
require.True(t, p.Disabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.True(t, found, "dormant notification preference was not found")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNotificationDispatchMethods(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
defaultOpts := createOpts(t)
|
||||||
|
webhookOpts := createOpts(t)
|
||||||
|
webhookOpts.DeploymentValues.Notifications.Method = serpent.String(database.NotificationMethodWebhook)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
opts *coderdtest.Options
|
||||||
|
expectedDefault string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "default",
|
||||||
|
opts: defaultOpts,
|
||||||
|
expectedDefault: string(database.NotificationMethodSmtp),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-default",
|
||||||
|
opts: webhookOpts,
|
||||||
|
expectedDefault: string(database.NotificationMethodWebhook),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var allMethods []string
|
||||||
|
for _, nm := range database.AllNotificationMethodValues() {
|
||||||
|
allMethods = append(allMethods, string(nm))
|
||||||
|
}
|
||||||
|
slices.Sort(allMethods)
|
||||||
|
|
||||||
|
// nolint:paralleltest // Not since Go v1.22.
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := testutil.Context(t, testutil.WaitShort)
|
||||||
|
api := coderdtest.New(t, tc.opts)
|
||||||
|
_ = coderdtest.CreateFirstUser(t, api)
|
||||||
|
|
||||||
|
resp, err := api.GetNotificationDispatchMethods(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
slices.Sort(resp.AvailableNotificationMethods)
|
||||||
|
require.EqualValues(t, resp.AvailableNotificationMethods, allMethods)
|
||||||
|
require.Equal(t, tc.expectedDefault, resp.DefaultNotificationMethod)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -102,6 +102,22 @@ var (
|
|||||||
Type: "license",
|
Type: "license",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResourceNotificationPreference
|
||||||
|
// Valid Actions
|
||||||
|
// - "ActionRead" :: read notification preferences
|
||||||
|
// - "ActionUpdate" :: update notification preferences
|
||||||
|
ResourceNotificationPreference = Object{
|
||||||
|
Type: "notification_preference",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceNotificationTemplate
|
||||||
|
// Valid Actions
|
||||||
|
// - "ActionRead" :: read notification templates
|
||||||
|
// - "ActionUpdate" :: update notification templates
|
||||||
|
ResourceNotificationTemplate = Object{
|
||||||
|
Type: "notification_template",
|
||||||
|
}
|
||||||
|
|
||||||
// ResourceOauth2App
|
// ResourceOauth2App
|
||||||
// Valid Actions
|
// Valid Actions
|
||||||
// - "ActionCreate" :: make an OAuth2 app.
|
// - "ActionCreate" :: make an OAuth2 app.
|
||||||
@@ -272,6 +288,8 @@ func AllResources() []Objecter {
|
|||||||
ResourceFile,
|
ResourceFile,
|
||||||
ResourceGroup,
|
ResourceGroup,
|
||||||
ResourceLicense,
|
ResourceLicense,
|
||||||
|
ResourceNotificationPreference,
|
||||||
|
ResourceNotificationTemplate,
|
||||||
ResourceOauth2App,
|
ResourceOauth2App,
|
||||||
ResourceOauth2AppCodeToken,
|
ResourceOauth2AppCodeToken,
|
||||||
ResourceOauth2AppSecret,
|
ResourceOauth2AppSecret,
|
||||||
|
|||||||
@@ -255,4 +255,16 @@ var RBACPermissions = map[string]PermissionDefinition{
|
|||||||
ActionDelete: actDef(""),
|
ActionDelete: actDef(""),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"notification_template": {
|
||||||
|
Actions: map[Action]ActionDefinition{
|
||||||
|
ActionRead: actDef("read notification templates"),
|
||||||
|
ActionUpdate: actDef("update notification templates"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"notification_preference": {
|
||||||
|
Actions: map[Action]ActionDefinition{
|
||||||
|
ActionRead: actDef("read notification preferences"),
|
||||||
|
ActionUpdate: actDef("update notification preferences"),
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -590,6 +590,54 @@ func TestRolePermissions(t *testing.T) {
|
|||||||
false: {},
|
false: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Any owner/admin across may access any users' preferences
|
||||||
|
// Members may not access other members' preferences
|
||||||
|
Name: "NotificationPreferencesOwn",
|
||||||
|
Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate},
|
||||||
|
Resource: rbac.ResourceNotificationPreference.WithOwner(currentUser.String()),
|
||||||
|
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||||
|
true: {memberMe, orgMemberMe, owner},
|
||||||
|
false: {
|
||||||
|
userAdmin, orgUserAdmin, templateAdmin,
|
||||||
|
orgAuditor, orgTemplateAdmin,
|
||||||
|
otherOrgMember, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin,
|
||||||
|
orgAdmin, otherOrgAdmin,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Any owner/admin may access notification templates
|
||||||
|
Name: "NotificationTemplates",
|
||||||
|
Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate},
|
||||||
|
Resource: rbac.ResourceNotificationTemplate,
|
||||||
|
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||||
|
true: {owner},
|
||||||
|
false: {
|
||||||
|
memberMe, orgMemberMe, userAdmin, orgUserAdmin, templateAdmin,
|
||||||
|
orgAuditor, orgTemplateAdmin,
|
||||||
|
otherOrgMember, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin,
|
||||||
|
orgAdmin, otherOrgAdmin,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Notification preferences are currently not organization-scoped
|
||||||
|
// Any owner/admin may access any users' preferences
|
||||||
|
// Members may not access other members' preferences
|
||||||
|
Name: "NotificationPreferencesOtherUser",
|
||||||
|
Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate},
|
||||||
|
Resource: rbac.ResourceNotificationPreference.WithOwner(uuid.NewString()), // some other user
|
||||||
|
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||||
|
true: {owner},
|
||||||
|
false: {
|
||||||
|
memberMe, templateAdmin, orgUserAdmin, userAdmin,
|
||||||
|
orgAdmin, orgAuditor, orgTemplateAdmin,
|
||||||
|
otherOrgMember, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin,
|
||||||
|
otherOrgAdmin, orgMemberMe,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
// AnyOrganization tests
|
// AnyOrganization tests
|
||||||
{
|
{
|
||||||
Name: "CreateOrgMember",
|
Name: "CreateOrgMember",
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ const (
|
|||||||
ResourceTypeOAuth2ProviderAppSecret ResourceType = "oauth2_provider_app_secret"
|
ResourceTypeOAuth2ProviderAppSecret ResourceType = "oauth2_provider_app_secret"
|
||||||
ResourceTypeCustomRole ResourceType = "custom_role"
|
ResourceTypeCustomRole ResourceType = "custom_role"
|
||||||
ResourceTypeOrganizationMember = "organization_member"
|
ResourceTypeOrganizationMember = "organization_member"
|
||||||
|
ResourceTypeNotificationTemplate = "notification_template"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r ResourceType) FriendlyString() string {
|
func (r ResourceType) FriendlyString() string {
|
||||||
@@ -75,6 +76,8 @@ func (r ResourceType) FriendlyString() string {
|
|||||||
return "custom role"
|
return "custom role"
|
||||||
case ResourceTypeOrganizationMember:
|
case ResourceTypeOrganizationMember:
|
||||||
return "organization member"
|
return "organization member"
|
||||||
|
case ResourceTypeNotificationTemplate:
|
||||||
|
return "notification template"
|
||||||
default:
|
default:
|
||||||
return "unknown"
|
return "unknown"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,43 @@ package codersdk
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"golang.org/x/xerrors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type NotificationsSettings struct {
|
type NotificationsSettings struct {
|
||||||
NotifierPaused bool `json:"notifier_paused"`
|
NotifierPaused bool `json:"notifier_paused"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NotificationTemplate struct {
|
||||||
|
ID uuid.UUID `json:"id" format:"uuid"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
TitleTemplate string `json:"title_template"`
|
||||||
|
BodyTemplate string `json:"body_template"`
|
||||||
|
Actions string `json:"actions" format:""`
|
||||||
|
Group string `json:"group"`
|
||||||
|
Method string `json:"method"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationMethodsResponse struct {
|
||||||
|
AvailableNotificationMethods []string `json:"available"`
|
||||||
|
DefaultNotificationMethod string `json:"default"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationPreference struct {
|
||||||
|
NotificationTemplateID uuid.UUID `json:"id" format:"uuid"`
|
||||||
|
Disabled bool `json:"disabled"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNotificationsSettings retrieves the notifications settings, which currently just describes whether all
|
||||||
|
// notifications are paused from sending.
|
||||||
func (c *Client) GetNotificationsSettings(ctx context.Context) (NotificationsSettings, error) {
|
func (c *Client) GetNotificationsSettings(ctx context.Context) (NotificationsSettings, error) {
|
||||||
res, err := c.Request(ctx, http.MethodGet, "/api/v2/notifications/settings", nil)
|
res, err := c.Request(ctx, http.MethodGet, "/api/v2/notifications/settings", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -23,6 +53,8 @@ func (c *Client) GetNotificationsSettings(ctx context.Context) (NotificationsSet
|
|||||||
return settings, json.NewDecoder(res.Body).Decode(&settings)
|
return settings, json.NewDecoder(res.Body).Decode(&settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PutNotificationsSettings modifies the notifications settings, which currently just controls whether all
|
||||||
|
// notifications are paused from sending.
|
||||||
func (c *Client) PutNotificationsSettings(ctx context.Context, settings NotificationsSettings) error {
|
func (c *Client) PutNotificationsSettings(ctx context.Context, settings NotificationsSettings) error {
|
||||||
res, err := c.Request(ctx, http.MethodPut, "/api/v2/notifications/settings", settings)
|
res, err := c.Request(ctx, http.MethodPut, "/api/v2/notifications/settings", settings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -38,3 +70,132 @@ func (c *Client) PutNotificationsSettings(ctx context.Context, settings Notifica
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateNotificationTemplateMethod modifies a notification template to use a specific notification method, overriding
|
||||||
|
// the method set in the deployment configuration.
|
||||||
|
func (c *Client) UpdateNotificationTemplateMethod(ctx context.Context, notificationTemplateID uuid.UUID, method string) error {
|
||||||
|
res, err := c.Request(ctx, http.MethodPut,
|
||||||
|
fmt.Sprintf("/api/v2/notifications/templates/%s/method", notificationTemplateID),
|
||||||
|
UpdateNotificationTemplateMethod{Method: method},
|
||||||
|
)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSystemNotificationTemplates retrieves all notification templates pertaining to internal system events.
|
||||||
|
func (c *Client) GetSystemNotificationTemplates(ctx context.Context) ([]NotificationTemplate, error) {
|
||||||
|
res, err := c.Request(ctx, http.MethodGet, "/api/v2/notifications/templates/system", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return nil, ReadBodyAsError(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
var templates []NotificationTemplate
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerrors.Errorf("read response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(body, &templates); err != nil {
|
||||||
|
return nil, xerrors.Errorf("unmarshal response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return templates, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserNotificationPreferences retrieves notification preferences for a given user.
|
||||||
|
func (c *Client) GetUserNotificationPreferences(ctx context.Context, userID uuid.UUID) ([]NotificationPreference, error) {
|
||||||
|
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/notifications/preferences", userID.String()), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return nil, ReadBodyAsError(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
var prefs []NotificationPreference
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerrors.Errorf("read response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(body, &prefs); err != nil {
|
||||||
|
return nil, xerrors.Errorf("unmarshal response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return prefs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUserNotificationPreferences updates notification preferences for a given user.
|
||||||
|
func (c *Client) UpdateUserNotificationPreferences(ctx context.Context, userID uuid.UUID, req UpdateUserNotificationPreferences) ([]NotificationPreference, error) {
|
||||||
|
res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/notifications/preferences", userID.String()), req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return nil, ReadBodyAsError(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
var prefs []NotificationPreference
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerrors.Errorf("read response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(body, &prefs); err != nil {
|
||||||
|
return nil, xerrors.Errorf("unmarshal response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return prefs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNotificationDispatchMethods the available and default notification dispatch methods.
|
||||||
|
func (c *Client) GetNotificationDispatchMethods(ctx context.Context) (NotificationMethodsResponse, error) {
|
||||||
|
res, err := c.Request(ctx, http.MethodGet, "/api/v2/notifications/dispatch-methods", nil)
|
||||||
|
if err != nil {
|
||||||
|
return NotificationMethodsResponse{}, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return NotificationMethodsResponse{}, ReadBodyAsError(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp NotificationMethodsResponse
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return NotificationMethodsResponse{}, xerrors.Errorf("read response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(body, &resp); err != nil {
|
||||||
|
return NotificationMethodsResponse{}, xerrors.Errorf("unmarshal response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateNotificationTemplateMethod struct {
|
||||||
|
Method string `json:"method,omitempty" example:"webhook"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateUserNotificationPreferences struct {
|
||||||
|
TemplateDisabledMap map[string]bool `json:"template_disabled_map"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,32 +4,34 @@ package codersdk
|
|||||||
type RBACResource string
|
type RBACResource string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ResourceWildcard RBACResource = "*"
|
ResourceWildcard RBACResource = "*"
|
||||||
ResourceApiKey RBACResource = "api_key"
|
ResourceApiKey RBACResource = "api_key"
|
||||||
ResourceAssignOrgRole RBACResource = "assign_org_role"
|
ResourceAssignOrgRole RBACResource = "assign_org_role"
|
||||||
ResourceAssignRole RBACResource = "assign_role"
|
ResourceAssignRole RBACResource = "assign_role"
|
||||||
ResourceAuditLog RBACResource = "audit_log"
|
ResourceAuditLog RBACResource = "audit_log"
|
||||||
ResourceDebugInfo RBACResource = "debug_info"
|
ResourceDebugInfo RBACResource = "debug_info"
|
||||||
ResourceDeploymentConfig RBACResource = "deployment_config"
|
ResourceDeploymentConfig RBACResource = "deployment_config"
|
||||||
ResourceDeploymentStats RBACResource = "deployment_stats"
|
ResourceDeploymentStats RBACResource = "deployment_stats"
|
||||||
ResourceFile RBACResource = "file"
|
ResourceFile RBACResource = "file"
|
||||||
ResourceGroup RBACResource = "group"
|
ResourceGroup RBACResource = "group"
|
||||||
ResourceLicense RBACResource = "license"
|
ResourceLicense RBACResource = "license"
|
||||||
ResourceOauth2App RBACResource = "oauth2_app"
|
ResourceNotificationPreference RBACResource = "notification_preference"
|
||||||
ResourceOauth2AppCodeToken RBACResource = "oauth2_app_code_token"
|
ResourceNotificationTemplate RBACResource = "notification_template"
|
||||||
ResourceOauth2AppSecret RBACResource = "oauth2_app_secret"
|
ResourceOauth2App RBACResource = "oauth2_app"
|
||||||
ResourceOrganization RBACResource = "organization"
|
ResourceOauth2AppCodeToken RBACResource = "oauth2_app_code_token"
|
||||||
ResourceOrganizationMember RBACResource = "organization_member"
|
ResourceOauth2AppSecret RBACResource = "oauth2_app_secret"
|
||||||
ResourceProvisionerDaemon RBACResource = "provisioner_daemon"
|
ResourceOrganization RBACResource = "organization"
|
||||||
ResourceProvisionerKeys RBACResource = "provisioner_keys"
|
ResourceOrganizationMember RBACResource = "organization_member"
|
||||||
ResourceReplicas RBACResource = "replicas"
|
ResourceProvisionerDaemon RBACResource = "provisioner_daemon"
|
||||||
ResourceSystem RBACResource = "system"
|
ResourceProvisionerKeys RBACResource = "provisioner_keys"
|
||||||
ResourceTailnetCoordinator RBACResource = "tailnet_coordinator"
|
ResourceReplicas RBACResource = "replicas"
|
||||||
ResourceTemplate RBACResource = "template"
|
ResourceSystem RBACResource = "system"
|
||||||
ResourceUser RBACResource = "user"
|
ResourceTailnetCoordinator RBACResource = "tailnet_coordinator"
|
||||||
ResourceWorkspace RBACResource = "workspace"
|
ResourceTemplate RBACResource = "template"
|
||||||
ResourceWorkspaceDormant RBACResource = "workspace_dormant"
|
ResourceUser RBACResource = "user"
|
||||||
ResourceWorkspaceProxy RBACResource = "workspace_proxy"
|
ResourceWorkspace RBACResource = "workspace"
|
||||||
|
ResourceWorkspaceDormant RBACResource = "workspace_dormant"
|
||||||
|
ResourceWorkspaceProxy RBACResource = "workspace_proxy"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RBACAction string
|
type RBACAction string
|
||||||
@@ -53,30 +55,32 @@ const (
|
|||||||
// RBACResourceActions is the mapping of resources to which actions are valid for
|
// RBACResourceActions is the mapping of resources to which actions are valid for
|
||||||
// said resource type.
|
// said resource type.
|
||||||
var RBACResourceActions = map[RBACResource][]RBACAction{
|
var RBACResourceActions = map[RBACResource][]RBACAction{
|
||||||
ResourceWildcard: {},
|
ResourceWildcard: {},
|
||||||
ResourceApiKey: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
ResourceApiKey: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||||
ResourceAssignOrgRole: {ActionAssign, ActionCreate, ActionDelete, ActionRead},
|
ResourceAssignOrgRole: {ActionAssign, ActionCreate, ActionDelete, ActionRead},
|
||||||
ResourceAssignRole: {ActionAssign, ActionCreate, ActionDelete, ActionRead},
|
ResourceAssignRole: {ActionAssign, ActionCreate, ActionDelete, ActionRead},
|
||||||
ResourceAuditLog: {ActionCreate, ActionRead},
|
ResourceAuditLog: {ActionCreate, ActionRead},
|
||||||
ResourceDebugInfo: {ActionRead},
|
ResourceDebugInfo: {ActionRead},
|
||||||
ResourceDeploymentConfig: {ActionRead, ActionUpdate},
|
ResourceDeploymentConfig: {ActionRead, ActionUpdate},
|
||||||
ResourceDeploymentStats: {ActionRead},
|
ResourceDeploymentStats: {ActionRead},
|
||||||
ResourceFile: {ActionCreate, ActionRead},
|
ResourceFile: {ActionCreate, ActionRead},
|
||||||
ResourceGroup: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
ResourceGroup: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||||
ResourceLicense: {ActionCreate, ActionDelete, ActionRead},
|
ResourceLicense: {ActionCreate, ActionDelete, ActionRead},
|
||||||
ResourceOauth2App: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
ResourceNotificationPreference: {ActionRead, ActionUpdate},
|
||||||
ResourceOauth2AppCodeToken: {ActionCreate, ActionDelete, ActionRead},
|
ResourceNotificationTemplate: {ActionRead, ActionUpdate},
|
||||||
ResourceOauth2AppSecret: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
ResourceOauth2App: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||||
ResourceOrganization: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
ResourceOauth2AppCodeToken: {ActionCreate, ActionDelete, ActionRead},
|
||||||
ResourceOrganizationMember: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
ResourceOauth2AppSecret: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||||
ResourceProvisionerDaemon: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
ResourceOrganization: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||||
ResourceProvisionerKeys: {ActionCreate, ActionDelete, ActionRead},
|
ResourceOrganizationMember: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||||
ResourceReplicas: {ActionRead},
|
ResourceProvisionerDaemon: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||||
ResourceSystem: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
ResourceProvisionerKeys: {ActionCreate, ActionDelete, ActionRead},
|
||||||
ResourceTailnetCoordinator: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
ResourceReplicas: {ActionRead},
|
||||||
ResourceTemplate: {ActionCreate, ActionDelete, ActionRead, ActionUpdate, ActionViewInsights},
|
ResourceSystem: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||||
ResourceUser: {ActionCreate, ActionDelete, ActionRead, ActionReadPersonal, ActionUpdate, ActionUpdatePersonal},
|
ResourceTailnetCoordinator: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||||
ResourceWorkspace: {ActionApplicationConnect, ActionCreate, ActionDelete, ActionRead, ActionSSH, ActionWorkspaceStart, ActionWorkspaceStop, ActionUpdate},
|
ResourceTemplate: {ActionCreate, ActionDelete, ActionRead, ActionUpdate, ActionViewInsights},
|
||||||
ResourceWorkspaceDormant: {ActionApplicationConnect, ActionCreate, ActionDelete, ActionRead, ActionSSH, ActionWorkspaceStart, ActionWorkspaceStop, ActionUpdate},
|
ResourceUser: {ActionCreate, ActionDelete, ActionRead, ActionReadPersonal, ActionUpdate, ActionUpdatePersonal},
|
||||||
ResourceWorkspaceProxy: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
ResourceWorkspace: {ActionApplicationConnect, ActionCreate, ActionDelete, ActionRead, ActionSSH, ActionWorkspaceStart, ActionWorkspaceStop, ActionUpdate},
|
||||||
|
ResourceWorkspaceDormant: {ActionApplicationConnect, ActionCreate, ActionDelete, ActionRead, ActionSSH, ActionWorkspaceStart, ActionWorkspaceStop, ActionUpdate},
|
||||||
|
ResourceWorkspaceProxy: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ We track the following resources:
|
|||||||
| GitSSHKey<br><i>create</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>private_key</td><td>true</td></tr><tr><td>public_key</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
| GitSSHKey<br><i>create</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>private_key</td><td>true</td></tr><tr><td>public_key</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
||||||
| HealthSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>dismissed_healthchecks</td><td>true</td></tr><tr><td>id</td><td>false</td></tr></tbody></table> |
|
| HealthSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>dismissed_healthchecks</td><td>true</td></tr><tr><td>id</td><td>false</td></tr></tbody></table> |
|
||||||
| License<br><i>create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>exp</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>jwt</td><td>false</td></tr><tr><td>uploaded_at</td><td>true</td></tr><tr><td>uuid</td><td>true</td></tr></tbody></table> |
|
| License<br><i>create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>exp</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>jwt</td><td>false</td></tr><tr><td>uploaded_at</td><td>true</td></tr><tr><td>uuid</td><td>true</td></tr></tbody></table> |
|
||||||
|
| NotificationTemplate<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>actions</td><td>true</td></tr><tr><td>body_template</td><td>true</td></tr><tr><td>group</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>kind</td><td>true</td></tr><tr><td>method</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>title_template</td><td>true</td></tr></tbody></table> |
|
||||||
| NotificationsSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>id</td><td>false</td></tr><tr><td>notifier_paused</td><td>true</td></tr></tbody></table> |
|
| NotificationsSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>id</td><td>false</td></tr><tr><td>notifier_paused</td><td>true</td></tr></tbody></table> |
|
||||||
| OAuth2ProviderApp<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>callback_url</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
| OAuth2ProviderApp<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>callback_url</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
||||||
| OAuth2ProviderAppSecret<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>app_id</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>display_secret</td><td>false</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>secret_prefix</td><td>false</td></tr></tbody></table> |
|
| OAuth2ProviderAppSecret<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>app_id</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>display_secret</td><td>false</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>secret_prefix</td><td>false</td></tr></tbody></table> |
|
||||||
|
|||||||
Generated
+27
@@ -537,6 +537,33 @@ curl -X DELETE http://coder-server:8080/api/v2/licenses/{id} \
|
|||||||
|
|
||||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||||
|
|
||||||
|
## Update notification template dispatch method
|
||||||
|
|
||||||
|
### Code samples
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Example request using curl
|
||||||
|
curl -X PUT http://coder-server:8080/api/v2/notifications/templates/{notification_template}/method \
|
||||||
|
-H 'Coder-Session-Token: API_KEY'
|
||||||
|
```
|
||||||
|
|
||||||
|
`PUT /notifications/templates/{notification_template}/method`
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | In | Type | Required | Description |
|
||||||
|
| ----------------------- | ---- | ------ | -------- | -------------------------- |
|
||||||
|
| `notification_template` | path | string | true | Notification template UUID |
|
||||||
|
|
||||||
|
### Responses
|
||||||
|
|
||||||
|
| Status | Meaning | Description | Schema |
|
||||||
|
| ------ | --------------------------------------------------------------- | ------------ | ------ |
|
||||||
|
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | Success | |
|
||||||
|
| 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).
|
||||||
|
|
||||||
## Get OAuth2 applications.
|
## Get OAuth2 applications.
|
||||||
|
|
||||||
### Code samples
|
### Code samples
|
||||||
|
|||||||
Generated
-78
@@ -667,84 +667,6 @@ Status Code **200**
|
|||||||
|
|
||||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
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
|
## Update check
|
||||||
|
|
||||||
### Code samples
|
### Code samples
|
||||||
|
|||||||
Generated
+129
-123
@@ -164,47 +164,49 @@ Status Code **200**
|
|||||||
|
|
||||||
#### Enumerated Values
|
#### Enumerated Values
|
||||||
|
|
||||||
| Property | Value |
|
| Property | Value |
|
||||||
| --------------- | ----------------------- |
|
| --------------- | ------------------------- |
|
||||||
| `action` | `application_connect` |
|
| `action` | `application_connect` |
|
||||||
| `action` | `assign` |
|
| `action` | `assign` |
|
||||||
| `action` | `create` |
|
| `action` | `create` |
|
||||||
| `action` | `delete` |
|
| `action` | `delete` |
|
||||||
| `action` | `read` |
|
| `action` | `read` |
|
||||||
| `action` | `read_personal` |
|
| `action` | `read_personal` |
|
||||||
| `action` | `ssh` |
|
| `action` | `ssh` |
|
||||||
| `action` | `update` |
|
| `action` | `update` |
|
||||||
| `action` | `update_personal` |
|
| `action` | `update_personal` |
|
||||||
| `action` | `use` |
|
| `action` | `use` |
|
||||||
| `action` | `view_insights` |
|
| `action` | `view_insights` |
|
||||||
| `action` | `start` |
|
| `action` | `start` |
|
||||||
| `action` | `stop` |
|
| `action` | `stop` |
|
||||||
| `resource_type` | `*` |
|
| `resource_type` | `*` |
|
||||||
| `resource_type` | `api_key` |
|
| `resource_type` | `api_key` |
|
||||||
| `resource_type` | `assign_org_role` |
|
| `resource_type` | `assign_org_role` |
|
||||||
| `resource_type` | `assign_role` |
|
| `resource_type` | `assign_role` |
|
||||||
| `resource_type` | `audit_log` |
|
| `resource_type` | `audit_log` |
|
||||||
| `resource_type` | `debug_info` |
|
| `resource_type` | `debug_info` |
|
||||||
| `resource_type` | `deployment_config` |
|
| `resource_type` | `deployment_config` |
|
||||||
| `resource_type` | `deployment_stats` |
|
| `resource_type` | `deployment_stats` |
|
||||||
| `resource_type` | `file` |
|
| `resource_type` | `file` |
|
||||||
| `resource_type` | `group` |
|
| `resource_type` | `group` |
|
||||||
| `resource_type` | `license` |
|
| `resource_type` | `license` |
|
||||||
| `resource_type` | `oauth2_app` |
|
| `resource_type` | `notification_preference` |
|
||||||
| `resource_type` | `oauth2_app_code_token` |
|
| `resource_type` | `notification_template` |
|
||||||
| `resource_type` | `oauth2_app_secret` |
|
| `resource_type` | `oauth2_app` |
|
||||||
| `resource_type` | `organization` |
|
| `resource_type` | `oauth2_app_code_token` |
|
||||||
| `resource_type` | `organization_member` |
|
| `resource_type` | `oauth2_app_secret` |
|
||||||
| `resource_type` | `provisioner_daemon` |
|
| `resource_type` | `organization` |
|
||||||
| `resource_type` | `provisioner_keys` |
|
| `resource_type` | `organization_member` |
|
||||||
| `resource_type` | `replicas` |
|
| `resource_type` | `provisioner_daemon` |
|
||||||
| `resource_type` | `system` |
|
| `resource_type` | `provisioner_keys` |
|
||||||
| `resource_type` | `tailnet_coordinator` |
|
| `resource_type` | `replicas` |
|
||||||
| `resource_type` | `template` |
|
| `resource_type` | `system` |
|
||||||
| `resource_type` | `user` |
|
| `resource_type` | `tailnet_coordinator` |
|
||||||
| `resource_type` | `workspace` |
|
| `resource_type` | `template` |
|
||||||
| `resource_type` | `workspace_dormant` |
|
| `resource_type` | `user` |
|
||||||
| `resource_type` | `workspace_proxy` |
|
| `resource_type` | `workspace` |
|
||||||
|
| `resource_type` | `workspace_dormant` |
|
||||||
|
| `resource_type` | `workspace_proxy` |
|
||||||
|
|
||||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||||
|
|
||||||
@@ -287,47 +289,49 @@ Status Code **200**
|
|||||||
|
|
||||||
#### Enumerated Values
|
#### Enumerated Values
|
||||||
|
|
||||||
| Property | Value |
|
| Property | Value |
|
||||||
| --------------- | ----------------------- |
|
| --------------- | ------------------------- |
|
||||||
| `action` | `application_connect` |
|
| `action` | `application_connect` |
|
||||||
| `action` | `assign` |
|
| `action` | `assign` |
|
||||||
| `action` | `create` |
|
| `action` | `create` |
|
||||||
| `action` | `delete` |
|
| `action` | `delete` |
|
||||||
| `action` | `read` |
|
| `action` | `read` |
|
||||||
| `action` | `read_personal` |
|
| `action` | `read_personal` |
|
||||||
| `action` | `ssh` |
|
| `action` | `ssh` |
|
||||||
| `action` | `update` |
|
| `action` | `update` |
|
||||||
| `action` | `update_personal` |
|
| `action` | `update_personal` |
|
||||||
| `action` | `use` |
|
| `action` | `use` |
|
||||||
| `action` | `view_insights` |
|
| `action` | `view_insights` |
|
||||||
| `action` | `start` |
|
| `action` | `start` |
|
||||||
| `action` | `stop` |
|
| `action` | `stop` |
|
||||||
| `resource_type` | `*` |
|
| `resource_type` | `*` |
|
||||||
| `resource_type` | `api_key` |
|
| `resource_type` | `api_key` |
|
||||||
| `resource_type` | `assign_org_role` |
|
| `resource_type` | `assign_org_role` |
|
||||||
| `resource_type` | `assign_role` |
|
| `resource_type` | `assign_role` |
|
||||||
| `resource_type` | `audit_log` |
|
| `resource_type` | `audit_log` |
|
||||||
| `resource_type` | `debug_info` |
|
| `resource_type` | `debug_info` |
|
||||||
| `resource_type` | `deployment_config` |
|
| `resource_type` | `deployment_config` |
|
||||||
| `resource_type` | `deployment_stats` |
|
| `resource_type` | `deployment_stats` |
|
||||||
| `resource_type` | `file` |
|
| `resource_type` | `file` |
|
||||||
| `resource_type` | `group` |
|
| `resource_type` | `group` |
|
||||||
| `resource_type` | `license` |
|
| `resource_type` | `license` |
|
||||||
| `resource_type` | `oauth2_app` |
|
| `resource_type` | `notification_preference` |
|
||||||
| `resource_type` | `oauth2_app_code_token` |
|
| `resource_type` | `notification_template` |
|
||||||
| `resource_type` | `oauth2_app_secret` |
|
| `resource_type` | `oauth2_app` |
|
||||||
| `resource_type` | `organization` |
|
| `resource_type` | `oauth2_app_code_token` |
|
||||||
| `resource_type` | `organization_member` |
|
| `resource_type` | `oauth2_app_secret` |
|
||||||
| `resource_type` | `provisioner_daemon` |
|
| `resource_type` | `organization` |
|
||||||
| `resource_type` | `provisioner_keys` |
|
| `resource_type` | `organization_member` |
|
||||||
| `resource_type` | `replicas` |
|
| `resource_type` | `provisioner_daemon` |
|
||||||
| `resource_type` | `system` |
|
| `resource_type` | `provisioner_keys` |
|
||||||
| `resource_type` | `tailnet_coordinator` |
|
| `resource_type` | `replicas` |
|
||||||
| `resource_type` | `template` |
|
| `resource_type` | `system` |
|
||||||
| `resource_type` | `user` |
|
| `resource_type` | `tailnet_coordinator` |
|
||||||
| `resource_type` | `workspace` |
|
| `resource_type` | `template` |
|
||||||
| `resource_type` | `workspace_dormant` |
|
| `resource_type` | `user` |
|
||||||
| `resource_type` | `workspace_proxy` |
|
| `resource_type` | `workspace` |
|
||||||
|
| `resource_type` | `workspace_dormant` |
|
||||||
|
| `resource_type` | `workspace_proxy` |
|
||||||
|
|
||||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||||
|
|
||||||
@@ -541,46 +545,48 @@ Status Code **200**
|
|||||||
|
|
||||||
#### Enumerated Values
|
#### Enumerated Values
|
||||||
|
|
||||||
| Property | Value |
|
| Property | Value |
|
||||||
| --------------- | ----------------------- |
|
| --------------- | ------------------------- |
|
||||||
| `action` | `application_connect` |
|
| `action` | `application_connect` |
|
||||||
| `action` | `assign` |
|
| `action` | `assign` |
|
||||||
| `action` | `create` |
|
| `action` | `create` |
|
||||||
| `action` | `delete` |
|
| `action` | `delete` |
|
||||||
| `action` | `read` |
|
| `action` | `read` |
|
||||||
| `action` | `read_personal` |
|
| `action` | `read_personal` |
|
||||||
| `action` | `ssh` |
|
| `action` | `ssh` |
|
||||||
| `action` | `update` |
|
| `action` | `update` |
|
||||||
| `action` | `update_personal` |
|
| `action` | `update_personal` |
|
||||||
| `action` | `use` |
|
| `action` | `use` |
|
||||||
| `action` | `view_insights` |
|
| `action` | `view_insights` |
|
||||||
| `action` | `start` |
|
| `action` | `start` |
|
||||||
| `action` | `stop` |
|
| `action` | `stop` |
|
||||||
| `resource_type` | `*` |
|
| `resource_type` | `*` |
|
||||||
| `resource_type` | `api_key` |
|
| `resource_type` | `api_key` |
|
||||||
| `resource_type` | `assign_org_role` |
|
| `resource_type` | `assign_org_role` |
|
||||||
| `resource_type` | `assign_role` |
|
| `resource_type` | `assign_role` |
|
||||||
| `resource_type` | `audit_log` |
|
| `resource_type` | `audit_log` |
|
||||||
| `resource_type` | `debug_info` |
|
| `resource_type` | `debug_info` |
|
||||||
| `resource_type` | `deployment_config` |
|
| `resource_type` | `deployment_config` |
|
||||||
| `resource_type` | `deployment_stats` |
|
| `resource_type` | `deployment_stats` |
|
||||||
| `resource_type` | `file` |
|
| `resource_type` | `file` |
|
||||||
| `resource_type` | `group` |
|
| `resource_type` | `group` |
|
||||||
| `resource_type` | `license` |
|
| `resource_type` | `license` |
|
||||||
| `resource_type` | `oauth2_app` |
|
| `resource_type` | `notification_preference` |
|
||||||
| `resource_type` | `oauth2_app_code_token` |
|
| `resource_type` | `notification_template` |
|
||||||
| `resource_type` | `oauth2_app_secret` |
|
| `resource_type` | `oauth2_app` |
|
||||||
| `resource_type` | `organization` |
|
| `resource_type` | `oauth2_app_code_token` |
|
||||||
| `resource_type` | `organization_member` |
|
| `resource_type` | `oauth2_app_secret` |
|
||||||
| `resource_type` | `provisioner_daemon` |
|
| `resource_type` | `organization` |
|
||||||
| `resource_type` | `provisioner_keys` |
|
| `resource_type` | `organization_member` |
|
||||||
| `resource_type` | `replicas` |
|
| `resource_type` | `provisioner_daemon` |
|
||||||
| `resource_type` | `system` |
|
| `resource_type` | `provisioner_keys` |
|
||||||
| `resource_type` | `tailnet_coordinator` |
|
| `resource_type` | `replicas` |
|
||||||
| `resource_type` | `template` |
|
| `resource_type` | `system` |
|
||||||
| `resource_type` | `user` |
|
| `resource_type` | `tailnet_coordinator` |
|
||||||
| `resource_type` | `workspace` |
|
| `resource_type` | `template` |
|
||||||
| `resource_type` | `workspace_dormant` |
|
| `resource_type` | `user` |
|
||||||
| `resource_type` | `workspace_proxy` |
|
| `resource_type` | `workspace` |
|
||||||
|
| `resource_type` | `workspace_dormant` |
|
||||||
|
| `resource_type` | `workspace_proxy` |
|
||||||
|
|
||||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||||
|
|||||||
Generated
+296
@@ -0,0 +1,296 @@
|
|||||||
|
# Notifications
|
||||||
|
|
||||||
|
## Get notification dispatch methods
|
||||||
|
|
||||||
|
### Code samples
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Example request using curl
|
||||||
|
curl -X GET http://coder-server:8080/api/v2/notifications/dispatch-methods \
|
||||||
|
-H 'Accept: application/json' \
|
||||||
|
-H 'Coder-Session-Token: API_KEY'
|
||||||
|
```
|
||||||
|
|
||||||
|
`GET /notifications/dispatch-methods`
|
||||||
|
|
||||||
|
### Example responses
|
||||||
|
|
||||||
|
> 200 Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"available": ["string"],
|
||||||
|
"default": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Responses
|
||||||
|
|
||||||
|
| Status | Meaning | Description | Schema |
|
||||||
|
| ------ | ------------------------------------------------------- | ----------- | ----------------------------------------------------------------------------------------------- |
|
||||||
|
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.NotificationMethodsResponse](schemas.md#codersdknotificationmethodsresponse) |
|
||||||
|
|
||||||
|
<h3 id="get-notification-dispatch-methods-responseschema">Response Schema</h3>
|
||||||
|
|
||||||
|
Status Code **200**
|
||||||
|
|
||||||
|
| Name | Type | Required | Restrictions | Description |
|
||||||
|
| -------------- | ------ | -------- | ------------ | ----------- |
|
||||||
|
| `[array item]` | array | false | | |
|
||||||
|
| `» available` | array | false | | |
|
||||||
|
| `» default` | string | false | | |
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
## Get system notification templates
|
||||||
|
|
||||||
|
### Code samples
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Example request using curl
|
||||||
|
curl -X GET http://coder-server:8080/api/v2/notifications/templates/system \
|
||||||
|
-H 'Accept: application/json' \
|
||||||
|
-H 'Coder-Session-Token: API_KEY'
|
||||||
|
```
|
||||||
|
|
||||||
|
`GET /notifications/templates/system`
|
||||||
|
|
||||||
|
### Example responses
|
||||||
|
|
||||||
|
> 200 Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"actions": "string",
|
||||||
|
"body_template": "string",
|
||||||
|
"group": "string",
|
||||||
|
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||||
|
"kind": "string",
|
||||||
|
"method": "string",
|
||||||
|
"name": "string",
|
||||||
|
"title_template": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Responses
|
||||||
|
|
||||||
|
| Status | Meaning | Description | Schema |
|
||||||
|
| ------ | ------------------------------------------------------- | ----------- | --------------------------------------------------------------------------------- |
|
||||||
|
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.NotificationTemplate](schemas.md#codersdknotificationtemplate) |
|
||||||
|
|
||||||
|
<h3 id="get-system-notification-templates-responseschema">Response Schema</h3>
|
||||||
|
|
||||||
|
Status Code **200**
|
||||||
|
|
||||||
|
| Name | Type | Required | Restrictions | Description |
|
||||||
|
| ------------------ | ------------ | -------- | ------------ | ----------- |
|
||||||
|
| `[array item]` | array | false | | |
|
||||||
|
| `» actions` | string | false | | |
|
||||||
|
| `» body_template` | string | false | | |
|
||||||
|
| `» group` | string | false | | |
|
||||||
|
| `» id` | string(uuid) | false | | |
|
||||||
|
| `» kind` | string | false | | |
|
||||||
|
| `» method` | string | false | | |
|
||||||
|
| `» name` | string | false | | |
|
||||||
|
| `» title_template` | string | false | | |
|
||||||
|
|
||||||
|
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||||
|
|
||||||
|
## Get user notification preferences
|
||||||
|
|
||||||
|
### Code samples
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Example request using curl
|
||||||
|
curl -X GET http://coder-server:8080/api/v2/users/{user}/notifications/preferences \
|
||||||
|
-H 'Accept: application/json' \
|
||||||
|
-H 'Coder-Session-Token: API_KEY'
|
||||||
|
```
|
||||||
|
|
||||||
|
`GET /users/{user}/notifications/preferences`
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | In | Type | Required | Description |
|
||||||
|
| ------ | ---- | ------ | -------- | -------------------- |
|
||||||
|
| `user` | path | string | true | User ID, name, or me |
|
||||||
|
|
||||||
|
### Example responses
|
||||||
|
|
||||||
|
> 200 Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"disabled": true,
|
||||||
|
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||||
|
"updated_at": "2019-08-24T14:15:22Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Responses
|
||||||
|
|
||||||
|
| Status | Meaning | Description | Schema |
|
||||||
|
| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------- |
|
||||||
|
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.NotificationPreference](schemas.md#codersdknotificationpreference) |
|
||||||
|
|
||||||
|
<h3 id="get-user-notification-preferences-responseschema">Response Schema</h3>
|
||||||
|
|
||||||
|
Status Code **200**
|
||||||
|
|
||||||
|
| Name | Type | Required | Restrictions | Description |
|
||||||
|
| -------------- | ----------------- | -------- | ------------ | ----------- |
|
||||||
|
| `[array item]` | array | false | | |
|
||||||
|
| `» disabled` | boolean | false | | |
|
||||||
|
| `» id` | string(uuid) | false | | |
|
||||||
|
| `» updated_at` | string(date-time) | false | | |
|
||||||
|
|
||||||
|
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||||
|
|
||||||
|
## Update user notification preferences
|
||||||
|
|
||||||
|
### Code samples
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Example request using curl
|
||||||
|
curl -X PUT http://coder-server:8080/api/v2/users/{user}/notifications/preferences \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-H 'Accept: application/json' \
|
||||||
|
-H 'Coder-Session-Token: API_KEY'
|
||||||
|
```
|
||||||
|
|
||||||
|
`PUT /users/{user}/notifications/preferences`
|
||||||
|
|
||||||
|
> Body parameter
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"template_disabled_map": {
|
||||||
|
"property1": true,
|
||||||
|
"property2": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | In | Type | Required | Description |
|
||||||
|
| ------ | ---- | -------------------------------------------------------------------------------------------------- | -------- | -------------------- |
|
||||||
|
| `user` | path | string | true | User ID, name, or me |
|
||||||
|
| `body` | body | [codersdk.UpdateUserNotificationPreferences](schemas.md#codersdkupdateusernotificationpreferences) | true | Preferences |
|
||||||
|
|
||||||
|
### Example responses
|
||||||
|
|
||||||
|
> 200 Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"disabled": true,
|
||||||
|
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||||
|
"updated_at": "2019-08-24T14:15:22Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Responses
|
||||||
|
|
||||||
|
| Status | Meaning | Description | Schema |
|
||||||
|
| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------- |
|
||||||
|
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.NotificationPreference](schemas.md#codersdknotificationpreference) |
|
||||||
|
|
||||||
|
<h3 id="update-user-notification-preferences-responseschema">Response Schema</h3>
|
||||||
|
|
||||||
|
Status Code **200**
|
||||||
|
|
||||||
|
| Name | Type | Required | Restrictions | Description |
|
||||||
|
| -------------- | ----------------- | -------- | ------------ | ----------- |
|
||||||
|
| `[array item]` | array | false | | |
|
||||||
|
| `» disabled` | boolean | false | | |
|
||||||
|
| `» id` | string(uuid) | false | | |
|
||||||
|
| `» updated_at` | string(date-time) | false | | |
|
||||||
|
|
||||||
|
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||||
Generated
+110
-28
@@ -3141,6 +3141,68 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
|||||||
| `id` | string | true | | |
|
| `id` | string | true | | |
|
||||||
| `username` | string | true | | |
|
| `username` | string | true | | |
|
||||||
|
|
||||||
|
## codersdk.NotificationMethodsResponse
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"available": ["string"],
|
||||||
|
"default": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Properties
|
||||||
|
|
||||||
|
| Name | Type | Required | Restrictions | Description |
|
||||||
|
| ----------- | --------------- | -------- | ------------ | ----------- |
|
||||||
|
| `available` | array of string | false | | |
|
||||||
|
| `default` | string | false | | |
|
||||||
|
|
||||||
|
## codersdk.NotificationPreference
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"disabled": true,
|
||||||
|
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||||
|
"updated_at": "2019-08-24T14:15:22Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Properties
|
||||||
|
|
||||||
|
| Name | Type | Required | Restrictions | Description |
|
||||||
|
| ------------ | ------- | -------- | ------------ | ----------- |
|
||||||
|
| `disabled` | boolean | false | | |
|
||||||
|
| `id` | string | false | | |
|
||||||
|
| `updated_at` | string | false | | |
|
||||||
|
|
||||||
|
## codersdk.NotificationTemplate
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"actions": "string",
|
||||||
|
"body_template": "string",
|
||||||
|
"group": "string",
|
||||||
|
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||||
|
"kind": "string",
|
||||||
|
"method": "string",
|
||||||
|
"name": "string",
|
||||||
|
"title_template": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Properties
|
||||||
|
|
||||||
|
| Name | Type | Required | Restrictions | Description |
|
||||||
|
| ---------------- | ------ | -------- | ------------ | ----------- |
|
||||||
|
| `actions` | string | false | | |
|
||||||
|
| `body_template` | string | false | | |
|
||||||
|
| `group` | string | false | | |
|
||||||
|
| `id` | string | false | | |
|
||||||
|
| `kind` | string | false | | |
|
||||||
|
| `method` | string | false | | |
|
||||||
|
| `name` | string | false | | |
|
||||||
|
| `title_template` | string | false | | |
|
||||||
|
|
||||||
## codersdk.NotificationsConfig
|
## codersdk.NotificationsConfig
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -4153,34 +4215,36 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
|||||||
|
|
||||||
#### Enumerated Values
|
#### Enumerated Values
|
||||||
|
|
||||||
| Value |
|
| Value |
|
||||||
| ----------------------- |
|
| ------------------------- |
|
||||||
| `*` |
|
| `*` |
|
||||||
| `api_key` |
|
| `api_key` |
|
||||||
| `assign_org_role` |
|
| `assign_org_role` |
|
||||||
| `assign_role` |
|
| `assign_role` |
|
||||||
| `audit_log` |
|
| `audit_log` |
|
||||||
| `debug_info` |
|
| `debug_info` |
|
||||||
| `deployment_config` |
|
| `deployment_config` |
|
||||||
| `deployment_stats` |
|
| `deployment_stats` |
|
||||||
| `file` |
|
| `file` |
|
||||||
| `group` |
|
| `group` |
|
||||||
| `license` |
|
| `license` |
|
||||||
| `oauth2_app` |
|
| `notification_preference` |
|
||||||
| `oauth2_app_code_token` |
|
| `notification_template` |
|
||||||
| `oauth2_app_secret` |
|
| `oauth2_app` |
|
||||||
| `organization` |
|
| `oauth2_app_code_token` |
|
||||||
| `organization_member` |
|
| `oauth2_app_secret` |
|
||||||
| `provisioner_daemon` |
|
| `organization` |
|
||||||
| `provisioner_keys` |
|
| `organization_member` |
|
||||||
| `replicas` |
|
| `provisioner_daemon` |
|
||||||
| `system` |
|
| `provisioner_keys` |
|
||||||
| `tailnet_coordinator` |
|
| `replicas` |
|
||||||
| `template` |
|
| `system` |
|
||||||
| `user` |
|
| `tailnet_coordinator` |
|
||||||
| `workspace` |
|
| `template` |
|
||||||
| `workspace_dormant` |
|
| `user` |
|
||||||
| `workspace_proxy` |
|
| `workspace` |
|
||||||
|
| `workspace_dormant` |
|
||||||
|
| `workspace_proxy` |
|
||||||
|
|
||||||
## codersdk.RateLimitConfig
|
## codersdk.RateLimitConfig
|
||||||
|
|
||||||
@@ -5535,6 +5599,24 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
|||||||
| ------------------ | ------ | -------- | ------------ | ----------- |
|
| ------------------ | ------ | -------- | ------------ | ----------- |
|
||||||
| `theme_preference` | string | true | | |
|
| `theme_preference` | string | true | | |
|
||||||
|
|
||||||
|
## codersdk.UpdateUserNotificationPreferences
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"template_disabled_map": {
|
||||||
|
"property1": true,
|
||||||
|
"property2": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Properties
|
||||||
|
|
||||||
|
| Name | Type | Required | Restrictions | Description |
|
||||||
|
| ----------------------- | ------- | -------- | ------------ | ----------- |
|
||||||
|
| `template_disabled_map` | object | false | | |
|
||||||
|
| » `[any property]` | boolean | false | | |
|
||||||
|
|
||||||
## codersdk.UpdateUserPasswordRequest
|
## codersdk.UpdateUserPasswordRequest
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -601,6 +601,10 @@
|
|||||||
"title": "Members",
|
"title": "Members",
|
||||||
"path": "./api/members.md"
|
"path": "./api/members.md"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Notifications",
|
||||||
|
"path": "./api/notifications.md"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Organizations",
|
"title": "Organizations",
|
||||||
"path": "./api/organizations.md"
|
"path": "./api/organizations.md"
|
||||||
|
|||||||
@@ -142,6 +142,13 @@ func convertDiffType(left, right any) (newLeft, newRight any, changed bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return leftInt64Ptr, rightInt64Ptr, true
|
return leftInt64Ptr, rightInt64Ptr, true
|
||||||
|
case database.NullNotificationMethod:
|
||||||
|
vl, vr := string(typedLeft.NotificationMethod), ""
|
||||||
|
if val, ok := right.(database.NullNotificationMethod); ok {
|
||||||
|
vr = string(val.NotificationMethod)
|
||||||
|
}
|
||||||
|
|
||||||
|
return vl, vr, true
|
||||||
case database.TemplateACL:
|
case database.TemplateACL:
|
||||||
return fmt.Sprintf("%+v", left), fmt.Sprintf("%+v", right), true
|
return fmt.Sprintf("%+v", left), fmt.Sprintf("%+v", right), true
|
||||||
case database.CustomRolePermissions:
|
case database.CustomRolePermissions:
|
||||||
|
|||||||
@@ -272,6 +272,16 @@ var auditableResourcesTypes = map[any]map[string]Action{
|
|||||||
"display_name": ActionTrack,
|
"display_name": ActionTrack,
|
||||||
"icon": ActionTrack,
|
"icon": ActionTrack,
|
||||||
},
|
},
|
||||||
|
&database.NotificationTemplate{}: {
|
||||||
|
"id": ActionIgnore,
|
||||||
|
"name": ActionTrack,
|
||||||
|
"title_template": ActionTrack,
|
||||||
|
"body_template": ActionTrack,
|
||||||
|
"actions": ActionTrack,
|
||||||
|
"group": ActionTrack,
|
||||||
|
"method": ActionTrack,
|
||||||
|
"kind": ActionTrack,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// auditMap converts a map of struct pointers to a map of struct names as
|
// auditMap converts a map of struct pointers to a map of struct names as
|
||||||
|
|||||||
@@ -368,7 +368,6 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
|
|||||||
r.Put("/", api.putAppearance)
|
r.Put("/", api.putAppearance)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
r.Route("/users/{user}/quiet-hours", func(r chi.Router) {
|
r.Route("/users/{user}/quiet-hours", func(r chi.Router) {
|
||||||
r.Use(
|
r.Use(
|
||||||
api.autostopRequirementEnabledMW,
|
api.autostopRequirementEnabledMW,
|
||||||
@@ -388,6 +387,15 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
|
|||||||
r.Post("/jfrog/xray-scan", api.postJFrogXrayScan)
|
r.Post("/jfrog/xray-scan", api.postJFrogXrayScan)
|
||||||
r.Get("/jfrog/xray-scan", api.jFrogXrayScan)
|
r.Get("/jfrog/xray-scan", api.jFrogXrayScan)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// The /notifications base route is mounted by the AGPL router, so we can't group it here.
|
||||||
|
// Additionally, because we have a static route for /notifications/templates/system which conflicts
|
||||||
|
// with the below route, we need to register this route without any mounts or groups to make both work.
|
||||||
|
r.With(
|
||||||
|
apiKeyMiddleware,
|
||||||
|
httpmw.RequireExperiment(api.AGPL.Experiments, codersdk.ExperimentNotifications),
|
||||||
|
httpmw.ExtractNotificationTemplateParam(options.Database),
|
||||||
|
).Put("/notifications/templates/{notification_template}/method", api.updateNotificationTemplateMethod)
|
||||||
})
|
})
|
||||||
|
|
||||||
if len(options.SCIMAPIKey) != 0 {
|
if len(options.SCIMAPIKey) != 0 {
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package coderd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
|
"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/httpmw"
|
||||||
|
"github.com/coder/coder/v2/codersdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @Summary Update notification template dispatch method
|
||||||
|
// @ID update-notification-template-dispatch-method
|
||||||
|
// @Security CoderSessionToken
|
||||||
|
// @Produce json
|
||||||
|
// @Param notification_template path string true "Notification template UUID"
|
||||||
|
// @Tags Enterprise
|
||||||
|
// @Success 200 "Success"
|
||||||
|
// @Success 304 "Not modified"
|
||||||
|
// @Router /notifications/templates/{notification_template}/method [put]
|
||||||
|
func (api *API) updateNotificationTemplateMethod(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
ctx = r.Context()
|
||||||
|
template = httpmw.NotificationTemplateParam(r)
|
||||||
|
auditor = api.AGPL.Auditor.Load()
|
||||||
|
aReq, commitAudit = audit.InitRequest[database.NotificationTemplate](rw, &audit.RequestParams{
|
||||||
|
Audit: *auditor,
|
||||||
|
Log: api.Logger,
|
||||||
|
Request: r,
|
||||||
|
Action: database.AuditActionWrite,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
var req codersdk.UpdateNotificationTemplateMethod
|
||||||
|
if !httpapi.Read(ctx, rw, r, &req) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var nm database.NullNotificationMethod
|
||||||
|
if err := nm.Scan(req.Method); err != nil || !nm.Valid || !nm.NotificationMethod.Valid() {
|
||||||
|
vals := database.AllNotificationMethodValues()
|
||||||
|
acceptable := make([]string, len(vals))
|
||||||
|
for i, v := range vals {
|
||||||
|
acceptable[i] = string(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||||
|
Message: "Invalid request to update notification template method",
|
||||||
|
Validations: []codersdk.ValidationError{
|
||||||
|
{
|
||||||
|
Field: "method",
|
||||||
|
Detail: fmt.Sprintf("%q is not a valid method; %s are the available options",
|
||||||
|
req.Method, strings.Join(acceptable, ", "),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if template.Method == nm {
|
||||||
|
httpapi.Write(ctx, rw, http.StatusNotModified, codersdk.Response{
|
||||||
|
Message: "Notification template method unchanged.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer commitAudit()
|
||||||
|
aReq.Old = template
|
||||||
|
|
||||||
|
err := api.Database.InTx(func(tx database.Store) error {
|
||||||
|
var err error
|
||||||
|
template, err = api.Database.UpdateNotificationTemplateMethodByID(r.Context(), database.UpdateNotificationTemplateMethodByIDParams{
|
||||||
|
ID: template.ID,
|
||||||
|
Method: nm,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("failed to update notification template ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
httpapi.InternalServerError(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
aReq.New = template
|
||||||
|
|
||||||
|
httpapi.Write(ctx, rw, http.StatusOK, codersdk.Response{
|
||||||
|
Message: "Successfully updated notification template method.",
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
package coderd_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||||
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
|
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
||||||
|
"github.com/coder/coder/v2/coderd/notifications"
|
||||||
|
"github.com/coder/coder/v2/codersdk"
|
||||||
|
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
|
||||||
|
"github.com/coder/coder/v2/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func createOpts(t *testing.T) *coderdenttest.Options {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
dt := coderdtest.DeploymentValues(t)
|
||||||
|
dt.Experiments = []string{string(codersdk.ExperimentNotifications)}
|
||||||
|
return &coderdenttest.Options{
|
||||||
|
Options: &coderdtest.Options{
|
||||||
|
DeploymentValues: dt,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateNotificationTemplateMethod(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("Happy path", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if !dbtestutil.WillUsePostgres() {
|
||||||
|
t.Skip("This test requires postgres; it relies on read from and writing to the notification_templates table")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := testutil.Context(t, testutil.WaitSuperLong)
|
||||||
|
api, _ := coderdenttest.New(t, createOpts(t))
|
||||||
|
|
||||||
|
var (
|
||||||
|
method = string(database.NotificationMethodSmtp)
|
||||||
|
templateID = notifications.TemplateWorkspaceDeleted
|
||||||
|
)
|
||||||
|
|
||||||
|
// Given: a template whose method is initially empty (i.e. deferring to the global method value).
|
||||||
|
template, err := getTemplateByID(t, ctx, api, templateID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, template)
|
||||||
|
require.Empty(t, template.Method)
|
||||||
|
|
||||||
|
// When: calling the API to update the method.
|
||||||
|
require.NoError(t, api.UpdateNotificationTemplateMethod(ctx, notifications.TemplateWorkspaceDeleted, method), "initial request to set the method failed")
|
||||||
|
|
||||||
|
// Then: the method should be set.
|
||||||
|
template, err = getTemplateByID(t, ctx, api, templateID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, template)
|
||||||
|
require.Equal(t, method, template.Method)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Insufficient permissions", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if !dbtestutil.WillUsePostgres() {
|
||||||
|
t.Skip("This test requires postgres; it relies on read from and writing to the notification_templates table")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := testutil.Context(t, testutil.WaitSuperLong)
|
||||||
|
|
||||||
|
// Given: the first user which has an "owner" role, and another user which does not.
|
||||||
|
api, firstUser := coderdenttest.New(t, createOpts(t))
|
||||||
|
anotherClient, _ := coderdtest.CreateAnotherUser(t, api, firstUser.OrganizationID)
|
||||||
|
|
||||||
|
// When: calling the API as an unprivileged user.
|
||||||
|
err := anotherClient.UpdateNotificationTemplateMethod(ctx, notifications.TemplateWorkspaceDeleted, string(database.NotificationMethodWebhook))
|
||||||
|
|
||||||
|
// Then: the request is denied because of insufficient permissions.
|
||||||
|
var sdkError *codersdk.Error
|
||||||
|
require.Error(t, err)
|
||||||
|
require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error")
|
||||||
|
require.Equal(t, http.StatusNotFound, sdkError.StatusCode())
|
||||||
|
require.Equal(t, "Resource not found or you do not have access to this resource", sdkError.Response.Message)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Invalid notification method", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if !dbtestutil.WillUsePostgres() {
|
||||||
|
t.Skip("This test requires postgres; it relies on read from and writing to the notification_templates table")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := testutil.Context(t, testutil.WaitSuperLong)
|
||||||
|
|
||||||
|
// Given: the first user which has an "owner" role
|
||||||
|
api, _ := coderdenttest.New(t, createOpts(t))
|
||||||
|
|
||||||
|
// When: calling the API with an invalid method.
|
||||||
|
const method = "nope"
|
||||||
|
|
||||||
|
// nolint:gocritic // Using an owner-scope user is kinda the point.
|
||||||
|
err := api.UpdateNotificationTemplateMethod(ctx, notifications.TemplateWorkspaceDeleted, method)
|
||||||
|
|
||||||
|
// Then: the request is invalid because of the unacceptable method.
|
||||||
|
var sdkError *codersdk.Error
|
||||||
|
require.Error(t, err)
|
||||||
|
require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error")
|
||||||
|
require.Equal(t, http.StatusBadRequest, sdkError.StatusCode())
|
||||||
|
require.Equal(t, "Invalid request to update notification template method", sdkError.Response.Message)
|
||||||
|
require.Len(t, sdkError.Response.Validations, 1)
|
||||||
|
require.Equal(t, "method", sdkError.Response.Validations[0].Field)
|
||||||
|
require.Equal(t, fmt.Sprintf("%q is not a valid method; smtp, webhook are the available options", method), sdkError.Response.Validations[0].Detail)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Not modified", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if !dbtestutil.WillUsePostgres() {
|
||||||
|
t.Skip("This test requires postgres; it relies on read from and writing to the notification_templates table")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := testutil.Context(t, testutil.WaitSuperLong)
|
||||||
|
api, _ := coderdenttest.New(t, createOpts(t))
|
||||||
|
|
||||||
|
var (
|
||||||
|
method = string(database.NotificationMethodSmtp)
|
||||||
|
templateID = notifications.TemplateWorkspaceDeleted
|
||||||
|
)
|
||||||
|
|
||||||
|
template, err := getTemplateByID(t, ctx, api, templateID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, template)
|
||||||
|
|
||||||
|
// Given: a template whose method is initially empty (i.e. deferring to the global method value).
|
||||||
|
require.Empty(t, template.Method)
|
||||||
|
|
||||||
|
// When: calling the API to update the method, it should set it.
|
||||||
|
require.NoError(t, api.UpdateNotificationTemplateMethod(ctx, notifications.TemplateWorkspaceDeleted, method), "initial request to set the method failed")
|
||||||
|
template, err = getTemplateByID(t, ctx, api, templateID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, template)
|
||||||
|
require.Equal(t, method, template.Method)
|
||||||
|
|
||||||
|
// Then: when calling the API again with the same method, the method will remain unchanged.
|
||||||
|
require.NoError(t, api.UpdateNotificationTemplateMethod(ctx, notifications.TemplateWorkspaceDeleted, method), "second request to set the method failed")
|
||||||
|
template, err = getTemplateByID(t, ctx, api, templateID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, template)
|
||||||
|
require.Equal(t, method, template.Method)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// nolint:revive // t takes precedence.
|
||||||
|
func getTemplateByID(t *testing.T, ctx context.Context, api *codersdk.Client, id uuid.UUID) (*codersdk.NotificationTemplate, error) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var template codersdk.NotificationTemplate
|
||||||
|
templates, err := api.GetSystemNotificationTemplates(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tmpl := range templates {
|
||||||
|
if tmpl.ID == id {
|
||||||
|
template = tmpl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if template.ID == uuid.Nil {
|
||||||
|
return nil, xerrors.Errorf("template not found: %q", id.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return &template, nil
|
||||||
|
}
|
||||||
@@ -55,6 +55,14 @@ export const RBACResourceActions: Partial<
|
|||||||
delete: "delete license",
|
delete: "delete license",
|
||||||
read: "read licenses",
|
read: "read licenses",
|
||||||
},
|
},
|
||||||
|
notification_preference: {
|
||||||
|
read: "read notification preferences",
|
||||||
|
update: "update notification preferences",
|
||||||
|
},
|
||||||
|
notification_template: {
|
||||||
|
read: "read notification templates",
|
||||||
|
update: "update notification templates",
|
||||||
|
},
|
||||||
oauth2_app: {
|
oauth2_app: {
|
||||||
create: "make an OAuth2 app.",
|
create: "make an OAuth2 app.",
|
||||||
delete: "delete an OAuth2 app",
|
delete: "delete an OAuth2 app",
|
||||||
|
|||||||
Generated
+39
@@ -709,6 +709,31 @@ export interface MinimalUser {
|
|||||||
readonly avatar_url: string;
|
readonly avatar_url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// From codersdk/notifications.go
|
||||||
|
export interface NotificationMethodsResponse {
|
||||||
|
readonly available: readonly string[];
|
||||||
|
readonly default: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// From codersdk/notifications.go
|
||||||
|
export interface NotificationPreference {
|
||||||
|
readonly id: string;
|
||||||
|
readonly disabled: boolean;
|
||||||
|
readonly updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// From codersdk/notifications.go
|
||||||
|
export interface NotificationTemplate {
|
||||||
|
readonly id: string;
|
||||||
|
readonly name: string;
|
||||||
|
readonly title_template: string;
|
||||||
|
readonly body_template: string;
|
||||||
|
readonly actions: string;
|
||||||
|
readonly group: string;
|
||||||
|
readonly method: string;
|
||||||
|
readonly kind: string;
|
||||||
|
}
|
||||||
|
|
||||||
// From codersdk/deployment.go
|
// From codersdk/deployment.go
|
||||||
export interface NotificationsConfig {
|
export interface NotificationsConfig {
|
||||||
readonly max_send_attempts: number;
|
readonly max_send_attempts: number;
|
||||||
@@ -1447,6 +1472,11 @@ export interface UpdateCheckResponse {
|
|||||||
readonly url: string;
|
readonly url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// From codersdk/notifications.go
|
||||||
|
export interface UpdateNotificationTemplateMethod {
|
||||||
|
readonly method?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// From codersdk/organizations.go
|
// From codersdk/organizations.go
|
||||||
export interface UpdateOrganizationRequest {
|
export interface UpdateOrganizationRequest {
|
||||||
readonly name?: string;
|
readonly name?: string;
|
||||||
@@ -1495,6 +1525,11 @@ export interface UpdateUserAppearanceSettingsRequest {
|
|||||||
readonly theme_preference: string;
|
readonly theme_preference: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// From codersdk/notifications.go
|
||||||
|
export interface UpdateUserNotificationPreferences {
|
||||||
|
readonly template_disabled_map: Record<string, boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
// From codersdk/users.go
|
// From codersdk/users.go
|
||||||
export interface UpdateUserPasswordRequest {
|
export interface UpdateUserPasswordRequest {
|
||||||
readonly old_password: string;
|
readonly old_password: string;
|
||||||
@@ -2269,6 +2304,8 @@ export type RBACResource =
|
|||||||
| "file"
|
| "file"
|
||||||
| "group"
|
| "group"
|
||||||
| "license"
|
| "license"
|
||||||
|
| "notification_preference"
|
||||||
|
| "notification_template"
|
||||||
| "oauth2_app"
|
| "oauth2_app"
|
||||||
| "oauth2_app_code_token"
|
| "oauth2_app_code_token"
|
||||||
| "oauth2_app_secret"
|
| "oauth2_app_secret"
|
||||||
@@ -2296,6 +2333,8 @@ export const RBACResources: RBACResource[] = [
|
|||||||
"file",
|
"file",
|
||||||
"group",
|
"group",
|
||||||
"license",
|
"license",
|
||||||
|
"notification_preference",
|
||||||
|
"notification_template",
|
||||||
"oauth2_app",
|
"oauth2_app",
|
||||||
"oauth2_app_code_token",
|
"oauth2_app_code_token",
|
||||||
"oauth2_app_secret",
|
"oauth2_app_secret",
|
||||||
|
|||||||
Reference in New Issue
Block a user