Skip to content

Commit fde9663

Browse files
committed
Basic implementation
Signed-off-by: Danny Kopping <danny@coder.com>
1 parent bdd9263 commit fde9663

File tree

4 files changed

+211
-0
lines changed

4 files changed

+211
-0
lines changed

coderd/coderd.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1252,6 +1252,10 @@ func New(options *Options) *API {
12521252
r.Route("/templates", func(r chi.Router) {
12531253
r.Get("/system", api.systemNotificationTemplates)
12541254
})
1255+
r.Route("/preferences", func(r chi.Router) {
1256+
r.Get("/", api.userNotificationPreferences)
1257+
r.Put("/", api.putUserNotificationPreferences)
1258+
})
12551259
})
12561260
})
12571261

coderd/notifications.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,90 @@ func (api *API) systemNotificationTemplates(rw http.ResponseWriter, r *http.Requ
141141
httpapi.Write(r.Context(), rw, http.StatusOK, out)
142142
}
143143

144+
// @Summary TODO Get notification templates pertaining to system events
145+
// @ID TODO system-notification-templates
146+
// @Security TODO CoderSessionToken
147+
// @Produce TODO json
148+
// @Tags TODO Notifications
149+
// @Success TODO 200 {array} codersdk.NotificationTemplate
150+
// @Router TODO /notifications/templates/system [get]
151+
func (api *API) userNotificationPreferences(rw http.ResponseWriter, r *http.Request) {
152+
ctx := r.Context()
153+
key := httpmw.APIKey(r)
154+
155+
if !api.Authorize(r, policy.ActionReadPersonal, rbac.ResourceNotificationPreference) {
156+
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
157+
Message: "Insufficient permissions to update notification preferences.",
158+
})
159+
return
160+
}
161+
162+
prefs, err := api.Database.GetUserNotificationPreferences(ctx, key.UserID)
163+
if err != nil {
164+
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
165+
Message: "Failed to retrieve user notification preferences.",
166+
Detail: err.Error(),
167+
})
168+
return
169+
}
170+
171+
out := convertNotificationPreferences(prefs)
172+
httpapi.Write(r.Context(), rw, http.StatusOK, out)
173+
}
174+
175+
// @Summary TODO Get notification templates pertaining to system events
176+
// @ID TODO system-notification-templates
177+
// @Security TODO CoderSessionToken
178+
// @Produce TODO json
179+
// @Tags TODO Notifications
180+
// @Success TODO 200 {array} codersdk.NotificationTemplate
181+
// @Router TODO /notifications/templates/system [put]
182+
func (api *API) putUserNotificationPreferences(rw http.ResponseWriter, r *http.Request) {
183+
ctx := r.Context()
184+
185+
if !api.Authorize(r, policy.ActionUpdatePersonal, rbac.ResourceNotificationPreference) {
186+
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
187+
Message: "Insufficient permissions to update notification preferences.",
188+
})
189+
return
190+
}
191+
192+
var prefs codersdk.UpdateUserNotificationPreferences
193+
if !httpapi.Read(ctx, rw, r, &prefs) {
194+
return
195+
}
196+
197+
input := database.UpdateUserNotificationPreferencesParams{
198+
NotificationTemplateIds: make([]uuid.UUID, 0, len(prefs.TemplateDisabledMap)),
199+
Disableds: make([]bool, 0, len(prefs.TemplateDisabledMap)),
200+
}
201+
for tmplID, disabled := range prefs.TemplateDisabledMap {
202+
id, err := uuid.Parse(tmplID)
203+
if err != nil {
204+
httpapi.Write(r.Context(), rw, http.StatusBadRequest, codersdk.Response{
205+
Message: "Unable to parse notification template UUID.",
206+
Detail: err.Error(),
207+
})
208+
return
209+
}
210+
211+
input.NotificationTemplateIds = append(input.NotificationTemplateIds, id)
212+
input.Disableds = append(input.Disableds, disabled)
213+
}
214+
215+
updated, err := api.Database.UpdateUserNotificationPreferences(ctx, input)
216+
if err != nil {
217+
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
218+
Message: "Failed to update notifications preferences.",
219+
Detail: err.Error(),
220+
})
221+
return
222+
}
223+
224+
out := convertNotificationPreferences(updated)
225+
httpapi.Write(r.Context(), rw, http.StatusOK, out)
226+
}
227+
144228
func convertNotificationTemplates(in []database.NotificationTemplate) (out []codersdk.NotificationTemplate) {
145229
for _, tmpl := range in {
146230
out = append(out, codersdk.NotificationTemplate{
@@ -157,3 +241,15 @@ func convertNotificationTemplates(in []database.NotificationTemplate) (out []cod
157241

158242
return out
159243
}
244+
245+
func convertNotificationPreferences(in []database.NotificationPreference) (out []codersdk.NotificationPreference) {
246+
for _, pref := range in {
247+
out = append(out, codersdk.NotificationPreference{
248+
NotificationTemplateID: pref.NotificationTemplateID,
249+
Disabled: pref.Disabled,
250+
UpdatedAt: pref.UpdatedAt,
251+
})
252+
}
253+
254+
return out
255+
}

coderd/notifications_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/stretchr/testify/require"
88

99
"github.com/coder/coder/v2/coderd/coderdtest"
10+
"github.com/coder/coder/v2/coderd/notifications"
1011
"github.com/coder/coder/v2/codersdk"
1112
"github.com/coder/coder/v2/testutil"
1213
)
@@ -103,3 +104,52 @@ func TestUpdateNotificationsSettings(t *testing.T) {
103104
require.Equal(t, expected.NotifierPaused, actual.NotifierPaused)
104105
})
105106
}
107+
108+
func TestNotificationPreferences(t *testing.T) {
109+
t.Parallel()
110+
111+
t.Run("Initial state", func(t *testing.T) {
112+
t.Parallel()
113+
114+
ctx := testutil.Context(t, testutil.WaitShort)
115+
116+
// Given: the first user in its initial state.
117+
client := coderdtest.New(t, createOpts(t))
118+
_ = coderdtest.CreateFirstUser(t, client)
119+
120+
// When: calling the API.
121+
prefs, err := client.GetUserNotificationPreferences(ctx)
122+
require.NoError(t, err)
123+
124+
// Then: no preferences will be returned.
125+
require.Len(t, prefs, 0)
126+
})
127+
128+
t.Run("Disable a template", func(t *testing.T) {
129+
t.Parallel()
130+
131+
ctx := testutil.Context(t, testutil.WaitShort)
132+
template := notifications.TemplateWorkspaceDormant
133+
134+
// Given: the first user with no set preferences.
135+
client := coderdtest.New(t, createOpts(t))
136+
_ = coderdtest.CreateFirstUser(t, client)
137+
138+
prefs, err := client.GetUserNotificationPreferences(ctx)
139+
require.NoError(t, err)
140+
require.Len(t, prefs, 0)
141+
142+
// When: calling the API.
143+
prefs, err = client.UpdateUserNotificationPreferences(ctx, codersdk.UpdateUserNotificationPreferences{
144+
TemplateDisabledMap: map[string]bool{
145+
template.String(): true,
146+
},
147+
})
148+
require.NoError(t, err)
149+
150+
// Then: the single preference will be returned.
151+
require.Len(t, prefs, 1)
152+
require.Equal(t, template, prefs[0].NotificationTemplateID)
153+
require.True(t, prefs[0].Disabled)
154+
})
155+
}

codersdk/notifications.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"io"
88
"net/http"
9+
"time"
910

1011
"github.com/google/uuid"
1112
"golang.org/x/xerrors"
@@ -26,6 +27,12 @@ type NotificationTemplate struct {
2627
Kind string `json:"kind"`
2728
}
2829

30+
type NotificationPreference struct {
31+
NotificationTemplateID uuid.UUID `json:"id" format:"uuid"`
32+
Disabled bool `json:"disabled"`
33+
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
34+
}
35+
2936
// GetNotificationsSettings retrieves the notifications settings, which currently just describes whether all
3037
// notifications are paused from sending.
3138
func (c *Client) GetNotificationsSettings(ctx context.Context) (NotificationsSettings, error) {
@@ -105,6 +112,60 @@ func (c *Client) GetSystemNotificationTemplates(ctx context.Context) ([]Notifica
105112
return templates, nil
106113
}
107114

115+
// GetUserNotificationPreferences TODO
116+
func (c *Client) GetUserNotificationPreferences(ctx context.Context) ([]NotificationPreference, error) {
117+
res, err := c.Request(ctx, http.MethodGet, "/api/v2/notifications/preferences", nil)
118+
if err != nil {
119+
return nil, err
120+
}
121+
defer res.Body.Close()
122+
123+
if res.StatusCode != http.StatusOK {
124+
return nil, ReadBodyAsError(res)
125+
}
126+
127+
var prefs []NotificationPreference
128+
body, err := io.ReadAll(res.Body)
129+
if err != nil {
130+
return nil, xerrors.Errorf("read response body: %w", err)
131+
}
132+
133+
if err := json.Unmarshal(body, &prefs); err != nil {
134+
return nil, xerrors.Errorf("unmarshal response body: %w", err)
135+
}
136+
137+
return prefs, nil
138+
}
139+
140+
// UpdateUserNotificationPreferences TODO
141+
func (c *Client) UpdateUserNotificationPreferences(ctx context.Context, req UpdateUserNotificationPreferences) ([]NotificationPreference, error) {
142+
res, err := c.Request(ctx, http.MethodPut, "/api/v2/notifications/preferences", req)
143+
if err != nil {
144+
return nil, err
145+
}
146+
defer res.Body.Close()
147+
148+
if res.StatusCode != http.StatusOK {
149+
return nil, ReadBodyAsError(res)
150+
}
151+
152+
var prefs []NotificationPreference
153+
body, err := io.ReadAll(res.Body)
154+
if err != nil {
155+
return nil, xerrors.Errorf("read response body: %w", err)
156+
}
157+
158+
if err := json.Unmarshal(body, &prefs); err != nil {
159+
return nil, xerrors.Errorf("unmarshal response body: %w", err)
160+
}
161+
162+
return prefs, nil
163+
}
164+
108165
type UpdateNotificationTemplateMethod struct {
109166
Method string `json:"method,omitempty" example:"webhook"`
110167
}
168+
169+
type UpdateUserNotificationPreferences struct {
170+
TemplateDisabledMap map[string]bool `json:"template_disabled_map"`
171+
}

0 commit comments

Comments
 (0)