diff --git a/README.md b/README.md index 0bc893d..c5be014 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ installed and configured this plugin: "language": "en", "email": "test@example.com", "password": "test1234", + "rememberEmailAndPassword": false, "accessToken": null, "accessTokenExpiry": null, "refreshToken": null, @@ -49,15 +50,21 @@ installed and configured this plugin: } ``` -The `email` and `password` values should only be set until the initial -authentication with the Airstage API has been completed successfully. At that -point, they will be set to `null`, and the `accessToken`, `accessTokenExpiry`, -and `refreshToken` values will be set. These values will be used to authenticate -with the Airstage API going forward. - -If the access token can't be refreshed for whatever reason, you will have to -re-authenticate with the Airstage API by setting `email` and `password` in the -config, and restarting Homebridge. +If the `rememberEmailAndPassword` option is disabled, the `email` and `password` +values will only be set until authentication with the Airstage API has been +completed successfully. At that point, they will be set to `null`. This is the +default behavior, in order to prevent your Airstage credentials from being +stored in plaintext in the Homebridge config. + +If the `rememberEmailAndPassword` option is enabled, the `email` and `password` +values will continue to be set after authentication with the Airstage API has +been completed successfully. This is useful for when the access token can't be +refreshed for whatever reason, and you need to re-authenticate with the +Airstage API. + +Once authentication with the Airstage API has been completed successfully, +the `accessToken`, `accessTokenExpiry`, and `refreshToken` values will be set. +These values will be used to authenticate with the Airstage API going forward. ## Accessories diff --git a/config.schema.json b/config.schema.json index ef4af90..53ed88a 100644 --- a/config.schema.json +++ b/config.schema.json @@ -43,32 +43,36 @@ "title": "Airstage Email", "type": "string", "format": "email", - "default": "", - "description": "This will be automatically removed after the initial authentication with the Airstage API, and an access token will be used instead." + "default": "" }, "password": { "title": "Airstage Password", "type": "string", - "default": "", - "description": "This will be automatically removed after the initial authentication with the Airstage API, and an access token will be used instead." + "default": "" + }, + "rememberEmailAndPassword": { + "title": "Remember Airstage Email and Password", + "type": "boolean", + "default": false, + "description": "If enabled, the Airstage email and password will be stored in the config after a successful authentication with the Airstage API." }, "accessToken": { "title": "Airstage Access Token", "type": "string", "default": "", - "description": "This will be automatically be set after the initial authentication with the Airstage API." + "description": "This will be automatically be set after a successful authentication with the Airstage API." }, "accessTokenExpiry": { "title": "Airstage Access Token Expiry", "type": "string", "default": "", - "description": "This will be automatically be set after the initial authentication with the Airstage API." + "description": "This will be automatically be set after a successful authentication with the Airstage API." }, "refreshToken": { "title": "Airstage Refresh Token", "type": "string", "default": "", - "description": "This will be automatically be set after the initial authentication with the Airstage API." + "description": "This will be automatically be set after a successful authentication with the Airstage API." }, "enableThermostat": { "title": "Enable thermostat control", diff --git a/src/config-manager.js b/src/config-manager.js index d8684c2..dd6eb26 100644 --- a/src/config-manager.js +++ b/src/config-manager.js @@ -12,6 +12,7 @@ class ConfigManager { updateConfigWithAccessToken(accessToken, accessTokenExpiry, refreshToken) { const homebridgeConfigPath = this.api.user.configPath(); const homebridgeConfigString = this._readFileSync(homebridgeConfigPath); + let homebridgeConfig = JSON.parse(homebridgeConfigString); let accessTokenExpiryISO = null; @@ -19,8 +20,10 @@ class ConfigManager { accessTokenExpiryISO = accessTokenExpiry.toISOString(); } - this.config.email = null; - this.config.password = null; + if (this.config.rememberEmailAndPassword === false) { + this.config.email = null; + this.config.password = null; + } this.config.accessToken = accessToken; this.config.accessTokenExpiry = accessTokenExpiryISO; diff --git a/test/test-config-manager.js b/test/test-config-manager.js index afb71ca..95f3bfc 100644 --- a/test/test-config-manager.js +++ b/test/test-config-manager.js @@ -12,10 +12,11 @@ const mockApi = { } }; -test('ConfigManager#updateConfigWithAccessToken updates the config with new access token', (context) => { +test('ConfigManager#updateConfigWithAccessToken updates the config with new access token, and does not persist Airstage email and password', (context) => { let platformConfig = { 'email': 'test@example.com', 'password': 'test1234', + 'rememberEmailAndPassword': false, 'accessToken': null, 'accessTokenExpiry': null, 'refreshToken': null @@ -62,10 +63,62 @@ test('ConfigManager#updateConfigWithAccessToken updates the config with new acce assert.strictEqual(configManager._writeFileSync.mock.calls[0].arguments[1], expectedHomebridgeConfig); }); +test('ConfigManager#updateConfigWithAccessToken updates the config with new access token, and persists Airstage email and password', (context) => { + let platformConfig = { + 'email': 'test@example.com', + 'password': 'test1234', + 'rememberEmailAndPassword': true, + 'accessToken': null, + 'accessTokenExpiry': null, + 'refreshToken': null + }; + let homebridgeConfig = { + 'platforms': [ + platformConfig + ] + }; + const accessToken = 'testAccessToken'; + const accessTokenExpiry = new Date(); + const refreshToken = 'testRefreshToken'; + const configManager = new ConfigManager(platformConfig, mockApi); + context.mock.method( + configManager, + '_readFileSync', + (path) => { + return JSON.stringify(homebridgeConfig, null, 4); + } + ); + context.mock.method( + configManager, + '_writeFileSync', + (path, jsonString) => {} + ); + + const result = configManager.updateConfigWithAccessToken( + accessToken, + accessTokenExpiry, + refreshToken + ); + + const expectedHomebridgeConfig = JSON.stringify(homebridgeConfig, null, 4); + assert.strictEqual(result, true); + assert.strictEqual(platformConfig.email, 'test@example.com'); + assert.strictEqual(platformConfig.password, 'test1234'); + assert.strictEqual(platformConfig.accessToken, accessToken); + assert.strictEqual(platformConfig.accessTokenExpiry, accessTokenExpiry.toISOString()); + assert.strictEqual(platformConfig.refreshToken, refreshToken); + assert.strictEqual(configManager._readFileSync.mock.calls.length, 1); + assert.strictEqual(configManager._readFileSync.mock.calls[0].arguments[0], '/test/path'); + assert.strictEqual(configManager._writeFileSync.mock.calls.length, 1); + assert.strictEqual(configManager._writeFileSync.mock.calls[0].arguments[0], '/test/path'); + assert.strictEqual(configManager._writeFileSync.mock.calls[0].arguments[1], expectedHomebridgeConfig); +}); + test('ConfigManager#updateConfigWithAccessToken does not update the config with existing access token', (context) => { let platformConfig = { 'email': null, 'password': null, + 'rememberEmailAndPassword': false, 'accessToken': 'testAccessToken', 'accessTokenExpiry': '2024-06-12T17:29:20.553Z', 'refreshToken': 'testRefreshToken'