Skip to content

Commit

Permalink
Merge pull request #7 from kyle-seongwoo-jun/feat/refresh-token-when-…
Browse files Browse the repository at this point in the history
…expired

fix: refresh api token when expired
  • Loading branch information
kyle-seongwoo-jun authored Feb 18, 2024
2 parents 379e6cd + f023dc0 commit 1fb7c1b
Show file tree
Hide file tree
Showing 11 changed files with 97 additions and 69 deletions.
6 changes: 3 additions & 3 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable no-console */

import { AuthException } from '../navien/exceptions';
import { NavienException } from '../navien/exceptions';
import { NavienAuth } from '../navien/navien.auth';

if (process.argv.length < 4) {
Expand All @@ -20,8 +20,8 @@ async function main() {
}

main().catch((error) => {
if (error instanceof AuthException) {
console.error('Error:', error.message);
if (error instanceof NavienException) {
console.error(error.toString());
return;
}
console.error('Unknown error occurred. Please report this to the developer:', error);
Expand Down
13 changes: 2 additions & 11 deletions src/navien/exceptions/api.exception.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,12 @@
import { NavienResponse } from '../interfaces';
import { NavienException } from './navien.exception';

export class ApiException<T> extends Error {
export class ApiException<T> extends NavienException {
constructor(
readonly response: T,
message: string,
) {
super(message);

this.name = 'ApiException';

// Maintaining proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, ApiException);
}

// ES5 compatible
Object.setPrototypeOf(this, ApiException.prototype);
}

static from<T>(response: NavienResponse<T>): ApiException<NavienResponse<T>> {
Expand Down
13 changes: 2 additions & 11 deletions src/navien/exceptions/auth.exception.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
import { NavienException } from './navien.exception';

export class AuthException extends Error {
export class AuthException extends NavienException {
constructor(message: string) {
super(message);

this.name = 'AuthException';

// Maintaining proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, AuthException);
}

// ES5 compatible
Object.setPrototypeOf(this, AuthException.prototype);
}
}
14 changes: 3 additions & 11 deletions src/navien/exceptions/configuration.exception.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
export class ConfigurationException extends Error {
import { NavienException } from './navien.exception';

export class ConfigurationException extends NavienException {
constructor(
readonly propertyName: string,
message: string,
) {
super(message);

this.name = 'ConfigurationException';

// Maintaining proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, ConfigurationException);
}

// ES5 compatible
Object.setPrototypeOf(this, ConfigurationException.prototype);
}

static empty(propertyName: string): ConfigurationException {
Expand Down
1 change: 1 addition & 0 deletions src/navien/exceptions/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './api.exception';
export * from './auth.exception';
export * from './configuration.exception';
export * from './navien.exception';
19 changes: 19 additions & 0 deletions src/navien/exceptions/navien.exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export abstract class NavienException extends Error {
constructor(readonly message: string) {
super(message);

this.name = new.target.name;

// Maintaining proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, new.target);
}

// ES5 compatible
Object.setPrototypeOf(this, new.target.prototype);
}

public toString(): string {
return `${this.name}: ${this.message}`;
}
}
24 changes: 11 additions & 13 deletions src/navien/navien.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import assert from 'assert';
import { Logger } from 'homebridge';
import fetch, { BodyInit, HeadersInit, Response } from 'node-fetch';

import { AwsSession } from '../aws/aws.session';
import { API_URL } from './constants';
import { ApiException } from './exceptions';
import { CommonResponse, Device, DevicesResponse, ResponseCode } from './interfaces';
Expand All @@ -22,10 +21,6 @@ export class NavienApi {
return this.sessionManager.session;
}

private get awsSession(): AwsSession | undefined {
return this.sessionManager.awsSession;
}

private get user(): NavienUser | undefined {
return this.sessionManager.user;
}
Expand Down Expand Up @@ -76,15 +71,18 @@ export class NavienApi {

if (!response.ok) {
const json = await response.json() as CommonResponse;
if (response.status === 401) {
// login detected from another device
// TODO: re-login and retry
this.log.warn('Login detected from another device. We will re-login and retry.', json);
// login detected from another device
if (json.code === ResponseCode.COMMON_NOT_AUTHORIZED) {
this.log.error('Login detected from another device.');
// TODO: re-login and retry if authMode is 'account'
}
if (response.status === 403) {
// token expired
// TODO: refresh token and retry
this.log.warn('Token expired. We will refresh token and retry.', json);
// token expired
if (json.code === ResponseCode.COMMON_TOKEN_EXPIRED) {
this.log.warn('Token expired. We will refresh token and retry.');

// refresh token and retry
await this.sessionManager.refreshSession();
return await this.request(method, path, options);
}
throw ApiException.from(json);
}
Expand Down
8 changes: 6 additions & 2 deletions src/navien/navien.auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import fetch from 'node-fetch';
import { URLSearchParams } from 'url';

import { API_URL, LOGIN_API_URL, USER_AGENT } from './constants';
import { AuthException } from './exceptions';
import { CommonResponse, Login2Response, LoginResponse, RefreshTokenResponse } from './interfaces';
import { ApiException, AuthException } from './exceptions';
import { CommonResponse, Login2Response, LoginResponse, RefreshTokenResponse, ResponseCode } from './interfaces';

export class NavienAuth {
constructor(
Expand Down Expand Up @@ -66,6 +66,10 @@ export class NavienAuth {
});

const json = await response.json() as Login2Response;
if (json.code !== ResponseCode.SUCCESS) {
throw ApiException.from(json);
}

return json;
}

Expand Down
24 changes: 17 additions & 7 deletions src/navien/navien.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Logger } from 'homebridge';

import { AwsPubSub } from '../aws/pubsub';
import { NavienHomebridgePlatform } from '../platform';
import { ApiException } from './exceptions';
import { NavienException } from './exceptions';
import { Device } from './interfaces';
import { NavienApi } from './navien.api';
import { NavienAuth } from './navien.auth';
Expand Down Expand Up @@ -40,8 +40,16 @@ export class NavienService {
// refresh aws session and reconnect if connection is disrupted
if (connectionState === ConnectionState.ConnectionDisrupted) {
this.log.info('[AWS PubSub] Refreshing AWS session and reconnecting...');
const newSession = await this.sessionManager.refreshAwsSession();
pubsub.setSession(newSession);
try {
const newSession = await this.sessionManager.refreshAwsSession();
pubsub.setSession(newSession);
} catch (error) {
if (error instanceof NavienException) {
this.log.error(`[AWS PubSub] ${error}`);
return;
}
this.log.error('[AWS PubSub] Failed to refresh AWS session:', error);
}
}
});
}
Expand Down Expand Up @@ -73,8 +81,8 @@ export class NavienService {
this.log.debug('Setting power to', power, 'for device', device.name);

const success = await device.setPower(power).then(() => true).catch((error) => {
if (error instanceof ApiException) {
this.log.error('APIException:', error.message);
if (error instanceof NavienException) {
this.log.error(error.toString());
return false;
}
this.log.error('Unknown error while setting power for device', device.name, ':', error);
Expand All @@ -86,14 +94,15 @@ export class NavienService {
} else {
this.log.error('Failed to set power to', power, 'for device', device.name);
}

}

public async setTemperature(device: NavienDevice, temperature: number) {
this.log.debug('Setting temperature to', temperature, 'for device', device.name);

const success = await device.setTemperature(temperature).then(() => true).catch((error) => {
if (error instanceof ApiException) {
this.log.error('APIException:', error.message);
if (error instanceof NavienException) {
this.log.error(error.toString());
return false;
}
this.log.error('Unknown error while setting temperature for device', device.name, ':', error);
Expand All @@ -105,5 +114,6 @@ export class NavienService {
} else {
this.log.error('Failed to set temperature to', temperature, 'for device', device.name);
}

}
}
34 changes: 30 additions & 4 deletions src/navien/navien.session-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Logger } from 'homebridge';
import { AwsSession } from '../aws/aws.session';
import { NavienPlatformConfig } from '../platform';
import { Persist } from '../utils/persist.util';
import { ConfigurationException } from './exceptions';
import { AuthException, ConfigurationException } from './exceptions';
import { ResponseCode } from './interfaces';
import { NavienAuth } from './navien.auth';
import { NavienSession } from './navien.session';
Expand Down Expand Up @@ -59,20 +59,46 @@ export class NavienSessionManager {
]);
}

public async refreshSession() {
const { session } = this;
if (!session) {
throw new Error('Please call ready() first.');
}

// refresh token
const response = await this.auth.refreshToken(session.refreshToken);
if (!response.data) {
throw new AuthException(`Refresh token may be expired. refreshToken: ${session.refreshToken}`);
}

// save new session
const newSession = this._session = NavienSession.fromAuthInfo(response.data.authInfo, session.refreshToken);
await this.storage.set('session', session);

return newSession;
}

public async refreshAwsSession() {
if (!this._session || !this._user) {
throw new Error('Please call ready() first.');
}

const { accessToken } = this._session;
// refresh api session if expired
let session = this._session;
if (!session.hasValidToken()) {
session = await this.refreshSession();
}

// login to get new aws session
const { accessToken } = session;
const { userId, accountSeq } = this._user;
const response = await this.auth.login2(accessToken, userId, accountSeq);
assert(response.data, 'No data in login2 response.');

// save new aws session
const { authInfo } = response.data;
const awsSession = AwsSession.fromResponse(authInfo);
const awsSession = this._awsSession = AwsSession.fromResponse(authInfo);

this._awsSession = awsSession;
return awsSession;
}

Expand Down
10 changes: 3 additions & 7 deletions src/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { API, Characteristic, DynamicPlatformPlugin, Logger, PlatformAccessory,
import path from 'path';

import ElectricMat from './homebridge/electric-mat.device';
import { AuthException, ConfigurationException } from './navien/exceptions';
import { NavienException } from './navien/exceptions';
import { NavienDevice } from './navien/navien.device';
import { NavienService } from './navien/navien.service';
import { PLATFORM_NAME, PLUGIN_NAME } from './settings';
Expand Down Expand Up @@ -71,12 +71,8 @@ export class NavienHomebridgePlatform implements DynamicPlatformPlugin {
try {
await this.navienService.ready();
} catch (error) {
if (error instanceof ConfigurationException) {
this.log.error('ConfigurationException:', error.message);
return;
}
if (error instanceof AuthException) {
this.log.error('AuthException:', error.message);
if (error instanceof NavienException) {
this.log.error(error.toString());
return;
}
this.log.error(
Expand Down

0 comments on commit 1fb7c1b

Please sign in to comment.