From 1209b65ff4e6af0b6e8d1d136b3620329031dcc3 Mon Sep 17 00:00:00 2001 From: benolayinka Date: Tue, 23 Jun 2020 12:18:48 +0200 Subject: [PATCH 1/8] doc: Update webhooks --- doc/content/integrations/webhooks/_index.md | 44 ------------------- .../webhooks/creating-webhooks.md | 19 ++++++++ .../integrations/webhooks/path-variables.md | 24 ++++++++++ .../webhooks/scheduling-downlinks.md | 37 ++++++++++++++++ .../webhooks/webhook-templates/_index.md | 10 +---- 5 files changed, 82 insertions(+), 52 deletions(-) create mode 100644 doc/content/integrations/webhooks/creating-webhooks.md create mode 100644 doc/content/integrations/webhooks/path-variables.md create mode 100644 doc/content/integrations/webhooks/scheduling-downlinks.md diff --git a/doc/content/integrations/webhooks/_index.md b/doc/content/integrations/webhooks/_index.md index 823ddfd8ce..d469e1c035 100644 --- a/doc/content/integrations/webhooks/_index.md +++ b/doc/content/integrations/webhooks/_index.md @@ -4,47 +4,3 @@ description: "" --- The webhooks feature allows the Application Server to send application related messages to specific HTTP(S) endpoints. - - - -## Creating a Webhook - -Creating a webhook requires you to have an HTTP(S) endpoint available. - -In your application select the **Webhooks** submenu from the **Integrations** side menu. Clicking on the **+ Add Webhook** button will open the Webhook creation screen. Fill in your webhook ID, format and base URL. - -{{< figure src="webhook-creation.png" alt="Webhook creation screen" >}} - -The paths are appended to the base URL. So, the Application Server will perform `POST` requests on the endpoint `https://app.example.com/lorahooks/join` for join-accepts and `https://app.example.com/lorahooks/up` for uplink messages. Clicking the **Add Webhook** button will create the Webhook. - ->Note: If you don't have an endpoint available for testing, use for example [PostBin](https://postb.in). - -## Scheduling Downlinks - -You can schedule downlink messages using webhooks too. This requires an API key with traffic writing rights, which can be created using the Console. In your application, select the **API Keys** sidemenu and click on the **+ Add API Key** button. You can now fill in the name and the rights of your API key. - -{{< figure src="api-key-creation.png" alt="API key creation screen" >}} - -Click on the **Create API Key** button in order to create the API key. This will open the API key information screen. - -{{< figure src="api-key-created.png" alt="API key created" >}} - -Make sure to save your API key at this point, since it will no longer be retrievable after you leave the page. You can now pass the API key as bearer token on the `Authorization` header. - -The downlink queue operation paths are: - -- For push: `/api/v3/as/applications/{application_id}/webhooks/{webhook_id}/devices/{device_id}/down/push` -- For replace: `/api/v3/as/applications/{application_id}/webhooks/{webhook_id}/devices/{device_id}/down/replace` - -For example: - -``` -$ curl https://thethings.example.com/api/v3/as/applications/app1/webhooks/wh1/devices/dev1/down/push \ - -X POST \ - -H 'Authorization: Bearer NNSXS.VEEBURF3KR77ZR..' \ - --data '{"downlinks":[{"frm_payload":"vu8=","f_port":15,"priority":"NORMAL"}]}' -``` - -Will push a downlink to the end device `dev1` of the application `app1` using the webhook `wh1`. - -You can also save the API key in the webhook configuration page using the the **Downlink API Key** field. The Application Server will provide it to your endpoint using the `X-Downlink-Apikey` header and the push and replace operations paths using the `X-Downlink-Push` and `X-Downlink-Replace` headers. diff --git a/doc/content/integrations/webhooks/creating-webhooks.md b/doc/content/integrations/webhooks/creating-webhooks.md new file mode 100644 index 0000000000..c88d18e68a --- /dev/null +++ b/doc/content/integrations/webhooks/creating-webhooks.md @@ -0,0 +1,19 @@ +--- +title: "Creating Webhooks" +description: "" +weight: -1 +--- + +This section provides instructions for creating a webhook in the console. + + + +Creating a webhook requires you to have an HTTP(S) endpoint available. + +In your application select the **Webhooks** submenu from the **Integrations** side menu. Clicking on the **+ Add Webhook** button will open the Webhook creation screen. Fill in your webhook ID, format and base URL. + +{{< figure src="../webhook-creation.png" alt="Webhook creation screen" >}} + +The paths are appended to the base URL. So, the Application Server will perform `POST` requests on the endpoint `https://app.example.com/lorahooks/join` for join-accepts and `https://app.example.com/lorahooks/up` for uplink messages. Clicking the **Add Webhook** button will create the Webhook. + +>Note: If you don't have an endpoint available for testing, you can test with a free service like [PostBin](https://postb.in). diff --git a/doc/content/integrations/webhooks/path-variables.md b/doc/content/integrations/webhooks/path-variables.md new file mode 100644 index 0000000000..cb36ea6025 --- /dev/null +++ b/doc/content/integrations/webhooks/path-variables.md @@ -0,0 +1,24 @@ +--- +title: "Webhook Path Variables" +description: "" +weight: -1 +--- + +Webhook path variables allow you to substitute device and application specific variables in webhook paths. This section provides instructions for using webhook path variables. + + + +Webhook path variables allow you to use the following variables in webhook paths: + +- `appID` +- `appEUI` +- `joinEUI` +- `devID` +- `devEUI` +- `devAddr` + +Path variables can be inserted in the **Base URL** webhook field or the **Path** field for a particular type of message. + +For example, if the **Base URL** is `https://app.example.com/lorahooks{/appID}` and the **Path** is `/up{/devID}` an uplink from the device `dev1` of application `app1` will be posted at `https://app.example.com/lorahooks/app1/up/dev1`. + +See [IETF RFC65700](https://tools.ietf.org/html/rfc6570) for more documentation about URL path variables. {{% tts %}} supports all forms of path variable substitution. diff --git a/doc/content/integrations/webhooks/scheduling-downlinks.md b/doc/content/integrations/webhooks/scheduling-downlinks.md new file mode 100644 index 0000000000..ac4d2db1f4 --- /dev/null +++ b/doc/content/integrations/webhooks/scheduling-downlinks.md @@ -0,0 +1,37 @@ +--- +title: "Scheduling Downlinks" +description: "" +weight: -1 +--- + +This section provides instructions for creating scheduling downlinks using webhooks. + + + +You can schedule downlink messages using webhooks. This requires an API key with traffic writing rights, which can be created using the Console. In your application, select the **API Keys** sidemenu and click on the **+ Add API Key** button. You can now fill in the name and the rights of your API key. + +{{< figure src="../api-key-creation.png" alt="API key creation screen" >}} + +Click on the **Create API Key** button in order to create the API key. This will open the API key information screen. + +{{< figure src="../api-key-created.png" alt="API key created" >}} + +Make sure to save your API key at this point, since it will no longer be retrievable after you leave the page. You can now pass the API key as bearer token on the `Authorization` header. + +The downlink queue operation paths are: + +- For push: `/api/v3/as/applications/{application_id}/webhooks/{webhook_id}/devices/{device_id}/down/push` +- For replace: `/api/v3/as/applications/{application_id}/webhooks/{webhook_id}/devices/{device_id}/down/replace` + +For example: + +``` +$ curl https://thethings.example.com/api/v3/as/applications/app1/webhooks/wh1/devices/dev1/down/push \ + -X POST \ + -H 'Authorization: Bearer NNSXS.VEEBURF3KR77ZR..' \ + --data '{"downlinks":[{"frm_payload":"vu8=","f_port":15,"priority":"NORMAL"}]}' +``` + +Will push a downlink to the end device `dev1` of the application `app1` using the webhook `wh1`. + +You can also save the API key in the webhook configuration page using the the **Downlink API Key** field. The Application Server will provide it to your endpoint using the `X-Downlink-Apikey` header and the push and replace operations paths using the `X-Downlink-Push` and `X-Downlink-Replace` headers. diff --git a/doc/content/integrations/webhooks/webhook-templates/_index.md b/doc/content/integrations/webhooks/webhook-templates/_index.md index 762409820f..34a30cf156 100644 --- a/doc/content/integrations/webhooks/webhook-templates/_index.md +++ b/doc/content/integrations/webhooks/webhook-templates/_index.md @@ -5,18 +5,12 @@ summary: Webhook templates define a webhook integration that is not created (yet weight: 1 --- -This is the reference for Webhook Templates - -It covers the format of the templates and how the template instantiation process works. - -## What is it? - Webhook templates define a webhook integration that is not created (yet). Templates allows for using common values for many webhooks, such as a common base URLs. -## Who is it for? - Webhook templates are primarily targeted at service providers who want to create specialized webhook integrations for the users of {{% tts %}}. + + ### Typical use cases 1. Create a webhook with a personalized base URL, format and message paths. From 13706cf46b545ba2431c414d510e6b1dda3e226e Mon Sep 17 00:00:00 2001 From: benolayinka Date: Thu, 2 Jul 2020 11:17:23 +0200 Subject: [PATCH 2/8] doc: Document MQTT version --- doc/content/integrations/mqtt/_index.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/content/integrations/mqtt/_index.md b/doc/content/integrations/mqtt/_index.md index 6e6298fec7..c2f2af51b6 100644 --- a/doc/content/integrations/mqtt/_index.md +++ b/doc/content/integrations/mqtt/_index.md @@ -3,13 +3,15 @@ title: "MQTT Server" description: "" --- -The Application Server exposes a MQTT server to work with streaming events. In order to use the MQTT server you need to create a new API key to authenticate. +{{% tts %}} exposes an MQTT server to work with streaming events. This section explains how to connect an MQTT client and subscribe to uplinks or publish downlinks. + +>{{% tts %}} supports the [MQTT Standard Version 3.1.1](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.pdf). ## Creating an API Key -The Console provides the required connection information and can be used to create an API key for authentication. In your application select the **MQTT** submenu from the **Integrations** side menu. + In order to use the MQTT server you need to create a new API key to authenticate. The Console provides the required connection information and can be used to create an API key for authentication. In your application select the **MQTT** submenu from the **Integrations** side menu. {{< figure src="mqtt-integration.png" alt="MQTT connection information" >}} From 8178a1c0a5822b789e245f968d25f5313f827c22 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 2 Jul 2020 12:49:26 +0300 Subject: [PATCH 3/8] console: Fix user form validation make primary_email_address required in validation schema --- pkg/webui/console/components/user-data-form/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/webui/console/components/user-data-form/index.js b/pkg/webui/console/components/user-data-form/index.js index 57fa340fad..ac8118e48f 100644 --- a/pkg/webui/console/components/user-data-form/index.js +++ b/pkg/webui/console/components/user-data-form/index.js @@ -42,7 +42,9 @@ const validationSchema = Yup.object().shape({ name: Yup.string() .min(2, Yup.passValues(sharedMessages.validateTooShort)) .max(50, Yup.passValues(sharedMessages.validateTooLong)), - primary_email_address: Yup.string().email(sharedMessages.validateEmail), + primary_email_address: Yup.string() + .email(sharedMessages.validateEmail) + .required(sharedMessages.validateRequired), state: Yup.string() .oneOf(approvalStates) .required(sharedMessages.validateRequired), From bb7dee43e7928504be3494678c39d2b822a1a000 Mon Sep 17 00:00:00 2001 From: Hylke Visser Date: Thu, 2 Jul 2020 12:02:16 +0200 Subject: [PATCH 4/8] is: Give more information about conflicting EUIs on create --- config/messages.json | 27 +++++++++++++++++++ pkg/identityserver/end_device_registry.go | 19 +++++++++++++ .../end_device_registry_test.go | 18 ++++++++++++- pkg/identityserver/gateway_registry.go | 13 +++++++++ pkg/identityserver/gateway_registry_test.go | 17 +++++++++++- pkg/identityserver/store/store.go | 20 +++++++++----- 6 files changed, 106 insertions(+), 8 deletions(-) diff --git a/config/messages.json b/config/messages.json index 8bb331b510..b1554cf2eb 100644 --- a/config/messages.json +++ b/config/messages.json @@ -4211,6 +4211,15 @@ "file": "store.go" } }, + "error:pkg/identityserver/store:eui_taken": { + "translations": { + "en": "EUI already taken" + }, + "description": { + "package": "pkg/identityserver/store", + "file": "store.go" + } + }, "error:pkg/identityserver/store:gateway_not_found": { "translations": { "en": "gateway `{gateway_id}` not found" @@ -4382,6 +4391,24 @@ "file": "identityserver.go" } }, + "error:pkg/identityserver:end_device_euis_taken": { + "translations": { + "en": "an end device with JoinEUI `{join_eui}` and DevEUI `{dev_eui}` is already registered as `{device_id}` in application `{application_id}`" + }, + "description": { + "package": "pkg/identityserver", + "file": "end_device_registry.go" + } + }, + "error:pkg/identityserver:gateway_eui_taken": { + "translations": { + "en": "a gateway with EUI `{gateway_eui}` is already registered as `{gateway_id}`" + }, + "description": { + "package": "pkg/identityserver", + "file": "gateway_registry.go" + } + }, "error:pkg/identityserver:invalid_authorization": { "translations": { "en": "invalid authorization" diff --git a/pkg/identityserver/end_device_registry.go b/pkg/identityserver/end_device_registry.go index b83019f91b..eac5a69dde 100644 --- a/pkg/identityserver/end_device_registry.go +++ b/pkg/identityserver/end_device_registry.go @@ -21,6 +21,7 @@ import ( "github.com/gogo/protobuf/types" "github.com/jinzhu/gorm" "go.thethings.network/lorawan-stack/v3/pkg/auth/rights" + "go.thethings.network/lorawan-stack/v3/pkg/errors" "go.thethings.network/lorawan-stack/v3/pkg/events" "go.thethings.network/lorawan-stack/v3/pkg/identityserver/blacklist" "go.thethings.network/lorawan-stack/v3/pkg/identityserver/store" @@ -42,6 +43,11 @@ var ( ) ) +var errEndDeviceEUIsTaken = errors.DefineAlreadyExists( + "end_device_euis_taken", + "an end device with JoinEUI `{join_eui}` and DevEUI `{dev_eui}` is already registered as `{device_id}` in application `{application_id}`", +) + func (is *IdentityServer) createEndDevice(ctx context.Context, req *ttnpb.CreateEndDeviceRequest) (dev *ttnpb.EndDevice, err error) { if err = rights.RequireApplication(ctx, req.EndDeviceIdentifiers.ApplicationIdentifiers, ttnpb.RIGHT_APPLICATION_DEVICES_WRITE); err != nil { return nil, err @@ -65,6 +71,19 @@ func (is *IdentityServer) createEndDevice(ctx context.Context, req *ttnpb.Create return nil }) if err != nil { + if errors.IsAlreadyExists(err) && errors.Resemble(err, store.ErrEUITaken) { + if ids, err := is.getEndDeviceIdentifiersForEUIs(ctx, &ttnpb.GetEndDeviceIdentifiersForEUIsRequest{ + JoinEUI: *req.JoinEUI, + DevEUI: *req.DevEUI, + }); err == nil { + return nil, errEndDeviceEUIsTaken.WithAttributes( + "join_eui", req.JoinEUI.String(), + "dev_eui", req.DevEUI.String(), + "device_id", ids.GetDeviceID(), + "application_id", ids.GetApplicationID(), + ) + } + } return nil, err } events.Publish(evtCreateEndDevice(ctx, req.EndDeviceIdentifiers, nil)) diff --git a/pkg/identityserver/end_device_registry_test.go b/pkg/identityserver/end_device_registry_test.go index 4cc26cdf68..e89a519a22 100644 --- a/pkg/identityserver/end_device_registry_test.go +++ b/pkg/identityserver/end_device_registry_test.go @@ -20,11 +20,11 @@ import ( pbtypes "github.com/gogo/protobuf/types" "github.com/smartystreets/assertions" - "github.com/smartystreets/assertions/should" "go.thethings.network/lorawan-stack/v3/pkg/errors" "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" "go.thethings.network/lorawan-stack/v3/pkg/types" "go.thethings.network/lorawan-stack/v3/pkg/util/test" + "go.thethings.network/lorawan-stack/v3/pkg/util/test/assertions/should" "google.golang.org/grpc" ) @@ -174,6 +174,22 @@ func TestEndDevicesCRUD(t *testing.T) { a.So(*ids, should.Resemble, created.EndDeviceIdentifiers) } + _, err = reg.Create(ctx, &ttnpb.CreateEndDeviceRequest{ + EndDevice: ttnpb.EndDevice{ + EndDeviceIdentifiers: ttnpb.EndDeviceIdentifiers{ + DeviceID: "other-test-device-id", + ApplicationIdentifiers: app.ApplicationIdentifiers, + JoinEUI: &joinEUI, + DevEUI: &devEUI, + }, + Name: "test-device-name", + }, + }, creds) + + if a.So(err, should.NotBeNil) { + a.So(err, should.HaveSameErrorDefinitionAs, errEndDeviceEUIsTaken) + } + list, err := reg.List(ctx, &ttnpb.ListEndDevicesRequest{ FieldMask: pbtypes.FieldMask{Paths: []string{"name"}}, ApplicationIdentifiers: app.ApplicationIdentifiers, diff --git a/pkg/identityserver/gateway_registry.go b/pkg/identityserver/gateway_registry.go index 6c518830cf..64bcf46994 100644 --- a/pkg/identityserver/gateway_registry.go +++ b/pkg/identityserver/gateway_registry.go @@ -20,6 +20,7 @@ import ( "github.com/gogo/protobuf/types" "github.com/jinzhu/gorm" "go.thethings.network/lorawan-stack/v3/pkg/auth/rights" + "go.thethings.network/lorawan-stack/v3/pkg/errors" "go.thethings.network/lorawan-stack/v3/pkg/events" "go.thethings.network/lorawan-stack/v3/pkg/identityserver/blacklist" "go.thethings.network/lorawan-stack/v3/pkg/identityserver/store" @@ -41,6 +42,8 @@ var ( ) ) +var errGatewayEUITaken = errors.DefineAlreadyExists("gateway_eui_taken", "a gateway with EUI `{gateway_eui}` is already registered as `{gateway_id}`") + func (is *IdentityServer) createGateway(ctx context.Context, req *ttnpb.CreateGatewayRequest) (gtw *ttnpb.Gateway, err error) { if err = blacklist.Check(ctx, req.GatewayID); err != nil { return nil, err @@ -84,6 +87,16 @@ func (is *IdentityServer) createGateway(ctx context.Context, req *ttnpb.CreateGa return nil }) if err != nil { + if errors.IsAlreadyExists(err) && errors.Resemble(err, store.ErrEUITaken) { + if ids, err := is.getGatewayIdentifiersForEUI(ctx, &ttnpb.GetGatewayIdentifiersForEUIRequest{ + EUI: *req.EUI, + }); err == nil { + return nil, errGatewayEUITaken.WithAttributes( + "gateway_eui", req.EUI.String(), + "gateway_id", ids.GetGatewayID(), + ) + } + } return nil, err } events.Publish(evtCreateGateway(ctx, req.GatewayIdentifiers, nil)) diff --git a/pkg/identityserver/gateway_registry_test.go b/pkg/identityserver/gateway_registry_test.go index ba87344bd6..1f16c19243 100644 --- a/pkg/identityserver/gateway_registry_test.go +++ b/pkg/identityserver/gateway_registry_test.go @@ -19,11 +19,11 @@ import ( ptypes "github.com/gogo/protobuf/types" "github.com/smartystreets/assertions" - "github.com/smartystreets/assertions/should" "go.thethings.network/lorawan-stack/v3/pkg/errors" "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" "go.thethings.network/lorawan-stack/v3/pkg/types" "go.thethings.network/lorawan-stack/v3/pkg/util/test" + "go.thethings.network/lorawan-stack/v3/pkg/util/test/assertions/should" "google.golang.org/grpc" ) @@ -167,6 +167,21 @@ func TestGatewaysCRUD(t *testing.T) { a.So(ids.GatewayID, should.Equal, created.GatewayID) } + _, err = reg.Create(ctx, &ttnpb.CreateGatewayRequest{ + Gateway: ttnpb.Gateway{ + GatewayIdentifiers: ttnpb.GatewayIdentifiers{ + GatewayID: "bar", + EUI: &eui, + }, + Name: "Bar Gateway", + }, + Collaborator: *userID.OrganizationOrUserIdentifiers(), + }, creds) + + if a.So(err, should.NotBeNil) { + a.So(err, should.HaveSameErrorDefinitionAs, errGatewayEUITaken) + } + got, err = reg.Get(ctx, &ttnpb.GetGatewayRequest{ GatewayIdentifiers: created.GatewayIdentifiers, FieldMask: ptypes.FieldMask{Paths: []string{"ids"}}, diff --git a/pkg/identityserver/store/store.go b/pkg/identityserver/store/store.go index ed460994fa..ee5e00a496 100644 --- a/pkg/identityserver/store/store.go +++ b/pkg/identityserver/store/store.go @@ -88,11 +88,15 @@ func (s *store) deleteEntity(ctx context.Context, entityID ttnpb.Identifiers) er var ( errDatabase = errors.DefineInternal("database", "database error") - errAlreadyExists = errors.DefineAlreadyExists("already_exists", "entity already exists", "field", "value") - errIDTaken = errors.DefineAlreadyExists("id_taken", "ID already taken") + errAlreadyExists = errors.DefineAlreadyExists("already_exists", "entity already exists") + + // ErrIDTaken is returned when an entity can not be created because the ID is already taken. + ErrIDTaken = errors.DefineAlreadyExists("id_taken", "ID already taken") + // ErrEUITaken is returned when an entity can not be created because the EUI is already taken. + ErrEUITaken = errors.DefineAlreadyExists("eui_taken", "EUI already taken") ) -var uniqueViolationRegex = regexp.MustCompile(`duplicate key value \(([^)]+)\)=\(([^)]+)\)`) +var uniqueViolationRegex = regexp.MustCompile(`duplicate key value( .+)? violates unique constraint "([a-z_]+)"`) func convertError(err error) error { switch err { @@ -106,10 +110,14 @@ func convertError(err error) error { switch pqErr.Code.Name() { case "unique_violation": if match := uniqueViolationRegex.FindStringSubmatch(pqErr.Message); match != nil { - if strings.HasSuffix(match[1], "_id") { - return errIDTaken.WithCause(err) + switch { + case strings.HasSuffix(match[2], "_id_index"): + return ErrIDTaken.WithCause(err) + case strings.HasSuffix(match[2], "_eui_index"): + return ErrEUITaken.WithCause(err) + default: + return errAlreadyExists.WithCause(err).WithAttributes("index", match[2]) } - return errAlreadyExists.WithCause(err).WithAttributes("field", match[1], "value", match[2]) } return errAlreadyExists.WithCause(err) default: From e47d143370ed5f79d72b8e5c760172c4faa57c75 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 2 Jul 2020 12:56:38 +0300 Subject: [PATCH 5/8] console: Fix join settings form validation make root_keys required in validation schema --- .../join-settings-form/validation-schema.js | 7 ++- .../validation-schema_test.js | 45 ++++++++++++++----- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/pkg/webui/console/views/device-add/wizard/join-settings-form/validation-schema.js b/pkg/webui/console/views/device-add/wizard/join-settings-form/validation-schema.js index 73730c0244..df40e5277c 100644 --- a/pkg/webui/console/views/device-add/wizard/join-settings-form/validation-schema.js +++ b/pkg/webui/console/views/device-add/wizard/join-settings-form/validation-schema.js @@ -31,10 +31,9 @@ const validationSchema = Yup.object() const keySchema = Yup.lazy(() => { return mayEditKeys ? Yup.object().shape({ - key: Yup.string().emptyOrLength( - 16 * 2, - Yup.passValues(sharedMessages.validateLength), - ), // 16 Byte hex. + key: Yup.string() + .length(16 * 2, Yup.passValues(sharedMessages.validateLength)) // 16 Byte hex. + .required(sharedMessages.validateRequired), }) : Yup.object().strip() }) diff --git a/pkg/webui/console/views/device-add/wizard/join-settings-form/validation-schema_test.js b/pkg/webui/console/views/device-add/wizard/join-settings-form/validation-schema_test.js index 8dcfe51630..66b9b3c498 100644 --- a/pkg/webui/console/views/device-add/wizard/join-settings-form/validation-schema_test.js +++ b/pkg/webui/console/views/device-add/wizard/join-settings-form/validation-schema_test.js @@ -65,6 +65,18 @@ describe(' validation schema', () => { }, }) + it('should require `app_key`', done => { + try { + validate(schema) + done.fail('should fail') + } catch (error) { + expect(error).toBeDefined() + expect(error.name).toBe('ValidationError') + expect(error.path).toBe('root_keys.app_key.key') + done() + } + }) + it('should handle `app_key`', () => { const appKey = '1'.repeat(32) @@ -108,22 +120,35 @@ describe(' validation schema', () => { }, }) - it('should handle `app_key`', () => { + it('should require `app_key`', done => { + try { + validate(schema) + done.fail('should fail') + } catch (error) { + expect(error).toBeDefined() + expect(error.name).toBe('ValidationError') + expect(error.path).toBe('root_keys.app_key.key') + done() + } + }) + + it('should require `nwk_key`', done => { const appKey = '1'.repeat(32) schema.root_keys.app_key = { key: appKey } - const validatedValue = validate(schema) - - expect(validatedValue).toBeDefined() - expect(validatedValue.root_keys).toBeDefined() - - const { root_keys } = validatedValue - expect(root_keys.app_key).toBeDefined() - expect(root_keys.app_key.key).toBe(appKey) + try { + validate(schema) + done.fail('should fail') + } catch (error) { + expect(error).toBeDefined() + expect(error.name).toBe('ValidationError') + expect(error.path).toBe('root_keys.nwk_key.key') + done() + } }) - it('should handle `nwk_key`', () => { + it('should handle `root_keys`', () => { const appKey = '1'.repeat(32) const nwkKey = '2'.repeat(32) From 683bd70f7f1539b93d02a1c8543550072b631ccf Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 25 Jun 2020 19:12:29 +0300 Subject: [PATCH 6/8] console,oauth: Extend error utils add isConflictError function --- pkg/webui/lib/errors/utils.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pkg/webui/lib/errors/utils.js b/pkg/webui/lib/errors/utils.js index d59f71c75d..f48821e55d 100644 --- a/pkg/webui/lib/errors/utils.js +++ b/pkg/webui/lib/errors/utils.js @@ -171,6 +171,15 @@ export const isPermissionDeniedError = error => export const isUnauthenticatedError = error => grpcStatusCode(error) === 16 || httpStatusCode(error) === 401 +/** + * Returns whether the grpc error represents a conflict with the current state on the server. + * + * @param {object} error - The error to be tested. + * @returns {boolean} `true` if `error` represents a `Conflict` error, `false` otherwise. + */ +export const isConflictError = error => + grpcStatusCode(error) === 10 || httpStatusCode(error) === 409 + /** * Returns whether `error` has translation ids. * From 7e779f67f67f234f413292e5d69d6452c0d369cf Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 25 Jun 2020 19:14:07 +0300 Subject: [PATCH 7/8] console: Fix application link fetching handle link result on 409 responses when fetching link stats --- .../store/middleware/logics/applications.js | 14 ++++++++++---- pkg/webui/console/store/reducers/link.js | 8 ++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/pkg/webui/console/store/middleware/logics/applications.js b/pkg/webui/console/store/middleware/logics/applications.js index f05c60d0af..10b6097f45 100644 --- a/pkg/webui/console/store/middleware/logics/applications.js +++ b/pkg/webui/console/store/middleware/logics/applications.js @@ -14,7 +14,7 @@ import api from '@console/api' -import { isNotFoundError } from '@ttn-lw/lib/errors/utils' +import { isNotFoundError, isConflictError } from '@ttn-lw/lib/errors/utils' import * as applications from '@console/store/actions/applications' import * as link from '@console/store/actions/link' @@ -117,9 +117,15 @@ const getApplicationLinkLogic = createRequestLogic({ return { link: linkResult, stats: statsResult, linked: true } } catch (error) { - // Consider errors that are not 404, since not found means that the - // application is not linked. - if (isNotFoundError(error)) { + // Ignore 404 error. It means that the application is not linked, but the response can + // still hold link data that we have to display to the user. + if (isNotFoundError(error) && typeof linkResult !== 'undefined') { + return { link: linkResult, stats: statsResult, linked: false } + } + + // Ignore 409 error. It means that the application link cannot be established, but + // the response can still hold link data that we have to displat to the user. + if (isConflictError(error) && typeof linkResult !== 'undefined') { return { link: linkResult, stats: statsResult, linked: false } } diff --git a/pkg/webui/console/store/reducers/link.js b/pkg/webui/console/store/reducers/link.js index 3ee83423fa..4f3a9adc66 100644 --- a/pkg/webui/console/store/reducers/link.js +++ b/pkg/webui/console/store/reducers/link.js @@ -36,12 +36,12 @@ const getLinkSuccess = function(state, { payload }) { } } -const getLinkFailure = function(state) { +const getLinkFailure = function(state, { payload }) { return { ...state, - link: {}, - stats: undefined, - linked: false, + link: payload.link || {}, + stats: payload.stats || undefined, + linked: payload.linked || false, } } From 74fd3a3907d82afef0db9cd76104a40531582a20 Mon Sep 17 00:00:00 2001 From: Hylke Visser Date: Mon, 6 Jul 2020 10:22:13 +0200 Subject: [PATCH 8/8] all: Bump to version 3.8.5 --- CHANGELOG.md | 19 ++++++++++++++++--- doc/config/_default/config.toml | 2 +- doc/themes/the-things-stack/package.json | 2 +- package.json | 2 +- pkg/version/ttn.go | 2 +- sdk/js/package.json | 2 +- 6 files changed, 21 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a9b440e8b..6b82ac6068 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +### Changed + +### Deprecated + +### Removed + +### Fixed + +### Security + +## [3.8.5] - 2020-07-06 + +### Added + - Option to reset end device payload formatters in the Console. - Service discovery using DNS SRV records for external Application Server linking. - Functionality to set end device attributes in the Console. @@ -28,8 +42,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Defer events subscriptions until there is actual interest for events. - End device creation form with wizard in the Console. -### Deprecated - ### Removed - Requirement to specify `frequency_plan_id` when creating gateways in the Console. @@ -906,7 +918,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 NOTE: These links should respect backports. See https://github.com/TheThingsNetwork/lorawan-stack/pull/1444/files#r333379706. --> -[unreleased]: https://github.com/TheThingsNetwork/lorawan-stack/compare/v3.8.4...HEAD +[unreleased]: https://github.com/TheThingsNetwork/lorawan-stack/compare/v3.8.5...HEAD +[3.8.5]: https://github.com/TheThingsNetwork/lorawan-stack/compare/v3.8.4...v3.8.5 [3.8.4]: https://github.com/TheThingsNetwork/lorawan-stack/compare/v3.8.3...v3.8.4 [3.8.3]: https://github.com/TheThingsNetwork/lorawan-stack/compare/v3.8.2...v3.8.3 [3.8.2]: https://github.com/TheThingsNetwork/lorawan-stack/compare/v3.7.2...v3.8.2 diff --git a/doc/config/_default/config.toml b/doc/config/_default/config.toml index 3314ef7a02..132a51dd72 100644 --- a/doc/config/_default/config.toml +++ b/doc/config/_default/config.toml @@ -27,7 +27,7 @@ pygmentsUseClasses = true keywords = [] github_repository = "https://github.com/TheThingsNetwork/lorawan-stack" github_repository_edit = "https://github.com/TheThingsNetwork/lorawan-stack/edit/master/doc/content" - version = "v3.8.4" + version = "v3.8.5" [markup] [markup.goldmark] diff --git a/doc/themes/the-things-stack/package.json b/doc/themes/the-things-stack/package.json index 9465dabb9d..fd47026efa 100644 --- a/doc/themes/the-things-stack/package.json +++ b/doc/themes/the-things-stack/package.json @@ -1,6 +1,6 @@ { "name": "hugo-theme-the-things-stack", - "version": "3.8.4", + "version": "3.8.5", "private": true, "description": "Hugo Theme for The Things Stack", "dependencies": { diff --git a/package.json b/package.json index 713fcf9522..c7348953f6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ttn-stack", - "version": "3.8.4", + "version": "3.8.5", "description": "The Things Stack", "main": "index.js", "repository": "https://github.com/TheThingsNetwork/lorawan-stack.git", diff --git a/pkg/version/ttn.go b/pkg/version/ttn.go index 83a0d176a5..288c90c658 100644 --- a/pkg/version/ttn.go +++ b/pkg/version/ttn.go @@ -3,4 +3,4 @@ package version // TTN Version -var TTN = "3.8.4-dev" +var TTN = "3.8.5-dev" diff --git a/sdk/js/package.json b/sdk/js/package.json index 73f9753153..587558af0a 100644 --- a/sdk/js/package.json +++ b/sdk/js/package.json @@ -1,6 +1,6 @@ { "name": "ttn-lw", - "version": "3.8.4", + "version": "3.8.5", "description": "The Things Stack for LoRaWAN JavaScript SDK", "url": "https://github.com/TheThingsNetwork/lorawan-stack/tree/master/sdk/js", "main": "dist/index.js",