Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add user email notification preferences feature (merge feature branch into version branch) #7394

Merged
merged 36 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
19927d3
all: Add NotificationType enum and use it throughout the code
ryaplots Sep 19, 2024
c92a068
is: Add notification preferences migration and user field
ryaplots Oct 1, 2024
ec3abbe
is: Deprecate email in Notification and createNotification messages
ryaplots Oct 2, 2024
0849d27
is: Fix email_notification_preferences migration and store
ryaplots Oct 2, 2024
72704e9
is, console: Fix sending emails and seeing notifications
ryaplots Oct 4, 2024
9b18575
console: Add fe validation schema
ryaplots Oct 7, 2024
46b2c27
is: Fix email_notification_preferences type and migration
ryaplots Oct 7, 2024
78c8e8a
is: Make only one request to get the users
ryaplots Oct 7, 2024
6935639
is: Add test case for emailNotificationPreferences
ryaplots Oct 9, 2024
3b07ae1
is: Remove email preferences check for admins
ryaplots Oct 17, 2024
1976bec
dev: Rename test templates
ryaplots Oct 23, 2024
6cf9c45
is: Add email notification preferences flow test
ryaplots Oct 24, 2024
10202c7
is: Add test for sending emails if admin
ryaplots Oct 28, 2024
b4172a2
is: Fix store path
ryaplots Oct 30, 2024
36121e9
is: Fix tests
ryaplots Oct 30, 2024
27b9d24
is: Add user preferences
ryaplots Oct 30, 2024
00c1b44
is: Improve readability of email preference logic
ryaplots Nov 1, 2024
faec567
is: Address comments
ryaplots Nov 11, 2024
0e83bc6
is: Use string notification type in lowercase
ryaplots Nov 12, 2024
fd8f408
is: Add comment
ryaplots Nov 12, 2024
914cbf0
is: Fix linitng
ryaplots Nov 12, 2024
706501b
is: Fix comment
ryaplots Nov 12, 2024
4a322f6
console: Revert constants
ryaplots Nov 12, 2024
704f334
is: Add issue in comment
ryaplots Nov 15, 2024
7464da7
is: Merge two functions into one and add panic
ryaplots Nov 15, 2024
38c770b
is: Add missing comment
ryaplots Nov 15, 2024
adbb189
is: Remove panic
ryaplots Nov 15, 2024
4ef05bb
is: Fix panic
ryaplots Nov 15, 2024
007165c
console: Add email notifications form
ryaplots Oct 29, 2024
2a6a72a
console: Fix messages and unsusbcribe all
ryaplots Nov 13, 2024
44e5364
console: Add correct fetch and fix styling
ryaplots Nov 15, 2024
ac3007a
console: Fix width
ryaplots Nov 18, 2024
a04dd78
console: Update checkbox to handle descriptions better
ryaplots Nov 18, 2024
ecbacbb
dev: Update changelog
ryaplots Nov 19, 2024
6313bf7
dev: Update Changelog
ryaplots Nov 25, 2024
b9935cc
is: Fix proto
ryaplots Nov 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ For details about compatibility between different releases, see the **Commitment

### Added

- Support user email notification preferences.
- This requires a database migration.

### Changed

### Deprecated
Expand Down
1 change: 1 addition & 0 deletions api/buf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ lint:
- ttn/lorawan/v3/messages.proto
- ttn/lorawan/v3/metadata.proto
- ttn/lorawan/v3/rights.proto
- ttn/lorawan/v3/notification_service.proto
ENUM_VALUE_UPPER_SNAKE_CASE:
- ttn/lorawan/v3/rights.proto
ENUM_ZERO_VALUE_SUFFIX:
Expand Down
44 changes: 42 additions & 2 deletions api/ttn/lorawan/v3/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,7 @@
- [Message `UpdateNotificationStatusRequest`](#ttn.lorawan.v3.UpdateNotificationStatusRequest)
- [Enum `NotificationReceiver`](#ttn.lorawan.v3.NotificationReceiver)
- [Enum `NotificationStatus`](#ttn.lorawan.v3.NotificationStatus)
- [Enum `NotificationType`](#ttn.lorawan.v3.NotificationType)
- [Service `NotificationService`](#ttn.lorawan.v3.NotificationService)
- [File `ttn/lorawan/v3/oauth.proto`](#ttn/lorawan/v3/oauth.proto)
- [Message `ListOAuthAccessTokensRequest`](#ttn.lorawan.v3.ListOAuthAccessTokensRequest)
Expand Down Expand Up @@ -798,6 +799,7 @@
- [Message `DeleteInvitationRequest`](#ttn.lorawan.v3.DeleteInvitationRequest)
- [Message `DeleteUserAPIKeyRequest`](#ttn.lorawan.v3.DeleteUserAPIKeyRequest)
- [Message `DeleteUserBookmarkRequest`](#ttn.lorawan.v3.DeleteUserBookmarkRequest)
- [Message `EmailNotificationPreferences`](#ttn.lorawan.v3.EmailNotificationPreferences)
- [Message `GetUserAPIKeyRequest`](#ttn.lorawan.v3.GetUserAPIKeyRequest)
- [Message `GetUserRequest`](#ttn.lorawan.v3.GetUserRequest)
- [Message `Invitation`](#ttn.lorawan.v3.Invitation)
Expand Down Expand Up @@ -9488,7 +9490,7 @@ The NsRelayConfigurationService provides configuration management capabilities f
| Field | Validations |
| ----- | ----------- |
| `entity_ids` | <p>`message.required`: `true`</p> |
| `notification_type` | <p>`string.min_len`: `1`</p><p>`string.max_len`: `100`</p> |
| `notification_type` | <p>`string.in`: `[unknown api_key_created api_key_changed client_requested collaborator_changed entity_state_changed invitation login_token password_changed temporary_password user_requested validate]`</p> |
| `receivers` | <p>`repeated.min_items`: `1`</p><p>`repeated.unique`: `true`</p><p>`repeated.items.enum.defined_only`: `true`</p> |

### <a name="ttn.lorawan.v3.CreateNotificationResponse">Message `CreateNotificationResponse`</a>
Expand Down Expand Up @@ -9541,14 +9543,20 @@ The NsRelayConfigurationService provides configuration management capabilities f
| `id` | [`string`](#string) | | The immutable ID of the notification. Generated by the server. |
| `created_at` | [`google.protobuf.Timestamp`](#google.protobuf.Timestamp) | | The time when the notification was triggered. |
| `entity_ids` | [`EntityIdentifiers`](#ttn.lorawan.v3.EntityIdentifiers) | | The entity this notification is about. |
| `notification_type` | [`string`](#string) | | The type of this notification. |
| `notification_type` | [`string`](#string) | | The type of this notification. TODO: Replace with type NotificationType in v4 https://github.com/TheThingsNetwork/lorawan-stack/issues/7384. |
| `data` | [`google.protobuf.Any`](#google.protobuf.Any) | | The data related to the notification. |
| `sender_ids` | [`UserIdentifiers`](#ttn.lorawan.v3.UserIdentifiers) | | If the notification was triggered by a user action, this contains the identifiers of the user that triggered the notification. |
| `receivers` | [`NotificationReceiver`](#ttn.lorawan.v3.NotificationReceiver) | repeated | Relation of the notification receiver to the entity. |
| `email` | [`bool`](#bool) | | Whether an email was sent for the notification. |
| `status` | [`NotificationStatus`](#ttn.lorawan.v3.NotificationStatus) | | The status of the notification. |
| `status_updated_at` | [`google.protobuf.Timestamp`](#google.protobuf.Timestamp) | | The time when the notification status was updated. |

#### Field Rules

| Field | Validations |
| ----- | ----------- |
| `notification_type` | <p>`string.in`: `[unknown api_key_created api_key_changed client_requested collaborator_changed entity_state_changed invitation login_token password_changed temporary_password user_requested validate]`</p> |

### <a name="ttn.lorawan.v3.UpdateNotificationStatusRequest">Message `UpdateNotificationStatusRequest`</a>

| Field | Type | Label | Description |
Expand Down Expand Up @@ -9582,6 +9590,23 @@ The NsRelayConfigurationService provides configuration management capabilities f
| `NOTIFICATION_STATUS_SEEN` | 1 | |
| `NOTIFICATION_STATUS_ARCHIVED` | 2 | |

### <a name="ttn.lorawan.v3.NotificationType">Enum `NotificationType`</a>

| Name | Number | Description |
| ---- | ------ | ----------- |
| `UNKNOWN` | 0 | |
| `API_KEY_CREATED` | 1 | |
| `API_KEY_CHANGED` | 2 | |
| `CLIENT_REQUESTED` | 3 | |
| `COLLABORATOR_CHANGED` | 4 | |
| `ENTITY_STATE_CHANGED` | 5 | |
| `INVITATION` | 6 | |
| `LOGIN_TOKEN` | 7 | |
| `PASSWORD_CHANGED` | 8 | |
| `TEMPORARY_PASSWORD` | 9 | |
| `USER_REQUESTED` | 10 | |
| `VALIDATE` | 11 | |

### <a name="ttn.lorawan.v3.NotificationService">Service `NotificationService`</a>

The NotificationService is used to send notifications.
Expand Down Expand Up @@ -11348,6 +11373,20 @@ Secret contains a secret value. It also contains the ID of the Encryption key us
| `user_ids` | <p>`message.required`: `true`</p> |
| `entity_ids` | <p>`message.required`: `true`</p> |

### <a name="ttn.lorawan.v3.EmailNotificationPreferences">Message `EmailNotificationPreferences`</a>

EmailNotificationPreferences is the message that defines the types of notifications for which the user wants to receive an email.

| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| `types` | [`NotificationType`](#ttn.lorawan.v3.NotificationType) | repeated | |

#### Field Rules

| Field | Validations |
| ----- | ----------- |
| `types` | <p>`repeated.unique`: `true`</p><p>`repeated.items.enum.defined_only`: `true`</p> |

### <a name="ttn.lorawan.v3.GetUserAPIKeyRequest">Message `GetUserAPIKeyRequest`</a>

| Field | Type | Label | Description |
Expand Down Expand Up @@ -11583,6 +11622,7 @@ User is the message that defines a user on the network.
| `temporary_password_expires_at` | [`google.protobuf.Timestamp`](#google.protobuf.Timestamp) | | |
| `profile_picture` | [`Picture`](#ttn.lorawan.v3.Picture) | | A profile picture for the user. This information is public and can be seen by any authenticated user in the network. |
| `console_preferences` | [`UserConsolePreferences`](#ttn.lorawan.v3.UserConsolePreferences) | | Console preferences contains the user's preferences regarding the behavior of the Console. |
| `email_notification_preferences` | [`EmailNotificationPreferences`](#ttn.lorawan.v3.EmailNotificationPreferences) | | next: 27 |

#### Field Rules

Expand Down
40 changes: 39 additions & 1 deletion api/ttn/lorawan/v3/api.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -22703,6 +22703,18 @@
"default": "DOWNLINK_PATH_CONSTRAINT_NONE",
"description": " - DOWNLINK_PATH_CONSTRAINT_NONE: Indicates that the gateway can be selected for downlink without constraints by the Network Server.\n - DOWNLINK_PATH_CONSTRAINT_PREFER_OTHER: Indicates that the gateway can be selected for downlink only if no other or better gateway can be selected.\n - DOWNLINK_PATH_CONSTRAINT_NEVER: Indicates that this gateway will never be selected for downlink, even if that results in no available downlink path."
},
"v3EmailNotificationPreferences": {
"type": "object",
"properties": {
"types": {
"type": "array",
"items": {
"$ref": "#/definitions/v3NotificationType"
}
}
},
"description": "EmailNotificationPreferences is the message that defines the types of notifications for which the user wants to receive an email."
},
"v3EmailValidation": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -27098,7 +27110,7 @@
},
"notification_type": {
"type": "string",
"description": "The type of this notification."
"description": "The type of this notification.\nTODO: Replace with type NotificationType in v4 https://github.com/TheThingsNetwork/lorawan-stack/issues/7384."
},
"data": {
"$ref": "#/definitions/protobufAny",
Expand Down Expand Up @@ -27150,6 +27162,24 @@
],
"default": "NOTIFICATION_STATUS_UNSEEN"
},
"v3NotificationType": {
"type": "string",
"enum": [
"UNKNOWN",
"API_KEY_CREATED",
"API_KEY_CHANGED",
"CLIENT_REQUESTED",
"COLLABORATOR_CHANGED",
"ENTITY_STATE_CHANGED",
"INVITATION",
"LOGIN_TOKEN",
"PASSWORD_CHANGED",
"TEMPORARY_PASSWORD",
"USER_REQUESTED",
"VALIDATE"
],
"default": "UNKNOWN"
},
"v3NsEndDeviceRegistrySetBody": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -29267,6 +29297,10 @@
"console_preferences": {
"$ref": "#/definitions/v3UserConsolePreferences",
"description": "Console preferences contains the user's preferences regarding the behavior of the Console."
},
"email_notification_preferences": {
"$ref": "#/definitions/v3EmailNotificationPreferences",
"title": "next: 27"
}
},
"description": "User is the message that defines a user on the network."
Expand Down Expand Up @@ -29529,6 +29563,10 @@
"console_preferences": {
"$ref": "#/definitions/v3UserConsolePreferences",
"description": "Console preferences contains the user's preferences regarding the behavior of the Console."
},
"email_notification_preferences": {
"$ref": "#/definitions/v3EmailNotificationPreferences",
"title": "next: 27"
}
},
"description": "User is the message that defines a user on the network."
Expand Down
55 changes: 50 additions & 5 deletions api/ttn/lorawan/v3/notification_service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,23 @@ message Notification {
EntityIdentifiers entity_ids = 3;

// The type of this notification.
string notification_type = 4;
// TODO: Replace with type NotificationType in v4 https://github.com/TheThingsNetwork/lorawan-stack/issues/7384.
string notification_type = 4 [(validate.rules).string = {
in: [
"unknown",
"api_key_created",
"api_key_changed",
"client_requested",
"collaborator_changed",
"entity_state_changed",
"invitation",
"login_token",
"password_changed",
"temporary_password",
"user_requested",
"validate"
]
}];

// The data related to the notification.
google.protobuf.Any data = 5;
Expand All @@ -57,7 +73,7 @@ message Notification {
repeated NotificationReceiver receivers = 8;

// Whether an email was sent for the notification.
bool email = 9;
bool email = 9 [deprecated = true];

// The status of the notification.
NotificationStatus status = 10;
Expand All @@ -66,6 +82,23 @@ message Notification {
google.protobuf.Timestamp status_updated_at = 11;
}

enum NotificationType {
option (thethings.json.enum) = {marshal_as_string: true};

UNKNOWN = 0;
API_KEY_CREATED = 1;
API_KEY_CHANGED = 2;
CLIENT_REQUESTED = 3;
COLLABORATOR_CHANGED = 4;
ENTITY_STATE_CHANGED = 5;
INVITATION = 6;
LOGIN_TOKEN = 7;
PASSWORD_CHANGED = 8;
TEMPORARY_PASSWORD = 9;
USER_REQUESTED = 10;
VALIDATE = 11;
}

enum NotificationReceiver {
option (thethings.json.enum) = {
marshal_as_string: true,
Expand Down Expand Up @@ -104,8 +137,20 @@ message CreateNotificationRequest {

// The type of this notification.
string notification_type = 2 [(validate.rules).string = {
min_len: 1,
max_len: 100,
in: [
"unknown",
"api_key_created",
"api_key_changed",
"client_requested",
"collaborator_changed",
"entity_state_changed",
"invitation",
"login_token",
"password_changed",
"temporary_password",
"user_requested",
"validate"
]
}];

// The data related to the notification.
Expand All @@ -124,7 +169,7 @@ message CreateNotificationRequest {
}];

// Whether an email should be sent for the notification.
bool email = 6;
bool email = 6 [deprecated = true];
}

message CreateNotificationResponse {
Expand Down
14 changes: 13 additions & 1 deletion api/ttn/lorawan/v3/user.proto
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import "thethings/json/annotations.proto";
import "ttn/lorawan/v3/contact_info.proto";
import "ttn/lorawan/v3/enums.proto";
import "ttn/lorawan/v3/identifiers.proto";
import "ttn/lorawan/v3/notification_service.proto";
import "ttn/lorawan/v3/picture.proto";
import "ttn/lorawan/v3/rights.proto";
import "validate/validate.proto";
Expand Down Expand Up @@ -62,6 +63,16 @@ enum DashboardLayout {
DASHBOARD_LAYOUT_GRID = 2;
}

// EmailNotificationPreferences is the message that defines the types of notifications for which the user wants to receive an email.
message EmailNotificationPreferences {
repeated NotificationType types = 1 [(validate.rules).repeated = {
unique: true,
items: {
enum: {defined_only: true}
}
}];
}

// UserConsolePreferences is the message that defines the user preferences for the Console.
message UserConsolePreferences {
option (thethings.flags.message) = {
Expand Down Expand Up @@ -306,7 +317,8 @@ message User {
// Console preferences contains the user's preferences regarding the behavior of the Console.
UserConsolePreferences console_preferences = 25;

// next: 26
EmailNotificationPreferences email_notification_preferences = 26;
// next: 27
}

message Users {
Expand Down
3 changes: 2 additions & 1 deletion pkg/email/dir/dir_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"testing"

"go.thethings.network/lorawan-stack/v3/pkg/email"
"go.thethings.network/lorawan-stack/v3/pkg/ttnpb"
"go.thethings.network/lorawan-stack/v3/pkg/util/test"
"go.thethings.network/lorawan-stack/v3/pkg/util/test/assertions/should"
)
Expand All @@ -40,7 +41,7 @@ func TestMailDir(t *testing.T) {
a.So(err, should.BeNil)

err = mailer.Send(&email.Message{
TemplateName: "irrelevant",
TemplateName: ttnpb.GetNotificationTypeString(ttnpb.NotificationType_UNKNOWN),
RecipientName: "John Doe",
RecipientAddress: "john.doe@example.com",
Subject: "Email Subject",
Expand Down
7 changes: 3 additions & 4 deletions pkg/email/email_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func TestEmail(t *testing.T) {
registry := email.NewTemplateRegistry()

welcomeEmailTemplate, err := email.NewTemplateFS(
os.DirFS("testdata"), "welcome",
os.DirFS("testdata"), ttnpb.GetNotificationTypeString(ttnpb.NotificationType_UNKNOWN),
email.FSTemplate{
SubjectTemplate: "Welcome to {{ .Network.Name }}",
HTMLTemplateBaseFile: "base.html",
Expand All @@ -53,9 +53,8 @@ func TestEmail(t *testing.T) {
a.So(err, should.BeNil)

registry.RegisterTemplate(welcomeEmailTemplate)

a.So(registry.RegisteredTemplates(), should.Contain, "welcome")
returnedTemplate := registry.GetTemplate(ctx, "welcome")
a.So(registry.RegisteredTemplates(), should.Contain, "unknown")
returnedTemplate := registry.GetTemplate(ctx, ttnpb.GetNotificationTypeString(ttnpb.NotificationType_UNKNOWN))

for i, template := range []*email.Template{welcomeEmailTemplate, returnedTemplate} {
template := template
Expand Down
3 changes: 2 additions & 1 deletion pkg/email/sendgrid/sendgrid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/smarty/assertions"
"go.thethings.network/lorawan-stack/v3/pkg/email"
"go.thethings.network/lorawan-stack/v3/pkg/log"
"go.thethings.network/lorawan-stack/v3/pkg/ttnpb"
"go.thethings.network/lorawan-stack/v3/pkg/util/test"
"go.thethings.network/lorawan-stack/v3/pkg/util/test/assertions/should"
)
Expand All @@ -46,7 +47,7 @@ func TestSendGrid(t *testing.T) {
a.So(err, should.BeNil)

err = sg.Send(&email.Message{
TemplateName: "test",
TemplateName: ttnpb.GetNotificationTypeString(ttnpb.NotificationType_UNKNOWN),
RecipientName: "John Doe",
RecipientAddress: "john.doe@example.com",
Subject: "Testing SendGrid",
Expand Down
3 changes: 2 additions & 1 deletion pkg/email/smtp/smtp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/smarty/assertions"
"go.thethings.network/lorawan-stack/v3/pkg/email"
"go.thethings.network/lorawan-stack/v3/pkg/log"
"go.thethings.network/lorawan-stack/v3/pkg/ttnpb"
"go.thethings.network/lorawan-stack/v3/pkg/util/test"
"go.thethings.network/lorawan-stack/v3/pkg/util/test/assertions/should"
)
Expand Down Expand Up @@ -64,7 +65,7 @@ func TestSMTP(t *testing.T) {
a.So(err, should.BeNil)

mail := &email.Message{
TemplateName: "test",
TemplateName: ttnpb.GetNotificationTypeString(ttnpb.NotificationType_UNKNOWN),
RecipientName: "John Doe",
RecipientAddress: "john.doe@example.com",
Subject: "Testing SMTP",
Expand Down
2 changes: 1 addition & 1 deletion pkg/email/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func newContextWithTemplateRegistry(parent context.Context, reg TemplateRegistry

// TemplateRegistry keeps track of email templates.
type TemplateRegistry interface {
RegisteredTemplates() []string
RegisteredTemplates() []*ttnpb.NotificationType
GetTemplate(ctx context.Context, name string) *Template
}

Expand Down
Loading
Loading