diff --git a/Makefile b/Makefile index 42258d82170b5..5ddc629b25b9b 100644 --- a/Makefile +++ b/Makefile @@ -667,6 +667,7 @@ update-golden-files: \ enterprise/tailnet/testdata/.gen-golden \ tailnet/testdata/.gen-golden \ coderd/.gen-golden \ + coderd/notifications/.gen-golden \ provisioner/terraform/testdata/.gen-golden .PHONY: update-golden-files @@ -698,6 +699,10 @@ coderd/.gen-golden: $(wildcard coderd/testdata/*/*.golden) $(GO_SRC_FILES) $(wil go test ./coderd -run="Test.*Golden$$" -update touch "$@" +coderd/notifications/.gen-golden: $(wildcard coderd/notifications/testdata/*/*.golden) $(GO_SRC_FILES) $(wildcard coderd/notifications/*_test.go) + go test ./coderd/notifications -run="Test.*Golden$$" -update + touch "$@" + provisioner/terraform/testdata/.gen-golden: $(wildcard provisioner/terraform/testdata/*/*.golden) $(GO_SRC_FILES) $(wildcard provisioner/terraform/*_test.go) go test ./provisioner/terraform -run="Test.*Golden$$" -update touch "$@" diff --git a/coderd/database/migrations/000262_improve_notification_templates.down.sql b/coderd/database/migrations/000262_improve_notification_templates.down.sql new file mode 100644 index 0000000000000..62a2799e52caa --- /dev/null +++ b/coderd/database/migrations/000262_improve_notification_templates.down.sql @@ -0,0 +1,84 @@ +UPDATE notification_templates +SET + body_template = E'Hi {{.UserName}},\nUser account **{{.Labels.suspended_account_name}}** has been suspended.' +WHERE + id = 'b02ddd82-4733-4d02-a2d7-c36f3598997d'; + +UPDATE notification_templates +SET + body_template = E'Hi {{.UserName}},\nYour account **{{.Labels.suspended_account_name}}** has been suspended.' +WHERE + id = '6a2f0609-9b69-4d36-a989-9f5925b6cbff'; + +UPDATE notification_templates +SET + body_template = E'Hi {{.UserName}},\nUser account **{{.Labels.activated_account_name}}** has been activated.' +WHERE + id = '9f5af851-8408-4e73-a7a1-c6502ba46689'; + +UPDATE notification_templates +SET + body_template = E'Hi {{.UserName}},\nYour account **{{.Labels.activated_account_name}}** has been activated.' +WHERE + id = '1a6a6bea-ee0a-43e2-9e7c-eabdb53730e4'; + +UPDATE notification_templates +SET + body_template = E'Hi {{.UserName}},\n\New user account **{{.Labels.created_account_name}}** has been created.' +WHERE + id = '4e19c0ac-94e1-4532-9515-d1801aa283b2'; + +UPDATE notification_templates +SET + body_template = E'Hi {{.UserName}},\n\nUser account **{{.Labels.deleted_account_name}}** has been deleted.' +WHERE + id = 'f44d9314-ad03-4bc8-95d0-5cad491da6b6'; + +UPDATE notification_templates +SET + body_template = E'Hi {{.UserName}}\n\n' || + E'The template **{{.Labels.name}}** was deleted by **{{ .Labels.initiator }}**.' +WHERE + id = '29a09665-2a4c-403f-9648-54301670e7be'; + +UPDATE notification_templates +SET body_template = E'Hi {{.UserName}}\n' || + E'Your workspace **{{.Labels.name}}** has been updated automatically to the latest template version ({{.Labels.template_version_name}}).\n' || + E'Reason for update: **{{.Labels.template_version_message}}**' +WHERE + id = 'c34a0c09-0704-4cac-bd1c-0c0146811c2b'; + +UPDATE notification_templates +SET + body_template = E'Hi {{.UserName}}\n\nYour workspace **{{.Labels.name}}** was deleted.\nThe specified reason was "**{{.Labels.reason}}{{ if .Labels.initiator }} ({{ .Labels.initiator }}){{end}}**".' +WHERE + id = '381df2a9-c0c0-4749-420f-80a9280c66f9'; + +UPDATE notification_templates +SET + body_template = E'Hi {{.UserName}}\n\nYour workspace **{{.Labels.name}}** was deleted.\nThe specified reason was "**{{.Labels.reason}}{{ if .Labels.initiator }} ({{ .Labels.initiator }}){{end}}**".' +WHERE + id = 'f517da0b-cdc9-410f-ab89-a86107c420ed'; + +UPDATE notification_templates +SET + body_template = E'Hi {{.UserName}}\n\n' || + E'Your workspace **{{.Labels.name}}** has been marked as [**dormant**](https://coder.com/docs/templates/schedule#dormancy-threshold-enterprise) because of {{.Labels.reason}}.\n' || + E'Dormant workspaces are [automatically deleted](https://coder.com/docs/templates/schedule#dormancy-auto-deletion-enterprise) after {{.Labels.timeTilDormant}} of inactivity.\n' || + E'To prevent deletion, use your workspace with the link below.' +WHERE + id = '0ea69165-ec14-4314-91f1-69566ac3c5a0'; + +UPDATE notification_templates +SET + body_template = E'Hi {{.UserName}}\n\n' || + E'Your workspace **{{.Labels.name}}** has been marked for **deletion** after {{.Labels.timeTilDormant}} of [dormancy](https://coder.com/docs/templates/schedule#dormancy-auto-deletion-enterprise) because of {{.Labels.reason}}.\n' || + E'To prevent deletion, use your workspace with the link below.' +WHERE + id = '51ce2fdf-c9ca-4be1-8d70-628674f9bc42'; + +UPDATE notification_templates +SET + body_template = E'Hi {{.UserName}},\n\nA manual build of the workspace **{{.Labels.name}}** using the template **{{.Labels.template_name}}** failed (version: **{{.Labels.template_version_name}}**).\nThe workspace build was initiated by **{{.Labels.initiator}}**.' +WHERE + id = '2faeee0f-26cb-4e96-821c-85ccb9f71513'; diff --git a/coderd/database/migrations/000262_improve_notification_templates.up.sql b/coderd/database/migrations/000262_improve_notification_templates.up.sql new file mode 100644 index 0000000000000..12dab392e2b20 --- /dev/null +++ b/coderd/database/migrations/000262_improve_notification_templates.up.sql @@ -0,0 +1,128 @@ +-- https://github.com/coder/coder/issues/14893 + +-- UserAccountSuspended +UPDATE notification_templates +SET + body_template = E'Hi {{.UserName}},\n\n' || -- Add a \n + E'User account **{{.Labels.suspended_account_name}}** has been suspended.\n\n' || + -- Mention the real name of the user who suspended the account: + E'The newly suspended account belongs to **{{.Labels.suspended_account_user_name}}** and was suspended by **{{.Labels.account_suspender_user_name}}**.' +WHERE + id = 'b02ddd82-4733-4d02-a2d7-c36f3598997d'; + +-- YourAccountSuspended +UPDATE notification_templates +SET + body_template = E'Hi {{.UserName}},\n\n' || -- Add a \n + -- Mention who suspended the account: + E'Your account **{{.Labels.suspended_account_name}}** has been suspended by **{{.Labels.account_suspender_user_name}}**.' +WHERE + id = '6a2f0609-9b69-4d36-a989-9f5925b6cbff'; + +-- UserAccountActivated +UPDATE notification_templates +SET + body_template = E'Hi {{.UserName}},\n\n' || -- Add a \n + E'User account **{{.Labels.activated_account_name}}** has been activated.\n\n' || + -- Mention the real name of the user who activated the account: + E'The newly activated account belongs to **{{.Labels.activated_account_user_name}}** and was activated by **{{.Labels.account_activator_user_name}}**.' +WHERE + id = '9f5af851-8408-4e73-a7a1-c6502ba46689'; + +-- YourAccountActivated +UPDATE notification_templates +SET + body_template = E'Hi {{.UserName}},\n\n' || -- Add a \n + -- Mention who activated the account: + E'Your account **{{.Labels.activated_account_name}}** has been activated by **{{.Labels.account_activator_user_name}}**.' +WHERE + id = '1a6a6bea-ee0a-43e2-9e7c-eabdb53730e4'; + +-- UserAccountCreated +UPDATE notification_templates +SET + body_template = E'Hi {{.UserName}},\n\n' || + E'New user account **{{.Labels.created_account_name}}** has been created.\n\n' || + -- Mention the real name of the user who created the account: + E'This new user account was created for **{{.Labels.created_account_user_name}}** by **{{.Labels.account_creator}}**.' +WHERE + id = '4e19c0ac-94e1-4532-9515-d1801aa283b2'; + +-- UserAccountDeleted +UPDATE notification_templates +SET + body_template = E'Hi {{.UserName}},\n\n' || + E'User account **{{.Labels.deleted_account_name}}** has been deleted.\n\n' || + -- Mention the real name of the user who deleted the account: + E'The deleted account belonged to **{{.Labels.deleted_account_user_name}}** and was deleted by **{{.Labels.account_deleter_user_name}}**.' +WHERE + id = 'f44d9314-ad03-4bc8-95d0-5cad491da6b6'; + +-- TemplateDeleted +UPDATE notification_templates +SET + body_template = E'Hi {{.UserName}},\n\n' || -- Add a comma + E'The template **{{.Labels.name}}** was deleted by **{{ .Labels.initiator }}**.\n\n' || + -- Mention template display name: + E'The template''s display name was **{{.Labels.display_name}}**.' +WHERE + id = '29a09665-2a4c-403f-9648-54301670e7be'; + +-- WorkspaceAutoUpdated +UPDATE notification_templates +SET body_template = E'Hi {{.UserName}},\n\n' || -- Add a comma and a \n + -- Add a \n: + E'Your workspace **{{.Labels.name}}** has been updated automatically to the latest template version ({{.Labels.template_version_name}}).\n\n' || + E'Reason for update: **{{.Labels.template_version_message}}**.' +WHERE + id = 'c34a0c09-0704-4cac-bd1c-0c0146811c2b'; + +-- WorkspaceAutoBuildFailed +UPDATE notification_templates +SET + body_template = E'Hi {{.UserName}},\n\n' || -- Add a comma + -- Add a \n after: + E'Automatic build of your workspace **{{.Labels.name}}** failed.\n\n' || + E'The specified reason was "**{{.Labels.reason}}**".' +WHERE + id = '381df2a9-c0c0-4749-420f-80a9280c66f9'; + +-- WorkspaceDeleted +UPDATE notification_templates +SET + body_template = E'Hi {{.UserName}},\n\n' || -- Add a comma + -- Add a \n after: + E'Your workspace **{{.Labels.name}}** was deleted.\n\n' || + E'The specified reason was "**{{.Labels.reason}}{{ if .Labels.initiator }} ({{ .Labels.initiator }}){{end}}**".' +WHERE + id = 'f517da0b-cdc9-410f-ab89-a86107c420ed'; + +-- WorkspaceDormant +UPDATE notification_templates +SET + body_template = E'Hi {{.UserName}},\n\n' || -- add comma + E'Your workspace **{{.Labels.name}}** has been marked as [**dormant**](https://coder.com/docs/templates/schedule#dormancy-threshold-enterprise) because of {{.Labels.reason}}.\n' || + E'Dormant workspaces are [automatically deleted](https://coder.com/docs/templates/schedule#dormancy-auto-deletion-enterprise) after {{.Labels.timeTilDormant}} of inactivity.\n' || + E'To prevent deletion, use your workspace with the link below.' +WHERE + id = '0ea69165-ec14-4314-91f1-69566ac3c5a0'; + +-- WorkspaceMarkedForDeletion +UPDATE notification_templates +SET + body_template = E'Hi {{.UserName}},\n\n' || -- add comma + E'Your workspace **{{.Labels.name}}** has been marked for **deletion** after {{.Labels.timeTilDormant}} of [dormancy](https://coder.com/docs/templates/schedule#dormancy-auto-deletion-enterprise) because of {{.Labels.reason}}.\n' || + E'To prevent deletion, use your workspace with the link below.' +WHERE + id = '51ce2fdf-c9ca-4be1-8d70-628674f9bc42'; + +-- WorkspaceManualBuildFailed +UPDATE notification_templates +SET + body_template = E'Hi {{.UserName}},\n\n' || + E'A manual build of the workspace **{{.Labels.name}}** using the template **{{.Labels.template_name}}** failed (version: **{{.Labels.template_version_name}}**).\n\n' || + -- Mention template display name: + E'The template''s display name was **{{.Labels.template_display_name}}**. ' || + E'The workspace build was initiated by **{{.Labels.initiator}}**.' +WHERE + id = '2faeee0f-26cb-4e96-821c-85ccb9f71513'; diff --git a/coderd/notifications/notifications_test.go b/coderd/notifications/notifications_test.go index 1878fcdd45007..28bd8b5190c71 100644 --- a/coderd/notifications/notifications_test.go +++ b/coderd/notifications/notifications_test.go @@ -684,7 +684,7 @@ func enumerateAllTemplates(t *testing.T) ([]string, error) { return out, nil } -func TestNotificationTemplatesCanRender(t *testing.T) { +func TestNotificationTemplates_Golden(t *testing.T) { t.Parallel() if !dbtestutil.WillUsePostgres() { @@ -764,7 +764,9 @@ func TestNotificationTemplatesCanRender(t *testing.T) { payload: types.MessagePayload{ UserName: "Bobby", Labels: map[string]string{ - "created_account_name": "bobby", + "created_account_name": "bobby", + "created_account_user_name": "William Tables", + "account_creator": "rob", }, }, }, @@ -774,7 +776,9 @@ func TestNotificationTemplatesCanRender(t *testing.T) { payload: types.MessagePayload{ UserName: "Bobby", Labels: map[string]string{ - "deleted_account_name": "bobby", + "deleted_account_name": "bobby", + "deleted_account_user_name": "william tables", + "account_deleter_user_name": "rob", }, }, }, @@ -784,7 +788,9 @@ func TestNotificationTemplatesCanRender(t *testing.T) { payload: types.MessagePayload{ UserName: "Bobby", Labels: map[string]string{ - "suspended_account_name": "bobby", + "suspended_account_name": "bobby", + "suspended_account_user_name": "william tables", + "account_suspender_user_name": "rob", }, }, }, @@ -794,7 +800,9 @@ func TestNotificationTemplatesCanRender(t *testing.T) { payload: types.MessagePayload{ UserName: "Bobby", Labels: map[string]string{ - "activated_account_name": "bobby", + "activated_account_name": "bobby", + "activated_account_user_name": "william tables", + "account_activator_user_name": "rob", }, }, }, @@ -804,7 +812,8 @@ func TestNotificationTemplatesCanRender(t *testing.T) { payload: types.MessagePayload{ UserName: "Bobby", Labels: map[string]string{ - "suspended_account_name": "bobby", + "suspended_account_name": "bobby", + "account_suspender_user_name": "rob", }, }, }, @@ -814,7 +823,8 @@ func TestNotificationTemplatesCanRender(t *testing.T) { payload: types.MessagePayload{ UserName: "Bobby", Labels: map[string]string{ - "activated_account_name": "bobby", + "activated_account_name": "bobby", + "account_activator_user_name": "rob", }, }, }, @@ -824,8 +834,9 @@ func TestNotificationTemplatesCanRender(t *testing.T) { payload: types.MessagePayload{ UserName: "Bobby", Labels: map[string]string{ - "name": "bobby-template", - "initiator": "rob", + "name": "bobby-template", + "display_name": "Bobby's Template", + "initiator": "rob", }, }, }, @@ -837,6 +848,7 @@ func TestNotificationTemplatesCanRender(t *testing.T) { Labels: map[string]string{ "name": "bobby-workspace", "template_name": "bobby-template", + "template_display_name": "William's Template", "template_version_name": "bobby-template-version", "initiator": "joe", "workspace_owner_username": "mrbobby", @@ -960,13 +972,15 @@ func TestNotificationTemplatesCanRender(t *testing.T) { return } + const hint = "run \"DB=ci make update-golden-files\" and commit the changes" + wantBody, err := os.ReadFile(bodyGoldenFile) - require.NoError(t, err, "open golden file, run \"DB=ci make update-golden-files\" and commit the changes") + require.NoError(t, err, fmt.Sprintf("missing golden notification body file. %s", hint)) wantTitle, err := os.ReadFile(titleGoldenFile) - require.NoError(t, err, "open golden file, run \"DB=ci make update-golden-files\" and commit the changes") + require.NoError(t, err, fmt.Sprintf("missing golden notification title file. %s", hint)) - require.Equal(t, string(wantBody), body, "body should be equal") - require.Equal(t, string(wantTitle), title, "title should be equal") + require.Equal(t, string(wantBody), body, fmt.Sprintf("rendered template body does not match golden file. If this is expected, %s", hint)) + require.Equal(t, string(wantTitle), title, fmt.Sprintf("rendered template title does not match golden file. If this is expected, %s", hint)) }) } } diff --git a/coderd/notifications/testdata/rendered-templates/TemplateTemplateDeleted-body.md.golden b/coderd/notifications/testdata/rendered-templates/TemplateTemplateDeleted-body.md.golden index be3a61e695652..ade9c87ff791a 100644 --- a/coderd/notifications/testdata/rendered-templates/TemplateTemplateDeleted-body.md.golden +++ b/coderd/notifications/testdata/rendered-templates/TemplateTemplateDeleted-body.md.golden @@ -1,3 +1,5 @@ -Hi Bobby +Hi Bobby, -The template **bobby-template** was deleted by **rob**. \ No newline at end of file +The template **bobby-template** was deleted by **rob**. + +The template's display name was **Bobby's Template**. \ No newline at end of file diff --git a/coderd/notifications/testdata/rendered-templates/TemplateUserAccountActivated-body.md.golden b/coderd/notifications/testdata/rendered-templates/TemplateUserAccountActivated-body.md.golden index 2665a781492ea..5a773a51dc181 100644 --- a/coderd/notifications/testdata/rendered-templates/TemplateUserAccountActivated-body.md.golden +++ b/coderd/notifications/testdata/rendered-templates/TemplateUserAccountActivated-body.md.golden @@ -1,2 +1,5 @@ Hi Bobby, -User account **bobby** has been activated. \ No newline at end of file + +User account **bobby** has been activated. + +The newly activated account belongs to **william tables** and was activated by **rob**. \ No newline at end of file diff --git a/coderd/notifications/testdata/rendered-templates/TemplateUserAccountCreated-body.md.golden b/coderd/notifications/testdata/rendered-templates/TemplateUserAccountCreated-body.md.golden index e5a5be89c11e0..88a46735f0483 100644 --- a/coderd/notifications/testdata/rendered-templates/TemplateUserAccountCreated-body.md.golden +++ b/coderd/notifications/testdata/rendered-templates/TemplateUserAccountCreated-body.md.golden @@ -1,3 +1,5 @@ Hi Bobby, -New user account **bobby** has been created. \ No newline at end of file +New user account **bobby** has been created. + +This new user account was created for **William Tables** by **rob**. \ No newline at end of file diff --git a/coderd/notifications/testdata/rendered-templates/TemplateUserAccountDeleted-body.md.golden b/coderd/notifications/testdata/rendered-templates/TemplateUserAccountDeleted-body.md.golden index bd1066c25fb50..4ca8bedd0ca52 100644 --- a/coderd/notifications/testdata/rendered-templates/TemplateUserAccountDeleted-body.md.golden +++ b/coderd/notifications/testdata/rendered-templates/TemplateUserAccountDeleted-body.md.golden @@ -1,3 +1,5 @@ Hi Bobby, -User account **bobby** has been deleted. \ No newline at end of file +User account **bobby** has been deleted. + +The deleted account belonged to **william tables** and was deleted by **rob**. \ No newline at end of file diff --git a/coderd/notifications/testdata/rendered-templates/TemplateUserAccountSuspended-body.md.golden b/coderd/notifications/testdata/rendered-templates/TemplateUserAccountSuspended-body.md.golden index 70a43f2960ec0..ecf276504ebc9 100644 --- a/coderd/notifications/testdata/rendered-templates/TemplateUserAccountSuspended-body.md.golden +++ b/coderd/notifications/testdata/rendered-templates/TemplateUserAccountSuspended-body.md.golden @@ -1,2 +1,5 @@ Hi Bobby, -User account **bobby** has been suspended. \ No newline at end of file + +User account **bobby** has been suspended. + +The newly suspended account belongs to **william tables** and was suspended by **rob**. \ No newline at end of file diff --git a/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceAutoUpdated-body.md.golden b/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceAutoUpdated-body.md.golden index 79248150987c2..d9f4e27cb4c6e 100644 --- a/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceAutoUpdated-body.md.golden +++ b/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceAutoUpdated-body.md.golden @@ -1,3 +1,5 @@ -Hi Bobby +Hi Bobby, + Your workspace **bobby-workspace** has been updated automatically to the latest template version (1.0). -Reason for update: **template now includes catnip** \ No newline at end of file + +Reason for update: **template now includes catnip**. \ No newline at end of file diff --git a/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceAutobuildFailed-body.md.golden b/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceAutobuildFailed-body.md.golden index 731f71f22ae88..cddf2149d0d46 100644 --- a/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceAutobuildFailed-body.md.golden +++ b/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceAutobuildFailed-body.md.golden @@ -1,3 +1,5 @@ -Hi Bobby +Hi Bobby, + Automatic build of your workspace **bobby-workspace** failed. + The specified reason was "**autostart**". \ No newline at end of file diff --git a/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceDeleted-body.md.golden b/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceDeleted-body.md.golden index 06aec5692465f..a5c71fb3e0170 100644 --- a/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceDeleted-body.md.golden +++ b/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceDeleted-body.md.golden @@ -1,4 +1,5 @@ -Hi Bobby +Hi Bobby, Your workspace **bobby-workspace** was deleted. + The specified reason was "**autodeleted due to dormancy (autobuild)**". \ No newline at end of file diff --git a/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceDormant-body.md.golden b/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceDormant-body.md.golden index aa10f4864cf24..35bfe8d2c19b6 100644 --- a/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceDormant-body.md.golden +++ b/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceDormant-body.md.golden @@ -1,4 +1,4 @@ -Hi Bobby +Hi Bobby, Your workspace **bobby-workspace** has been marked as [**dormant**](https://coder.com/docs/templates/schedule#dormancy-threshold-enterprise) because of breached the template's threshold for inactivity. Dormant workspaces are [automatically deleted](https://coder.com/docs/templates/schedule#dormancy-auto-deletion-enterprise) after 24 hours of inactivity. diff --git a/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceManualBuildFailed-body.md.golden b/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceManualBuildFailed-body.md.golden index 45f8733dd2931..e1091b2888830 100644 --- a/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceManualBuildFailed-body.md.golden +++ b/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceManualBuildFailed-body.md.golden @@ -1,4 +1,5 @@ Hi Bobby, A manual build of the workspace **bobby-workspace** using the template **bobby-template** failed (version: **bobby-template-version**). -The workspace build was initiated by **joe**. \ No newline at end of file + +The template's display name was **William's Template**. The workspace build was initiated by **joe**. \ No newline at end of file diff --git a/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceMarkedForDeletion-body.md.golden b/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceMarkedForDeletion-body.md.golden index 3d9fe99accd94..21defa7c1d500 100644 --- a/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceMarkedForDeletion-body.md.golden +++ b/coderd/notifications/testdata/rendered-templates/TemplateWorkspaceMarkedForDeletion-body.md.golden @@ -1,4 +1,4 @@ -Hi Bobby +Hi Bobby, Your workspace **bobby-workspace** has been marked for **deletion** after 24 hours of [dormancy](https://coder.com/docs/templates/schedule#dormancy-auto-deletion-enterprise) because of template updated to new dormancy policy. To prevent deletion, use your workspace with the link below. \ No newline at end of file diff --git a/coderd/notifications/testdata/rendered-templates/TemplateYourAccountActivated-body.md.golden b/coderd/notifications/testdata/rendered-templates/TemplateYourAccountActivated-body.md.golden index 160fdc66e8990..a3a8710de4673 100644 --- a/coderd/notifications/testdata/rendered-templates/TemplateYourAccountActivated-body.md.golden +++ b/coderd/notifications/testdata/rendered-templates/TemplateYourAccountActivated-body.md.golden @@ -1,2 +1,3 @@ Hi Bobby, -Your account **bobby** has been activated. \ No newline at end of file + +Your account **bobby** has been activated by **rob**. \ No newline at end of file diff --git a/coderd/notifications/testdata/rendered-templates/TemplateYourAccountSuspended-body.md.golden b/coderd/notifications/testdata/rendered-templates/TemplateYourAccountSuspended-body.md.golden index ce30139213bb0..86fc40401774b 100644 --- a/coderd/notifications/testdata/rendered-templates/TemplateYourAccountSuspended-body.md.golden +++ b/coderd/notifications/testdata/rendered-templates/TemplateYourAccountSuspended-body.md.golden @@ -1,2 +1,3 @@ Hi Bobby, -Your account **bobby** has been suspended. \ No newline at end of file + +Your account **bobby** has been suspended by **rob**. \ No newline at end of file diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 2db5bbd1062b1..0fc89b239a8d4 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -1128,6 +1128,7 @@ func (s *server) notifyWorkspaceManualBuildFailed(ctx context.Context, workspace map[string]string{ "name": workspace.Name, "template_name": template.Name, + "template_display_name": template.DisplayName, "template_version_name": templateVersion.Name, "initiator": build.InitiatorByUsername, "workspace_owner_username": workspaceOwner.Username, diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index d429ae1a1bcb1..32ae2efa648a0 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -1820,7 +1820,7 @@ func TestNotifications(t *testing.T) { _ = dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: user.ID, OrganizationID: pd.OrganizationID}) template := dbgen.Template(t, db, database.Template{ - Name: "template", Provisioner: database.ProvisionerTypeEcho, OrganizationID: pd.OrganizationID, + Name: "template", DisplayName: "William's Template", Provisioner: database.ProvisionerTypeEcho, OrganizationID: pd.OrganizationID, }) workspace := dbgen.Workspace(t, db, database.Workspace{ TemplateID: template.ID, OwnerID: user.ID, OrganizationID: pd.OrganizationID, @@ -1860,6 +1860,7 @@ func TestNotifications(t *testing.T) { assert.Contains(t, notifEnq.Sent[0].Targets, user.ID) assert.Equal(t, workspace.Name, notifEnq.Sent[0].Labels["name"]) assert.Equal(t, template.Name, notifEnq.Sent[0].Labels["template_name"]) + assert.Equal(t, template.DisplayName, notifEnq.Sent[0].Labels["template_display_name"]) assert.Equal(t, version.Name, notifEnq.Sent[0].Labels["template_version_name"]) assert.Equal(t, user.Username, notifEnq.Sent[0].Labels["initiator"]) assert.Equal(t, user.Username, notifEnq.Sent[0].Labels["workspace_owner_username"]) diff --git a/coderd/templates.go b/coderd/templates.go index dc32841b72aad..01c2a056bb813 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -137,8 +137,9 @@ func (api *API) notifyTemplateDeleted(ctx context.Context, template database.Tem if _, err := api.NotificationsEnqueuer.Enqueue(ctx, receiverID, notifications.TemplateTemplateDeleted, map[string]string{ - "name": template.Name, - "initiator": initiator.Username, + "name": template.Name, + "display_name": template.DisplayName, + "initiator": initiator.Username, }, "api-templates-delete", // Associate this notification with all the related entities. template.ID, template.OrganizationID, diff --git a/coderd/templates_test.go b/coderd/templates_test.go index ca8d9c4cf88f2..4273169d4ca82 100644 --- a/coderd/templates_test.go +++ b/coderd/templates_test.go @@ -1419,7 +1419,9 @@ func TestTemplateNotifications(t *testing.T) { // Setup template version = coderdtest.CreateTemplateVersion(t, client, initiator.OrganizationID, nil) _ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) - template = coderdtest.CreateTemplate(t, client, initiator.OrganizationID, version.ID) + template = coderdtest.CreateTemplate(t, client, initiator.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { + ctr.DisplayName = "Bobby's Template" + }) ) // Setup users with different roles @@ -1455,6 +1457,7 @@ func TestTemplateNotifications(t *testing.T) { require.Contains(t, n.Targets, template.ID) require.Contains(t, n.Targets, template.OrganizationID) require.Equal(t, n.Labels["name"], template.Name) + require.Equal(t, n.Labels["display_name"], template.DisplayName) require.Equal(t, n.Labels["initiator"], coderdtest.FirstUserParams.Username) } }) diff --git a/coderd/userauth.go b/coderd/userauth.go index a1e1252797de3..0ff3dfa8f97cc 100644 --- a/coderd/userauth.go +++ b/coderd/userauth.go @@ -1481,14 +1481,15 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C Username: params.Username, OrganizationIDs: orgIDs, }, - LoginType: params.LoginType, + LoginType: params.LoginType, + accountCreatorName: "oauth", }) if err != nil { return xerrors.Errorf("create user: %w", err) } } - // Activate dormant user on sigin + // Activate dormant user on sign-in if user.Status == database.UserStatusDormant { //nolint:gocritic // System needs to update status of the user account (dormant -> active). user, err = tx.UpdateUserStatus(dbauthz.AsSystemRestricted(ctx), database.UpdateUserStatusParams{ diff --git a/coderd/users.go b/coderd/users.go index 48bc3ee15e4c5..2356bf2211a61 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -194,7 +194,8 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) { Password: createUser.Password, OrganizationIDs: []uuid.UUID{defaultOrg.ID}, }, - LoginType: database.LoginTypePassword, + LoginType: database.LoginTypePassword, + accountCreatorName: "coder", }) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ @@ -479,10 +480,22 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) { return } + apiKey := httpmw.APIKey(r) + + accountCreator, err := api.Database.GetUserByID(ctx, apiKey.UserID) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Unable to determine the details of the actor creating the account.", + }) + return + } + user, err := api.CreateUser(ctx, api.Database, CreateUserRequest{ CreateUserRequestWithOrgs: req, LoginType: loginType, + accountCreatorName: accountCreator.Name, }) + if dbauthz.IsNotAuthorizedError(err) { httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{ Message: "You are not authorized to create users.", @@ -576,11 +589,24 @@ func (api *API) deleteUser(rw http.ResponseWriter, r *http.Request) { return } + apiKey := httpmw.APIKey(r) + + accountDeleter, err := api.Database.GetUserByID(ctx, apiKey.UserID) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Unable to determine the details of the actor deleting the account.", + }) + return + } + for _, u := range userAdmins { if _, err := api.NotificationsEnqueuer.Enqueue(ctx, u.ID, notifications.TemplateUserAccountDeleted, map[string]string{ - "deleted_account_name": user.Username, - }, "api-users-delete", + "deleted_account_name": user.Username, + "deleted_account_user_name": user.Name, + "account_deleter_user_name": accountDeleter.Name, + }, + "api-users-delete", user.ID, ); err != nil { api.Logger.Warn(ctx, "unable to notify about deleted user", slog.F("deleted_user", user.Username), slog.Error(err)) @@ -844,6 +870,14 @@ func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseW } } + actingUser, err := api.Database.GetUserByID(ctx, apiKey.UserID) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Unable to determine the details of the actor creating the account.", + }) + return + } + targetUser, err := api.Database.UpdateUserStatus(ctx, database.UpdateUserStatusParams{ ID: user.ID, Status: status, @@ -858,7 +892,7 @@ func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseW } aReq.New = targetUser - err = api.notifyUserStatusChanged(ctx, user, status) + err = api.notifyUserStatusChanged(ctx, actingUser.Name, user, status) if err != nil { api.Logger.Warn(ctx, "unable to notify about changed user's status", slog.F("affected_user", user.Username), slog.Error(err)) } @@ -871,24 +905,33 @@ func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseW }) return } + httpapi.Write(ctx, rw, http.StatusOK, db2sdk.User(targetUser, organizations)) } } -func (api *API) notifyUserStatusChanged(ctx context.Context, user database.User, status database.UserStatus) error { - var key string +func (api *API) notifyUserStatusChanged(ctx context.Context, actingUserName string, targetUser database.User, status database.UserStatus) error { + var labels map[string]string var adminTemplateID, personalTemplateID uuid.UUID switch status { case database.UserStatusSuspended: - key = "suspended_account_name" + labels = map[string]string{ + "suspended_account_name": targetUser.Username, + "suspended_account_user_name": targetUser.Name, + "account_suspender_user_name": actingUserName, + } adminTemplateID = notifications.TemplateUserAccountSuspended personalTemplateID = notifications.TemplateYourAccountSuspended case database.UserStatusActive: - key = "activated_account_name" + labels = map[string]string{ + "activated_account_name": targetUser.Username, + "activated_account_user_name": targetUser.Name, + "account_activator_user_name": actingUserName, + } adminTemplateID = notifications.TemplateUserAccountActivated personalTemplateID = notifications.TemplateYourAccountActivated default: - api.Logger.Error(ctx, "user status is not supported", slog.F("username", user.Username), slog.F("user_status", string(status))) + api.Logger.Error(ctx, "user status is not supported", slog.F("username", targetUser.Username), slog.F("user_status", string(status))) return xerrors.Errorf("unable to notify admins as the user's status is unsupported") } @@ -900,21 +943,17 @@ func (api *API) notifyUserStatusChanged(ctx context.Context, user database.User, // Send notifications to user admins and affected user for _, u := range userAdmins { if _, err := api.NotificationsEnqueuer.Enqueue(ctx, u.ID, adminTemplateID, - map[string]string{ - key: user.Username, - }, "api-put-user-status", - user.ID, + labels, "api-put-user-status", + targetUser.ID, ); err != nil { - api.Logger.Warn(ctx, "unable to notify about changed user's status", slog.F("affected_user", user.Username), slog.Error(err)) + api.Logger.Warn(ctx, "unable to notify about changed user's status", slog.F("affected_user", targetUser.Username), slog.Error(err)) } } - if _, err := api.NotificationsEnqueuer.Enqueue(ctx, user.ID, personalTemplateID, - map[string]string{ - key: user.Username, - }, "api-put-user-status", - user.ID, + if _, err := api.NotificationsEnqueuer.Enqueue(ctx, targetUser.ID, personalTemplateID, + labels, "api-put-user-status", + targetUser.ID, ); err != nil { - api.Logger.Warn(ctx, "unable to notify user about status change of their account", slog.F("affected_user", user.Username), slog.Error(err)) + api.Logger.Warn(ctx, "unable to notify user about status change of their account", slog.F("affected_user", targetUser.Username), slog.Error(err)) } return nil } @@ -1280,8 +1319,9 @@ func (api *API) organizationByUserAndName(rw http.ResponseWriter, r *http.Reques type CreateUserRequest struct { codersdk.CreateUserRequestWithOrgs - LoginType database.LoginType - SkipNotifications bool + LoginType database.LoginType + SkipNotifications bool + accountCreatorName string } func (api *API) CreateUser(ctx context.Context, store database.Store, req CreateUserRequest) (database.User, error) { @@ -1365,13 +1405,16 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create for _, u := range userAdmins { if _, err := api.NotificationsEnqueuer.Enqueue(ctx, u.ID, notifications.TemplateUserAccountCreated, map[string]string{ - "created_account_name": user.Username, + "created_account_name": user.Username, + "created_account_user_name": user.Name, + "account_creator": req.accountCreatorName, }, "api-users-create", user.ID, ); err != nil { api.Logger.Warn(ctx, "unable to notify about created user", slog.F("created_user", user.Username), slog.Error(err)) } } + return user, err } diff --git a/coderd/users_test.go b/coderd/users_test.go index 1ab052d4aa470..7406df75bf1db 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -489,13 +489,16 @@ func TestNotifyDeletedUser(t *testing.T) { adminClient := coderdtest.New(t, &coderdtest.Options{ NotificationsEnqueuer: notifyEnq, }) - firstUser := coderdtest.CreateFirstUser(t, adminClient) + firstUserResponse := coderdtest.CreateFirstUser(t, adminClient) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() + firstUser, err := adminClient.User(ctx, firstUserResponse.UserID.String()) + require.NoError(t, err) + user, err := adminClient.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ - OrganizationIDs: []uuid.UUID{firstUser.OrganizationID}, + OrganizationIDs: []uuid.UUID{firstUserResponse.OrganizationID}, Email: "another@user.org", Username: "someone-else", Password: "SomeSecurePassword!", @@ -510,9 +513,11 @@ func TestNotifyDeletedUser(t *testing.T) { require.Len(t, notifyEnq.Sent, 2) // notifyEnq.Sent[0] is create account event require.Equal(t, notifications.TemplateUserAccountDeleted, notifyEnq.Sent[1].TemplateID) - require.Equal(t, firstUser.UserID, notifyEnq.Sent[1].UserID) + require.Equal(t, firstUser.ID, notifyEnq.Sent[1].UserID) require.Contains(t, notifyEnq.Sent[1].Targets, user.ID) require.Equal(t, user.Username, notifyEnq.Sent[1].Labels["deleted_account_name"]) + require.Equal(t, user.Name, notifyEnq.Sent[1].Labels["deleted_account_user_name"]) + require.Equal(t, firstUser.Name, notifyEnq.Sent[1].Labels["account_deleter_user_name"]) }) t.Run("UserAdminNotified", func(t *testing.T) {