Skip to content

Commit

Permalink
Add option to remember Airstage email and password (#22)
Browse files Browse the repository at this point in the history
  • Loading branch information
PatrickStankard authored Jan 13, 2025
1 parent 5d85278 commit 44e779a
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 19 deletions.
25 changes: 16 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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

Expand Down
18 changes: 11 additions & 7 deletions config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 5 additions & 2 deletions src/config-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,18 @@ 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;

if (accessTokenExpiry !== null) {
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;
Expand Down
55 changes: 54 additions & 1 deletion test/test-config-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand Down

0 comments on commit 44e779a

Please sign in to comment.