mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: support custom notifications (#19751)
## Description Adds support for sending an ad‑hoc custom notification to the authenticated user via API and CLI. This is useful for surfacing the result of scripts or long‑running tasks. Notifications are delivered through the configured method and the dashboard Inbox, respecting existing preferences and delivery settings. ## Changes * New notification template: “Custom Notification” with a label for a custom title and a custom message. * New API endpoint: `POST /api/v2/notifications/custom` to send a custom notification to the requesting user. * New API endpoint: `GET /notifications/templates/custom` to get custom notification template. * New CLI subcommand: `coder notifications custom <title> <message>` to send a custom notification to the requesting user. * Documentation updates: Add a “Custom notifications” section under Administration > Monitoring > Notifications, including instructions on sending custom notifications and examples of when to use them. Closes: https://github.com/coder/coder/issues/19611
This commit is contained in:
+34
-2
@@ -16,7 +16,7 @@ func (r *RootCmd) notifications() *serpent.Command {
|
|||||||
Short: "Manage Coder notifications",
|
Short: "Manage Coder notifications",
|
||||||
Long: "Administrators can use these commands to change notification settings.\n" + FormatExamples(
|
Long: "Administrators can use these commands to change notification settings.\n" + FormatExamples(
|
||||||
Example{
|
Example{
|
||||||
Description: "Pause Coder notifications. Administrators can temporarily stop notifiers from dispatching messages in case of the target outage (for example: unavailable SMTP server or Webhook not responding).",
|
Description: "Pause Coder notifications. Administrators can temporarily stop notifiers from dispatching messages in case of the target outage (for example: unavailable SMTP server or Webhook not responding)",
|
||||||
Command: "coder notifications pause",
|
Command: "coder notifications pause",
|
||||||
},
|
},
|
||||||
Example{
|
Example{
|
||||||
@@ -24,9 +24,13 @@ func (r *RootCmd) notifications() *serpent.Command {
|
|||||||
Command: "coder notifications resume",
|
Command: "coder notifications resume",
|
||||||
},
|
},
|
||||||
Example{
|
Example{
|
||||||
Description: "Send a test notification. Administrators can use this to verify the notification target settings.",
|
Description: "Send a test notification. Administrators can use this to verify the notification target settings",
|
||||||
Command: "coder notifications test",
|
Command: "coder notifications test",
|
||||||
},
|
},
|
||||||
|
Example{
|
||||||
|
Description: "Send a custom notification to the requesting user. Sending notifications targeting other users or groups is currently not supported",
|
||||||
|
Command: "coder notifications custom \"Custom Title\" \"Custom Message\"",
|
||||||
|
},
|
||||||
),
|
),
|
||||||
Aliases: []string{"notification"},
|
Aliases: []string{"notification"},
|
||||||
Handler: func(inv *serpent.Invocation) error {
|
Handler: func(inv *serpent.Invocation) error {
|
||||||
@@ -36,6 +40,7 @@ func (r *RootCmd) notifications() *serpent.Command {
|
|||||||
r.pauseNotifications(),
|
r.pauseNotifications(),
|
||||||
r.resumeNotifications(),
|
r.resumeNotifications(),
|
||||||
r.testNotifications(),
|
r.testNotifications(),
|
||||||
|
r.customNotifications(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return cmd
|
return cmd
|
||||||
@@ -109,3 +114,30 @@ func (r *RootCmd) testNotifications() *serpent.Command {
|
|||||||
}
|
}
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *RootCmd) customNotifications() *serpent.Command {
|
||||||
|
client := new(codersdk.Client)
|
||||||
|
cmd := &serpent.Command{
|
||||||
|
Use: "custom <title> <message>",
|
||||||
|
Short: "Send a custom notification",
|
||||||
|
Middleware: serpent.Chain(
|
||||||
|
serpent.RequireNArgs(2),
|
||||||
|
r.InitClient(client),
|
||||||
|
),
|
||||||
|
Handler: func(inv *serpent.Invocation) error {
|
||||||
|
err := client.PostCustomNotification(inv.Context(), codersdk.CustomNotificationRequest{
|
||||||
|
Content: &codersdk.CustomNotificationContent{
|
||||||
|
Title: inv.Args[0],
|
||||||
|
Message: inv.Args[1],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("unable to post custom notification: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = fmt.Fprintln(inv.Stderr, "A custom notification has been sent.")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import (
|
|||||||
|
|
||||||
"github.com/coder/coder/v2/cli/clitest"
|
"github.com/coder/coder/v2/cli/clitest"
|
||||||
"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/database/dbgen"
|
||||||
"github.com/coder/coder/v2/coderd/notifications"
|
"github.com/coder/coder/v2/coderd/notifications"
|
||||||
"github.com/coder/coder/v2/coderd/notifications/notificationstest"
|
"github.com/coder/coder/v2/coderd/notifications/notificationstest"
|
||||||
"github.com/coder/coder/v2/codersdk"
|
"github.com/coder/coder/v2/codersdk"
|
||||||
@@ -166,3 +168,102 @@ func TestNotificationsTest(t *testing.T) {
|
|||||||
require.Len(t, sent, 0)
|
require.Len(t, sent, 0)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCustomNotifications(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("BadRequest", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
notifyEnq := ¬ificationstest.FakeEnqueuer{}
|
||||||
|
|
||||||
|
ownerClient := coderdtest.New(t, &coderdtest.Options{
|
||||||
|
DeploymentValues: coderdtest.DeploymentValues(t),
|
||||||
|
NotificationsEnqueuer: notifyEnq,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Given: A member user
|
||||||
|
ownerUser := coderdtest.CreateFirstUser(t, ownerClient)
|
||||||
|
memberClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, ownerUser.OrganizationID)
|
||||||
|
|
||||||
|
// When: The member user attempts to send a custom notification with empty title and message
|
||||||
|
inv, root := clitest.New(t, "notifications", "custom", "", "")
|
||||||
|
clitest.SetupConfig(t, memberClient, root)
|
||||||
|
|
||||||
|
// Then: an error is expected with no notifications sent
|
||||||
|
err := inv.Run()
|
||||||
|
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 body", sdkError.Message)
|
||||||
|
|
||||||
|
sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateTestNotification))
|
||||||
|
require.Len(t, sent, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("SystemUserNotAllowed", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
notifyEnq := ¬ificationstest.FakeEnqueuer{}
|
||||||
|
|
||||||
|
ownerClient, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
|
||||||
|
DeploymentValues: coderdtest.DeploymentValues(t),
|
||||||
|
NotificationsEnqueuer: notifyEnq,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Given: A system user (prebuilds system user)
|
||||||
|
_, token := dbgen.APIKey(t, db, database.APIKey{
|
||||||
|
UserID: database.PrebuildsSystemUserID,
|
||||||
|
LoginType: database.LoginTypeNone,
|
||||||
|
})
|
||||||
|
systemUserClient := codersdk.New(ownerClient.URL)
|
||||||
|
systemUserClient.SetSessionToken(token)
|
||||||
|
|
||||||
|
// When: The system user attempts to send a custom notification
|
||||||
|
inv, root := clitest.New(t, "notifications", "custom", "Custom Title", "Custom Message")
|
||||||
|
clitest.SetupConfig(t, systemUserClient, root)
|
||||||
|
|
||||||
|
// Then: an error is expected with no notifications sent
|
||||||
|
err := inv.Run()
|
||||||
|
var sdkError *codersdk.Error
|
||||||
|
require.Error(t, err)
|
||||||
|
require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error")
|
||||||
|
require.Equal(t, http.StatusForbidden, sdkError.StatusCode())
|
||||||
|
require.Equal(t, "Forbidden", sdkError.Message)
|
||||||
|
|
||||||
|
sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateTestNotification))
|
||||||
|
require.Len(t, sent, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Success", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
notifyEnq := ¬ificationstest.FakeEnqueuer{}
|
||||||
|
|
||||||
|
ownerClient := coderdtest.New(t, &coderdtest.Options{
|
||||||
|
DeploymentValues: coderdtest.DeploymentValues(t),
|
||||||
|
NotificationsEnqueuer: notifyEnq,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Given: A member user
|
||||||
|
ownerUser := coderdtest.CreateFirstUser(t, ownerClient)
|
||||||
|
memberClient, memberUser := coderdtest.CreateAnotherUser(t, ownerClient, ownerUser.OrganizationID)
|
||||||
|
|
||||||
|
// When: The member user attempts to send a custom notification
|
||||||
|
inv, root := clitest.New(t, "notifications", "custom", "Custom Title", "Custom Message")
|
||||||
|
clitest.SetupConfig(t, memberClient, root)
|
||||||
|
|
||||||
|
// Then: we expect a custom notification to be sent to the member user
|
||||||
|
err := inv.Run()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateCustomNotification))
|
||||||
|
require.Len(t, sent, 1)
|
||||||
|
require.Equal(t, memberUser.ID, sent[0].UserID)
|
||||||
|
require.Len(t, sent[0].Labels, 2)
|
||||||
|
require.Equal(t, "Custom Title", sent[0].Labels["custom_title"])
|
||||||
|
require.Equal(t, "Custom Message", sent[0].Labels["custom_message"])
|
||||||
|
require.Equal(t, memberUser.ID.String(), sent[0].CreatedBy)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
+8
-2
@@ -12,7 +12,7 @@ USAGE:
|
|||||||
from
|
from
|
||||||
dispatching messages in case of the target outage (for example: unavailable
|
dispatching messages in case of the target outage (for example: unavailable
|
||||||
SMTP
|
SMTP
|
||||||
server or Webhook not responding).:
|
server or Webhook not responding):
|
||||||
|
|
||||||
$ coder notifications pause
|
$ coder notifications pause
|
||||||
|
|
||||||
@@ -22,11 +22,17 @@ USAGE:
|
|||||||
|
|
||||||
- Send a test notification. Administrators can use this to verify the
|
- Send a test notification. Administrators can use this to verify the
|
||||||
notification
|
notification
|
||||||
target settings.:
|
target settings:
|
||||||
|
|
||||||
$ coder notifications test
|
$ coder notifications test
|
||||||
|
|
||||||
|
- Send a custom notification to the requesting user. Sending notifications
|
||||||
|
targeting other users or groups is currently not supported:
|
||||||
|
|
||||||
|
$ coder notifications custom "Custom Title" "Custom Message"
|
||||||
|
|
||||||
SUBCOMMANDS:
|
SUBCOMMANDS:
|
||||||
|
custom Send a custom notification
|
||||||
pause Pause notifications
|
pause Pause notifications
|
||||||
resume Resume notifications
|
resume Resume notifications
|
||||||
test Send a test notification
|
test Send a test notification
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
coder v0.0.0-devel
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
coder notifications custom <title> <message>
|
||||||
|
|
||||||
|
Send a custom notification
|
||||||
|
|
||||||
|
———
|
||||||
|
Run `coder --help` for a list of global options.
|
||||||
Generated
+113
@@ -1673,6 +1673,60 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/notifications/custom": {
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Notifications"
|
||||||
|
],
|
||||||
|
"summary": "Send a custom notification",
|
||||||
|
"operationId": "send-a-custom-notification",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Provide a non-empty title or message",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.CustomNotificationRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No Content"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid request body",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.Response"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "System users cannot send custom notifications",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.Response"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Failed to send custom notification",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.Response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/notifications/dispatch-methods": {
|
"/notifications/dispatch-methods": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -1926,6 +1980,40 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/notifications/templates/custom": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Notifications"
|
||||||
|
],
|
||||||
|
"summary": "Get custom notification templates",
|
||||||
|
"operationId": "get-custom-notification-templates",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/codersdk.NotificationTemplate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Failed to retrieve 'custom' notifications template",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.Response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/notifications/templates/system": {
|
"/notifications/templates/system": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -1950,6 +2038,12 @@ const docTemplate = `{
|
|||||||
"$ref": "#/definitions/codersdk.NotificationTemplate"
|
"$ref": "#/definitions/codersdk.NotificationTemplate"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Failed to retrieve 'system' notifications template",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.Response"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12451,6 +12545,25 @@ const docTemplate = `{
|
|||||||
"CryptoKeyFeatureTailnetResume"
|
"CryptoKeyFeatureTailnetResume"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"codersdk.CustomNotificationContent": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"codersdk.CustomNotificationRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"$ref": "#/definitions/codersdk.CustomNotificationContent"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"codersdk.CustomRoleRequest": {
|
"codersdk.CustomRoleRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
Generated
+103
@@ -1456,6 +1456,54 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/notifications/custom": {
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumes": ["application/json"],
|
||||||
|
"produces": ["application/json"],
|
||||||
|
"tags": ["Notifications"],
|
||||||
|
"summary": "Send a custom notification",
|
||||||
|
"operationId": "send-a-custom-notification",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Provide a non-empty title or message",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.CustomNotificationRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No Content"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid request body",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.Response"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "System users cannot send custom notifications",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.Response"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Failed to send custom notification",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.Response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/notifications/dispatch-methods": {
|
"/notifications/dispatch-methods": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -1678,6 +1726,36 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/notifications/templates/custom": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"produces": ["application/json"],
|
||||||
|
"tags": ["Notifications"],
|
||||||
|
"summary": "Get custom notification templates",
|
||||||
|
"operationId": "get-custom-notification-templates",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/codersdk.NotificationTemplate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Failed to retrieve 'custom' notifications template",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.Response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/notifications/templates/system": {
|
"/notifications/templates/system": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -1698,6 +1776,12 @@
|
|||||||
"$ref": "#/definitions/codersdk.NotificationTemplate"
|
"$ref": "#/definitions/codersdk.NotificationTemplate"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Failed to retrieve 'system' notifications template",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.Response"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11106,6 +11190,25 @@
|
|||||||
"CryptoKeyFeatureTailnetResume"
|
"CryptoKeyFeatureTailnetResume"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"codersdk.CustomNotificationContent": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"codersdk.CustomNotificationRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"$ref": "#/definitions/codersdk.CustomNotificationContent"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"codersdk.CustomRoleRequest": {
|
"codersdk.CustomRoleRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -1578,9 +1578,11 @@ func New(options *Options) *API {
|
|||||||
r.Put("/settings", api.putNotificationsSettings)
|
r.Put("/settings", api.putNotificationsSettings)
|
||||||
r.Route("/templates", func(r chi.Router) {
|
r.Route("/templates", func(r chi.Router) {
|
||||||
r.Get("/system", api.systemNotificationTemplates)
|
r.Get("/system", api.systemNotificationTemplates)
|
||||||
|
r.Get("/custom", api.customNotificationTemplates)
|
||||||
})
|
})
|
||||||
r.Get("/dispatch-methods", api.notificationDispatchMethods)
|
r.Get("/dispatch-methods", api.notificationDispatchMethods)
|
||||||
r.Post("/test", api.postTestNotification)
|
r.Post("/test", api.postTestNotification)
|
||||||
|
r.Post("/custom", api.postCustomNotification)
|
||||||
})
|
})
|
||||||
r.Route("/tailnet", func(r chi.Router) {
|
r.Route("/tailnet", func(r chi.Router) {
|
||||||
r.Use(apiKeyMiddleware)
|
r.Use(apiKeyMiddleware)
|
||||||
|
|||||||
@@ -2281,8 +2281,8 @@ func (q *querier) GetNotificationTemplateByID(ctx context.Context, id uuid.UUID)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (q *querier) GetNotificationTemplatesByKind(ctx context.Context, kind database.NotificationTemplateKind) ([]database.NotificationTemplate, error) {
|
func (q *querier) GetNotificationTemplatesByKind(ctx context.Context, kind database.NotificationTemplateKind) ([]database.NotificationTemplate, error) {
|
||||||
// Anyone can read the system notification templates.
|
// Anyone can read the 'system' and 'custom' notification templates.
|
||||||
if kind == database.NotificationTemplateKindSystem {
|
if kind == database.NotificationTemplateKindSystem || kind == database.NotificationTemplateKindCustom {
|
||||||
return q.db.GetNotificationTemplatesByKind(ctx, kind)
|
return q.db.GetNotificationTemplatesByKind(ctx, kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-1
@@ -150,7 +150,8 @@ CREATE TYPE notification_method AS ENUM (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TYPE notification_template_kind AS ENUM (
|
CREATE TYPE notification_template_kind AS ENUM (
|
||||||
'system'
|
'system',
|
||||||
|
'custom'
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TYPE parameter_destination_scheme AS ENUM (
|
CREATE TYPE parameter_destination_scheme AS ENUM (
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- Remove Custom Notification template
|
||||||
|
DELETE FROM notification_templates WHERE id = '39b1e189-c857-4b0c-877a-511144c18516';
|
||||||
|
|
||||||
|
-- Recreate the old enum without 'custom'
|
||||||
|
CREATE TYPE old_notification_template_kind AS ENUM ('system');
|
||||||
|
|
||||||
|
-- Update notification_templates to use the old enum
|
||||||
|
ALTER TABLE notification_templates
|
||||||
|
ALTER COLUMN kind DROP DEFAULT,
|
||||||
|
ALTER COLUMN kind TYPE old_notification_template_kind USING (kind::text::old_notification_template_kind),
|
||||||
|
ALTER COLUMN kind SET DEFAULT 'system'::old_notification_template_kind;
|
||||||
|
|
||||||
|
-- Drop the current enum and restore the original name
|
||||||
|
DROP TYPE notification_template_kind;
|
||||||
|
ALTER TYPE old_notification_template_kind RENAME TO notification_template_kind;
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
-- Create new enum with 'custom' value
|
||||||
|
CREATE TYPE new_notification_template_kind AS ENUM (
|
||||||
|
'system',
|
||||||
|
'custom'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Update the notification_templates table to use new enum
|
||||||
|
ALTER TABLE notification_templates
|
||||||
|
ALTER COLUMN kind DROP DEFAULT,
|
||||||
|
ALTER COLUMN kind TYPE new_notification_template_kind USING (kind::text::new_notification_template_kind),
|
||||||
|
ALTER COLUMN kind SET DEFAULT 'system'::new_notification_template_kind;
|
||||||
|
|
||||||
|
-- Drop old enum and rename new one
|
||||||
|
DROP TYPE notification_template_kind;
|
||||||
|
ALTER TYPE new_notification_template_kind RENAME TO notification_template_kind;
|
||||||
|
|
||||||
|
-- Insert new Custom Notification template with 'custom' kind
|
||||||
|
INSERT INTO notification_templates (
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
title_template,
|
||||||
|
body_template,
|
||||||
|
actions,
|
||||||
|
"group",
|
||||||
|
method,
|
||||||
|
kind,
|
||||||
|
enabled_by_default
|
||||||
|
) VALUES (
|
||||||
|
'39b1e189-c857-4b0c-877a-511144c18516',
|
||||||
|
'Custom Notification',
|
||||||
|
'{{.Labels.custom_title}}',
|
||||||
|
'{{.Labels.custom_message}}',
|
||||||
|
'[]',
|
||||||
|
'Custom Events',
|
||||||
|
NULL,
|
||||||
|
'custom'::notification_template_kind,
|
||||||
|
true
|
||||||
|
);
|
||||||
@@ -1201,6 +1201,7 @@ type NotificationTemplateKind string
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
NotificationTemplateKindSystem NotificationTemplateKind = "system"
|
NotificationTemplateKindSystem NotificationTemplateKind = "system"
|
||||||
|
NotificationTemplateKindCustom NotificationTemplateKind = "custom"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (e *NotificationTemplateKind) Scan(src interface{}) error {
|
func (e *NotificationTemplateKind) Scan(src interface{}) error {
|
||||||
@@ -1240,7 +1241,8 @@ func (ns NullNotificationTemplateKind) Value() (driver.Value, error) {
|
|||||||
|
|
||||||
func (e NotificationTemplateKind) Valid() bool {
|
func (e NotificationTemplateKind) Valid() bool {
|
||||||
switch e {
|
switch e {
|
||||||
case NotificationTemplateKindSystem:
|
case NotificationTemplateKindSystem,
|
||||||
|
NotificationTemplateKindCustom:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -1249,6 +1251,7 @@ func (e NotificationTemplateKind) Valid() bool {
|
|||||||
func AllNotificationTemplateKindValues() []NotificationTemplateKind {
|
func AllNotificationTemplateKindValues() []NotificationTemplateKind {
|
||||||
return []NotificationTemplateKind{
|
return []NotificationTemplateKind{
|
||||||
NotificationTemplateKindSystem,
|
NotificationTemplateKindSystem,
|
||||||
|
NotificationTemplateKindCustom,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+115
-10
@@ -3,7 +3,9 @@ package coderd
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
|
||||||
@@ -124,20 +126,14 @@ func (api *API) putNotificationsSettings(rw http.ResponseWriter, r *http.Request
|
|||||||
httpapi.Write(r.Context(), rw, http.StatusOK, settings)
|
httpapi.Write(r.Context(), rw, http.StatusOK, settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Summary Get system notification templates
|
// notificationTemplatesByKind gets the notification templates by kind
|
||||||
// @ID get-system-notification-templates
|
func (api *API) notificationTemplatesByKind(rw http.ResponseWriter, r *http.Request, kind database.NotificationTemplateKind) {
|
||||||
// @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()
|
ctx := r.Context()
|
||||||
|
|
||||||
templates, err := api.Database.GetNotificationTemplatesByKind(ctx, database.NotificationTemplateKindSystem)
|
templates, err := api.Database.GetNotificationTemplatesByKind(ctx, kind)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
|
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
|
||||||
Message: "Failed to retrieve system notifications templates.",
|
Message: fmt.Sprintf("Failed to retrieve %q notifications templates.", kind),
|
||||||
Detail: err.Error(),
|
Detail: err.Error(),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@@ -147,6 +143,30 @@ func (api *API) systemNotificationTemplates(rw http.ResponseWriter, r *http.Requ
|
|||||||
httpapi.Write(r.Context(), rw, http.StatusOK, out)
|
httpapi.Write(r.Context(), rw, http.StatusOK, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary Get system notification templates
|
||||||
|
// @ID get-system-notification-templates
|
||||||
|
// @Security CoderSessionToken
|
||||||
|
// @Produce json
|
||||||
|
// @Tags Notifications
|
||||||
|
// @Success 200 {array} codersdk.NotificationTemplate
|
||||||
|
// @Failure 500 {object} codersdk.Response "Failed to retrieve 'system' notifications template"
|
||||||
|
// @Router /notifications/templates/system [get]
|
||||||
|
func (api *API) systemNotificationTemplates(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
api.notificationTemplatesByKind(rw, r, database.NotificationTemplateKindSystem)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Get custom notification templates
|
||||||
|
// @ID get-custom-notification-templates
|
||||||
|
// @Security CoderSessionToken
|
||||||
|
// @Produce json
|
||||||
|
// @Tags Notifications
|
||||||
|
// @Success 200 {array} codersdk.NotificationTemplate
|
||||||
|
// @Failure 500 {object} codersdk.Response "Failed to retrieve 'custom' notifications template"
|
||||||
|
// @Router /notifications/templates/custom [get]
|
||||||
|
func (api *API) customNotificationTemplates(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
api.notificationTemplatesByKind(rw, r, database.NotificationTemplateKindCustom)
|
||||||
|
}
|
||||||
|
|
||||||
// @Summary Get notification dispatch methods
|
// @Summary Get notification dispatch methods
|
||||||
// @ID get-notification-dispatch-methods
|
// @ID get-notification-dispatch-methods
|
||||||
// @Security CoderSessionToken
|
// @Security CoderSessionToken
|
||||||
@@ -323,6 +343,91 @@ func (api *API) putUserNotificationPreferences(rw http.ResponseWriter, r *http.R
|
|||||||
httpapi.Write(ctx, rw, http.StatusOK, out)
|
httpapi.Write(ctx, rw, http.StatusOK, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary Send a custom notification
|
||||||
|
// @ID send-a-custom-notification
|
||||||
|
// @Security CoderSessionToken
|
||||||
|
// @Tags Notifications
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body codersdk.CustomNotificationRequest true "Provide a non-empty title or message"
|
||||||
|
// @Success 204 "No Content"
|
||||||
|
// @Failure 400 {object} codersdk.Response "Invalid request body"
|
||||||
|
// @Failure 403 {object} codersdk.Response "System users cannot send custom notifications"
|
||||||
|
// @Failure 500 {object} codersdk.Response "Failed to send custom notification"
|
||||||
|
// @Router /notifications/custom [post]
|
||||||
|
func (api *API) postCustomNotification(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
ctx = r.Context()
|
||||||
|
apiKey = httpmw.APIKey(r)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parse request
|
||||||
|
var req codersdk.CustomNotificationRequest
|
||||||
|
if !httpapi.Read(ctx, rw, r, &req) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate request: require `content` and non-empty `title` and `message`
|
||||||
|
if err := req.Validate(); err != nil {
|
||||||
|
api.Logger.Error(ctx, "send custom notification: validation failed", slog.Error(err))
|
||||||
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||||
|
Message: "Invalid request body",
|
||||||
|
Detail: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block system users from sending custom notifications
|
||||||
|
user, err := api.Database.GetUserByID(ctx, apiKey.UserID)
|
||||||
|
if err != nil {
|
||||||
|
api.Logger.Error(ctx, "send custom notification", slog.Error(err))
|
||||||
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||||
|
Message: "Failed to send custom notification",
|
||||||
|
Detail: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if user.IsSystem {
|
||||||
|
api.Logger.Error(ctx, "send custom notification: system user is not allowed",
|
||||||
|
slog.F("id", user.ID.String()), slog.F("name", user.Name))
|
||||||
|
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
|
||||||
|
Message: "Forbidden",
|
||||||
|
Detail: "System users cannot send custom notifications.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := api.NotificationsEnqueuer.EnqueueWithData(
|
||||||
|
//nolint:gocritic // We need to be notifier to send the notification.
|
||||||
|
dbauthz.AsNotifier(ctx),
|
||||||
|
user.ID,
|
||||||
|
notifications.TemplateCustomNotification,
|
||||||
|
map[string]string{
|
||||||
|
"custom_title": req.Content.Title,
|
||||||
|
"custom_message": req.Content.Message,
|
||||||
|
},
|
||||||
|
map[string]any{
|
||||||
|
// Current dedupe is done via an hash of (template, user, method, payload, targets, day).
|
||||||
|
// Include a minute-bucketed timestamp to bypass per-day dedupe for self-sends,
|
||||||
|
// letting the caller resend identical content the same day (but not more than
|
||||||
|
// once per minute).
|
||||||
|
// TODO(ssncferreira): When custom notifications can target multiple users/roles,
|
||||||
|
// enforce proper deduplication across recipients to reduce noise and prevent spam.
|
||||||
|
"dedupe_bypass_ts": api.Clock.Now().UTC().Truncate(time.Minute),
|
||||||
|
},
|
||||||
|
user.ID.String(),
|
||||||
|
); err != nil {
|
||||||
|
api.Logger.Error(ctx, "send custom notification", slog.Error(err))
|
||||||
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||||
|
Message: "Failed to send custom notification",
|
||||||
|
Detail: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rw.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
func convertNotificationTemplates(in []database.NotificationTemplate) (out []codersdk.NotificationTemplate) {
|
func convertNotificationTemplates(in []database.NotificationTemplate) (out []codersdk.NotificationTemplate) {
|
||||||
for _, tmpl := range in {
|
for _, tmpl := range in {
|
||||||
out = append(out, codersdk.NotificationTemplate{
|
out = append(out, codersdk.NotificationTemplate{
|
||||||
|
|||||||
@@ -49,5 +49,6 @@ var (
|
|||||||
|
|
||||||
// Notification-related events.
|
// Notification-related events.
|
||||||
var (
|
var (
|
||||||
TemplateTestNotification = uuid.MustParse("c425f63e-716a-4bf4-ae24-78348f706c3f")
|
TemplateTestNotification = uuid.MustParse("c425f63e-716a-4bf4-ae24-78348f706c3f")
|
||||||
|
TemplateCustomNotification = uuid.MustParse("39b1e189-c857-4b0c-877a-511144c18516")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1258,6 +1258,20 @@ func TestNotificationTemplates_Golden(t *testing.T) {
|
|||||||
Data: map[string]any{},
|
Data: map[string]any{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "TemplateCustomNotification",
|
||||||
|
id: notifications.TemplateCustomNotification,
|
||||||
|
payload: types.MessagePayload{
|
||||||
|
UserName: "Bobby",
|
||||||
|
UserEmail: "bobby@coder.com",
|
||||||
|
UserUsername: "bobby",
|
||||||
|
Labels: map[string]string{
|
||||||
|
"custom_title": "Custom Title",
|
||||||
|
"custom_message": "Custom Message",
|
||||||
|
},
|
||||||
|
Data: map[string]any{},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// We must have a test case for every notification_template. This is enforced below:
|
// We must have a test case for every notification_template. This is enforced below:
|
||||||
|
|||||||
Vendored
+68
@@ -0,0 +1,68 @@
|
|||||||
|
From: system@coder.com
|
||||||
|
To: bobby@coder.com
|
||||||
|
Subject: Custom Title
|
||||||
|
Message-Id: 02ee4935-73be-4fa1-a290-ff9999026b13@blush-whale-48
|
||||||
|
Date: Fri, 11 Oct 2024 09:03:06 +0000
|
||||||
|
Content-Type: multipart/alternative; boundary=bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
|
||||||
|
MIME-Version: 1.0
|
||||||
|
|
||||||
|
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
Content-Type: text/plain; charset=UTF-8
|
||||||
|
|
||||||
|
Hi Bobby,
|
||||||
|
|
||||||
|
Custom Message
|
||||||
|
|
||||||
|
|
||||||
|
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
Content-Type: text/html; charset=UTF-8
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang=3D"en">
|
||||||
|
<head>
|
||||||
|
<meta charset=3D"UTF-8" />
|
||||||
|
<meta name=3D"viewport" content=3D"width=3Ddevice-width, initial-scale=
|
||||||
|
=3D1.0" />
|
||||||
|
<title>Custom Title</title>
|
||||||
|
</head>
|
||||||
|
<body style=3D"margin: 0; padding: 0; font-family: -apple-system, system-=
|
||||||
|
ui, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarel=
|
||||||
|
l', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; color: #020617=
|
||||||
|
; background: #f8fafc;">
|
||||||
|
<div style=3D"max-width: 600px; margin: 20px auto; padding: 60px; borde=
|
||||||
|
r: 1px solid #e2e8f0; border-radius: 8px; background-color: #fff; text-alig=
|
||||||
|
n: left; font-size: 14px; line-height: 1.5;">
|
||||||
|
<div style=3D"text-align: center;">
|
||||||
|
<img src=3D"https://coder.com/coder-logo-horizontal.png" alt=3D"Cod=
|
||||||
|
er Logo" style=3D"height: 40px;" />
|
||||||
|
</div>
|
||||||
|
<h1 style=3D"text-align: center; font-size: 24px; font-weight: 400; m=
|
||||||
|
argin: 8px 0 32px; line-height: 1.5;">
|
||||||
|
Custom Title
|
||||||
|
</h1>
|
||||||
|
<div style=3D"line-height: 1.5;">
|
||||||
|
<p>Hi Bobby,</p>
|
||||||
|
<p>Custom Message</p>
|
||||||
|
</div>
|
||||||
|
<div style=3D"text-align: center; margin-top: 32px;">
|
||||||
|
=20
|
||||||
|
</div>
|
||||||
|
<div style=3D"border-top: 1px solid #e2e8f0; color: #475569; font-siz=
|
||||||
|
e: 12px; margin-top: 64px; padding-top: 24px; line-height: 1.6;">
|
||||||
|
<p>© 2024 Coder. All rights reserved - <a =
|
||||||
|
href=3D"http://test.com" style=3D"color: #2563eb; text-decoration: none;">h=
|
||||||
|
ttp://test.com</a></p>
|
||||||
|
<p><a href=3D"http://test.com/settings/notifications" style=3D"colo=
|
||||||
|
r: #2563eb; text-decoration: none;">Click here to manage your notification =
|
||||||
|
settings</a></p>
|
||||||
|
<p><a href=3D"http://test.com/settings/notifications?disabled=3D39b=
|
||||||
|
1e189-c857-4b0c-877a-511144c18516" style=3D"color: #2563eb; text-decoration=
|
||||||
|
: none;">Stop receiving emails like this</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4--
|
||||||
Vendored
+24
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"_version": "1.1",
|
||||||
|
"msg_id": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"payload": {
|
||||||
|
"_version": "1.2",
|
||||||
|
"notification_name": "Custom Notification",
|
||||||
|
"notification_template_id": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"user_id": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"user_email": "bobby@coder.com",
|
||||||
|
"user_name": "Bobby",
|
||||||
|
"user_username": "bobby",
|
||||||
|
"actions": [],
|
||||||
|
"labels": {
|
||||||
|
"custom_message": "Custom Message",
|
||||||
|
"custom_title": "Custom Title"
|
||||||
|
},
|
||||||
|
"data": {},
|
||||||
|
"targets": null
|
||||||
|
},
|
||||||
|
"title": "Custom Title",
|
||||||
|
"title_markdown": "Custom Title",
|
||||||
|
"body": "Custom Message",
|
||||||
|
"body_markdown": "Custom Message"
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"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/database"
|
||||||
|
"github.com/coder/coder/v2/coderd/database/dbgen"
|
||||||
"github.com/coder/coder/v2/coderd/notifications"
|
"github.com/coder/coder/v2/coderd/notifications"
|
||||||
"github.com/coder/coder/v2/coderd/notifications/notificationstest"
|
"github.com/coder/coder/v2/coderd/notifications/notificationstest"
|
||||||
"github.com/coder/coder/v2/codersdk"
|
"github.com/coder/coder/v2/codersdk"
|
||||||
@@ -376,3 +377,113 @@ func TestNotificationTest(t *testing.T) {
|
|||||||
require.Len(t, sent, 0)
|
require.Len(t, sent, 0)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCustomNotification(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("BadRequest", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := testutil.Context(t, testutil.WaitShort)
|
||||||
|
|
||||||
|
notifyEnq := ¬ificationstest.FakeEnqueuer{}
|
||||||
|
ownerClient := coderdtest.New(t, &coderdtest.Options{
|
||||||
|
DeploymentValues: coderdtest.DeploymentValues(t),
|
||||||
|
NotificationsEnqueuer: notifyEnq,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Given: A member user
|
||||||
|
ownerUser := coderdtest.CreateFirstUser(t, ownerClient)
|
||||||
|
memberClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, ownerUser.OrganizationID)
|
||||||
|
|
||||||
|
// When: The member user attempts to send a custom notification with empty title and message
|
||||||
|
err := memberClient.PostCustomNotification(ctx, codersdk.CustomNotificationRequest{
|
||||||
|
Content: &codersdk.CustomNotificationContent{
|
||||||
|
Title: "",
|
||||||
|
Message: "",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Then: a bad request error is expected with no notifications sent
|
||||||
|
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 body", sdkError.Message)
|
||||||
|
|
||||||
|
sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateTestNotification))
|
||||||
|
require.Len(t, sent, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("SystemUserNotAllowed", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := testutil.Context(t, testutil.WaitShort)
|
||||||
|
|
||||||
|
notifyEnq := ¬ificationstest.FakeEnqueuer{}
|
||||||
|
ownerClient, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
|
||||||
|
DeploymentValues: coderdtest.DeploymentValues(t),
|
||||||
|
NotificationsEnqueuer: notifyEnq,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Given: A system user (prebuilds system user)
|
||||||
|
_, token := dbgen.APIKey(t, db, database.APIKey{
|
||||||
|
UserID: database.PrebuildsSystemUserID,
|
||||||
|
LoginType: database.LoginTypeNone,
|
||||||
|
})
|
||||||
|
systemUserClient := codersdk.New(ownerClient.URL)
|
||||||
|
systemUserClient.SetSessionToken(token)
|
||||||
|
|
||||||
|
// When: The system user attempts to send a custom notification
|
||||||
|
err := systemUserClient.PostCustomNotification(ctx, codersdk.CustomNotificationRequest{
|
||||||
|
Content: &codersdk.CustomNotificationContent{
|
||||||
|
Title: "Custom Title",
|
||||||
|
Message: "Custom Message",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Then: a forbidden error is expected with no notifications sent
|
||||||
|
var sdkError *codersdk.Error
|
||||||
|
require.Error(t, err)
|
||||||
|
require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error")
|
||||||
|
require.Equal(t, http.StatusForbidden, sdkError.StatusCode())
|
||||||
|
require.Equal(t, "Forbidden", sdkError.Message)
|
||||||
|
|
||||||
|
sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateTestNotification))
|
||||||
|
require.Len(t, sent, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Success", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := testutil.Context(t, testutil.WaitShort)
|
||||||
|
|
||||||
|
notifyEnq := ¬ificationstest.FakeEnqueuer{}
|
||||||
|
ownerClient := coderdtest.New(t, &coderdtest.Options{
|
||||||
|
DeploymentValues: coderdtest.DeploymentValues(t),
|
||||||
|
NotificationsEnqueuer: notifyEnq,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Given: A member user
|
||||||
|
ownerUser := coderdtest.CreateFirstUser(t, ownerClient)
|
||||||
|
memberClient, memberUser := coderdtest.CreateAnotherUser(t, ownerClient, ownerUser.OrganizationID)
|
||||||
|
|
||||||
|
// When: The member user attempts to send a custom notification
|
||||||
|
err := memberClient.PostCustomNotification(ctx, codersdk.CustomNotificationRequest{
|
||||||
|
Content: &codersdk.CustomNotificationContent{
|
||||||
|
Title: "Custom Title",
|
||||||
|
Message: "Custom Message",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Then: we expect a custom notification to be sent to the member user
|
||||||
|
sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateCustomNotification))
|
||||||
|
require.Len(t, sent, 1)
|
||||||
|
require.Equal(t, memberUser.ID, sent[0].UserID)
|
||||||
|
require.Len(t, sent[0].Labels, 2)
|
||||||
|
require.Equal(t, "Custom Title", sent[0].Labels["custom_title"])
|
||||||
|
require.Equal(t, "Custom Message", sent[0].Labels["custom_message"])
|
||||||
|
require.Equal(t, memberUser.ID.String(), sent[0].CreatedBy)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -280,3 +281,53 @@ func (c *Client) PostTestWebpushMessage(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CustomNotificationContent struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomNotificationRequest struct {
|
||||||
|
Content *CustomNotificationContent `json:"content"`
|
||||||
|
// TODO(ssncferreira): Add target (user_ids, roles) to support multi-user and role-based delivery.
|
||||||
|
// See: https://github.com/coder/coder/issues/19768
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxCustomNotificationTitleLen = 120
|
||||||
|
maxCustomNotificationMessageLen = 2000
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c CustomNotificationRequest) Validate() error {
|
||||||
|
if c.Content == nil {
|
||||||
|
return xerrors.Errorf("content is required")
|
||||||
|
}
|
||||||
|
return c.Content.Validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CustomNotificationContent) Validate() error {
|
||||||
|
if strings.TrimSpace(c.Title) == "" ||
|
||||||
|
strings.TrimSpace(c.Message) == "" {
|
||||||
|
return xerrors.Errorf("provide a non-empty 'content.title' and 'content.message'")
|
||||||
|
}
|
||||||
|
if len(c.Title) > maxCustomNotificationTitleLen {
|
||||||
|
return xerrors.Errorf("'content.title' must be less than %d characters", maxCustomNotificationTitleLen)
|
||||||
|
}
|
||||||
|
if len(c.Message) > maxCustomNotificationMessageLen {
|
||||||
|
return xerrors.Errorf("'content.message' must be less than %d characters", maxCustomNotificationMessageLen)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) PostCustomNotification(ctx context.Context, req CustomNotificationRequest) error {
|
||||||
|
res, err := c.Request(ctx, http.MethodPost, "/api/v2/notifications/custom", req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
if res.StatusCode != http.StatusNoContent {
|
||||||
|
return ReadBodyAsError(res)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -143,9 +143,11 @@ After setting the required fields above:
|
|||||||
```text
|
```text
|
||||||
CODER_EMAIL_SMARTHOST=smtp.gmail.com:465
|
CODER_EMAIL_SMARTHOST=smtp.gmail.com:465
|
||||||
CODER_EMAIL_AUTH_USERNAME=<user>@<domain>
|
CODER_EMAIL_AUTH_USERNAME=<user>@<domain>
|
||||||
CODER_EMAIL_AUTH_PASSWORD="<app password created above>"
|
CODER_EMAIL_AUTH_PASSWORD="<app password created above (no spaces)>"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Note:** The `CODER_EMAIL_AUTH_PASSWORD` must be entered without spaces.
|
||||||
|
|
||||||
See
|
See
|
||||||
[this help article from Google](https://support.google.com/a/answer/176600?hl=en)
|
[this help article from Google](https://support.google.com/a/answer/176600?hl=en)
|
||||||
for more options.
|
for more options.
|
||||||
@@ -261,6 +263,43 @@ Administrators can configure which delivery methods are used for each different
|
|||||||
You can find this page under
|
You can find this page under
|
||||||
`https://$CODER_ACCESS_URL/deployment/notifications?tab=events`.
|
`https://$CODER_ACCESS_URL/deployment/notifications?tab=events`.
|
||||||
|
|
||||||
|
## Custom notifications
|
||||||
|
|
||||||
|
Custom notifications let you send an ad‑hoc notification to yourself using the Coder CLI.
|
||||||
|
These are useful for surfacing the result of long-running tasks or important state changes.
|
||||||
|
At this time, custom notifications can only be sent to the user making the request.
|
||||||
|
|
||||||
|
To send a custom notification, execute [`coder notifications custom <title> <message>`](../../../reference/cli/notifications_custom.md).
|
||||||
|
|
||||||
|
<!-- TODO(ssncferreira): Update when sending custom notifications to multiple users/roles is supported.
|
||||||
|
Explain deduplication behaviour for multiple users/roles.
|
||||||
|
See: https://github.com/coder/coder/issues/19768
|
||||||
|
-->
|
||||||
|
**Note:** The recipient is always the requesting user as targeting other users or groups isn’t supported yet.
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
- Send yourself a quick update:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
coder templates push -y && coder notifications custom "Template push complete" "Template version uploaded."
|
||||||
|
```
|
||||||
|
|
||||||
|
- Use in a script after a long-running task:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
if make test 2>&1 | tee test_output.log; then
|
||||||
|
coder notifications custom "Tests Succeeded" $'Test results:\n • ✅ success'
|
||||||
|
else
|
||||||
|
failures=$(grep -Po '\d+(?=\s+failures)' test_output.log | tail -n1 || echo 0)
|
||||||
|
coder notifications custom "Tests Failed" $'Test results:\n • ❌ failed ('"$failures"' tests failed)'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
## Stop sending notifications
|
## Stop sending notifications
|
||||||
|
|
||||||
Administrators may wish to stop _all_ notifications across the deployment. We
|
Administrators may wish to stop _all_ notifications across the deployment. We
|
||||||
|
|||||||
@@ -1287,6 +1287,11 @@
|
|||||||
"description": "Manage Coder notifications",
|
"description": "Manage Coder notifications",
|
||||||
"path": "reference/cli/notifications.md"
|
"path": "reference/cli/notifications.md"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "notifications custom",
|
||||||
|
"description": "Send a custom notification",
|
||||||
|
"path": "reference/cli/notifications_custom.md"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "notifications pause",
|
"title": "notifications pause",
|
||||||
"description": "Pause notifications",
|
"description": "Pause notifications",
|
||||||
|
|||||||
Generated
+122
-3
@@ -1,5 +1,64 @@
|
|||||||
# Notifications
|
# Notifications
|
||||||
|
|
||||||
|
## Send a custom notification
|
||||||
|
|
||||||
|
### Code samples
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Example request using curl
|
||||||
|
curl -X POST http://coder-server:8080/api/v2/notifications/custom \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-H 'Accept: application/json' \
|
||||||
|
-H 'Coder-Session-Token: API_KEY'
|
||||||
|
```
|
||||||
|
|
||||||
|
`POST /notifications/custom`
|
||||||
|
|
||||||
|
> Body parameter
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"message": "string",
|
||||||
|
"title": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | In | Type | Required | Description |
|
||||||
|
|--------|------|------------------------------------------------------------------------------------|----------|--------------------------------------|
|
||||||
|
| `body` | body | [codersdk.CustomNotificationRequest](schemas.md#codersdkcustomnotificationrequest) | true | Provide a non-empty title or message |
|
||||||
|
|
||||||
|
### Example responses
|
||||||
|
|
||||||
|
> 400 Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"detail": "string",
|
||||||
|
"message": "string",
|
||||||
|
"validations": [
|
||||||
|
{
|
||||||
|
"detail": "string",
|
||||||
|
"field": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Responses
|
||||||
|
|
||||||
|
| Status | Meaning | Description | Schema |
|
||||||
|
|--------|----------------------------------------------------------------------------|-----------------------------------------------|--------------------------------------------------|
|
||||||
|
| 204 | [No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5) | No Content | |
|
||||||
|
| 400 | [Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1) | Invalid request body | [codersdk.Response](schemas.md#codersdkresponse) |
|
||||||
|
| 403 | [Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3) | System users cannot send custom notifications | [codersdk.Response](schemas.md#codersdkresponse) |
|
||||||
|
| 500 | [Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1) | Failed to send custom notification | [codersdk.Response](schemas.md#codersdkresponse) |
|
||||||
|
|
||||||
|
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||||
|
|
||||||
## Get notification dispatch methods
|
## Get notification dispatch methods
|
||||||
|
|
||||||
### Code samples
|
### Code samples
|
||||||
@@ -315,6 +374,65 @@ curl -X PUT http://coder-server:8080/api/v2/notifications/settings \
|
|||||||
|
|
||||||
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 custom notification templates
|
||||||
|
|
||||||
|
### Code samples
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Example request using curl
|
||||||
|
curl -X GET http://coder-server:8080/api/v2/notifications/templates/custom \
|
||||||
|
-H 'Accept: application/json' \
|
||||||
|
-H 'Coder-Session-Token: API_KEY'
|
||||||
|
```
|
||||||
|
|
||||||
|
`GET /notifications/templates/custom`
|
||||||
|
|
||||||
|
### Example responses
|
||||||
|
|
||||||
|
> 200 Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"actions": "string",
|
||||||
|
"body_template": "string",
|
||||||
|
"enabled_by_default": true,
|
||||||
|
"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) |
|
||||||
|
| 500 | [Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1) | Failed to retrieve 'custom' notifications template | [codersdk.Response](schemas.md#codersdkresponse) |
|
||||||
|
|
||||||
|
<h3 id="get-custom-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 | | |
|
||||||
|
| `» enabled_by_default` | boolean | 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 system notification templates
|
## Get system notification templates
|
||||||
|
|
||||||
### Code samples
|
### Code samples
|
||||||
@@ -350,9 +468,10 @@ curl -X GET http://coder-server:8080/api/v2/notifications/templates/system \
|
|||||||
|
|
||||||
### Responses
|
### Responses
|
||||||
|
|
||||||
| Status | Meaning | Description | Schema |
|
| Status | Meaning | Description | Schema |
|
||||||
|--------|---------------------------------------------------------|-------------|-----------------------------------------------------------------------------------|
|
|--------|----------------------------------------------------------------------------|----------------------------------------------------|-----------------------------------------------------------------------------------|
|
||||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.NotificationTemplate](schemas.md#codersdknotificationtemplate) |
|
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.NotificationTemplate](schemas.md#codersdknotificationtemplate) |
|
||||||
|
| 500 | [Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1) | Failed to retrieve 'system' notifications template | [codersdk.Response](schemas.md#codersdkresponse) |
|
||||||
|
|
||||||
<h3 id="get-system-notification-templates-responseschema">Response Schema</h3>
|
<h3 id="get-system-notification-templates-responseschema">Response Schema</h3>
|
||||||
|
|
||||||
|
|||||||
Generated
+33
@@ -1872,6 +1872,39 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
|||||||
| `oidc_convert` |
|
| `oidc_convert` |
|
||||||
| `tailnet_resume` |
|
| `tailnet_resume` |
|
||||||
|
|
||||||
|
## codersdk.CustomNotificationContent
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "string",
|
||||||
|
"title": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Properties
|
||||||
|
|
||||||
|
| Name | Type | Required | Restrictions | Description |
|
||||||
|
|-----------|--------|----------|--------------|-------------|
|
||||||
|
| `message` | string | false | | |
|
||||||
|
| `title` | string | false | | |
|
||||||
|
|
||||||
|
## codersdk.CustomNotificationRequest
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"message": "string",
|
||||||
|
"title": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Properties
|
||||||
|
|
||||||
|
| Name | Type | Required | Restrictions | Description |
|
||||||
|
|-----------|--------------------------------------------------------------------------|----------|--------------|-------------|
|
||||||
|
| `content` | [codersdk.CustomNotificationContent](#codersdkcustomnotificationcontent) | false | | |
|
||||||
|
|
||||||
## codersdk.CustomRoleRequest
|
## codersdk.CustomRoleRequest
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
Generated
+13
-7
@@ -19,7 +19,7 @@ coder notifications
|
|||||||
Administrators can use these commands to change notification settings.
|
Administrators can use these commands to change notification settings.
|
||||||
- Pause Coder notifications. Administrators can temporarily stop notifiers from
|
- Pause Coder notifications. Administrators can temporarily stop notifiers from
|
||||||
dispatching messages in case of the target outage (for example: unavailable SMTP
|
dispatching messages in case of the target outage (for example: unavailable SMTP
|
||||||
server or Webhook not responding).:
|
server or Webhook not responding):
|
||||||
|
|
||||||
$ coder notifications pause
|
$ coder notifications pause
|
||||||
|
|
||||||
@@ -28,15 +28,21 @@ server or Webhook not responding).:
|
|||||||
$ coder notifications resume
|
$ coder notifications resume
|
||||||
|
|
||||||
- Send a test notification. Administrators can use this to verify the notification
|
- Send a test notification. Administrators can use this to verify the notification
|
||||||
target settings.:
|
target settings:
|
||||||
|
|
||||||
$ coder notifications test
|
$ coder notifications test
|
||||||
|
|
||||||
|
- Send a custom notification to the requesting user. Sending notifications
|
||||||
|
targeting other users or groups is currently not supported:
|
||||||
|
|
||||||
|
$ coder notifications custom "Custom Title" "Custom Message"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Subcommands
|
## Subcommands
|
||||||
|
|
||||||
| Name | Purpose |
|
| Name | Purpose |
|
||||||
|--------------------------------------------------|--------------------------|
|
|--------------------------------------------------|----------------------------|
|
||||||
| [<code>pause</code>](./notifications_pause.md) | Pause notifications |
|
| [<code>pause</code>](./notifications_pause.md) | Pause notifications |
|
||||||
| [<code>resume</code>](./notifications_resume.md) | Resume notifications |
|
| [<code>resume</code>](./notifications_resume.md) | Resume notifications |
|
||||||
| [<code>test</code>](./notifications_test.md) | Send a test notification |
|
| [<code>test</code>](./notifications_test.md) | Send a test notification |
|
||||||
|
| [<code>custom</code>](./notifications_custom.md) | Send a custom notification |
|
||||||
|
|||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
<!-- DO NOT EDIT | GENERATED CONTENT -->
|
||||||
|
# notifications custom
|
||||||
|
|
||||||
|
Send a custom notification
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```console
|
||||||
|
coder notifications custom <title> <message>
|
||||||
|
```
|
||||||
Generated
+17
@@ -629,6 +629,17 @@ export const CryptoKeyFeatures: CryptoKeyFeature[] = [
|
|||||||
"workspace_apps_token",
|
"workspace_apps_token",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// From codersdk/notifications.go
|
||||||
|
export interface CustomNotificationContent {
|
||||||
|
readonly title: string;
|
||||||
|
readonly message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// From codersdk/notifications.go
|
||||||
|
export interface CustomNotificationRequest {
|
||||||
|
readonly content: CustomNotificationContent | null;
|
||||||
|
}
|
||||||
|
|
||||||
// From codersdk/roles.go
|
// From codersdk/roles.go
|
||||||
export interface CustomRoleRequest {
|
export interface CustomRoleRequest {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
@@ -4129,6 +4140,12 @@ export const annotationSecretKey = "secret";
|
|||||||
// From codersdk/insights.go
|
// From codersdk/insights.go
|
||||||
export const insightsTimeLayout = "2006-01-02T15:04:05Z07:00";
|
export const insightsTimeLayout = "2006-01-02T15:04:05Z07:00";
|
||||||
|
|
||||||
|
// From codersdk/notifications.go
|
||||||
|
export const maxCustomNotificationMessageLen = 2000;
|
||||||
|
|
||||||
|
// From codersdk/notifications.go
|
||||||
|
export const maxCustomNotificationTitleLen = 120;
|
||||||
|
|
||||||
// From healthsdk/interfaces.go
|
// From healthsdk/interfaces.go
|
||||||
export const safeMTU = 1378;
|
export const safeMTU = 1378;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user