diff --git a/_locales/en/messages.json b/_locales/en/messages.json index a766cd2..2049f90 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -376,6 +376,10 @@ "message": "Login", "description": "Label for button on settings page." }, + "OTP_Code": { + "message": "OTP Code", + "description": "Label for field on settings page." + }, "Task_Display_Settings": { "message": "Task Display Settings", "description": "Header for settings section." diff --git a/src/background/onStateChange.ts b/src/background/onStateChange.ts index 6dd1dd9..1a97e68 100644 --- a/src/background/onStateChange.ts +++ b/src/background/onStateChange.ts @@ -16,6 +16,8 @@ export function onStoredStateChange(storedState: State) { baseUrl: getHostUrl(storedState.settings.connection), account: storedState.settings.connection.username, session: SessionName.DownloadStation, + device_id: storedState.settings.connection.deviceId, + device_name: storedState.settings.connection.deviceName, // Do NOT set password from here. It might not be set because of the "remember me" feature, so // we could erroneously overwrite it. Instead, read it once at startup time (if configured), and // otherwise, wait for an imperative login request message to be handled elsewhere. diff --git a/src/common/apis/connection.ts b/src/common/apis/connection.ts index 67a642e..45aedee 100644 --- a/src/common/apis/connection.ts +++ b/src/common/apis/connection.ts @@ -9,6 +9,9 @@ export async function testConnection( account: settings.username, passwd: settings.password, session: SessionName.DownloadStation, + otp_code: settings.otpCode, + device_id: settings.deviceId, + device_name: settings.deviceName, }); const loginResult = await api.Auth.Login({ timeout: 30000 }); diff --git a/src/common/apis/synology/Auth.ts b/src/common/apis/synology/Auth.ts index bc7dfa9..e772a46 100644 --- a/src/common/apis/synology/Auth.ts +++ b/src/common/apis/synology/Auth.ts @@ -9,11 +9,16 @@ export interface AuthLoginRequest extends BaseRequest { session: SessionName; // 2 is the lowest version that actually provides an sid. // 3 is the lowest version that DSM 7 supports. - version: 2 | 3; + // 6 is the lowest version that omit opt code login. + version: 2 | 3 | 6; + otp_code?: string | undefined; + device_id?: string | undefined; + device_name?: string | undefined; } export interface AuthLoginResponse { sid: string; + did?: string | undefined; } export interface AuthLogoutRequest extends BaseRequest { diff --git a/src/common/apis/synology/client.ts b/src/common/apis/synology/client.ts index fae21c5..58a04df 100644 --- a/src/common/apis/synology/client.ts +++ b/src/common/apis/synology/client.ts @@ -23,14 +23,24 @@ export interface SynologyClientSettings { account: string; passwd: string; session: SessionName; + otp_code: string; + device_id: string; + device_name: string; } -const SETTING_NAME_KEYS = typesafeUnionMembers({ +const SETTING_NAME_KEY_FLAGS = { baseUrl: true, account: true, passwd: true, session: true, -}); + otp_code: false, + device_id: false, + device_name: false, +}; + +const SETTING_NAME_KEYS = typesafeUnionMembers( + SETTING_NAME_KEY_FLAGS, +); export type ConnectionFailure = | { @@ -97,8 +107,11 @@ export class SynologyClient { private getValidatedSettings(): SynologyClientSettings | ConnectionFailure { const missingFields = SETTING_NAME_KEYS.filter((k) => { - const v = this.settings[k]; - return v == null || v.length === 0; + if (SETTING_NAME_KEY_FLAGS[k]) { + const v = this.settings[k]; + return v == null || v.length === 0; + } + return false; }); if (missingFields.length === 0) { return this.settings as SynologyClientSettings; @@ -115,24 +128,32 @@ export class SynologyClient { if (isConnectionFailure(settings)) { return settings; } else if (!this.loginPromise) { - const { baseUrl, ...restSettings } = settings; - this.loginPromise = Auth.Login(baseUrl, { + const { baseUrl, device_id, otp_code, ...restSettings } = settings; + + let option: any = { ...request, ...restSettings, - // First try with the lowest version that we can that supports sid, in an attempt to - // support the oldest DSMs we can. version: 2, - }) + }; + + if (device_id) { + option.device_id = device_id; + option.device_name = settings.device_name; + option.version = 6; + } else if (otp_code) { + option.otp_code = otp_code; + option.enable_device_token = "yes"; + option.version = 6; + } + + this.loginPromise = Auth.Login(baseUrl, option) .then((response) => { // We guess we're on DSM 7, which does not support earlier versions of the API. // We'd like to do this with an Info.Query, but DSM 7 erroneously reports that it // supports version 2, which it definitely does not. if (!response.success && response.error.code === NO_SUCH_METHOD_ERROR_CODE) { - return Auth.Login(baseUrl, { - ...request, - ...restSettings, - version: 3, - }); + option.version = 3; + return Auth.Login(baseUrl, option); } else { return response; } diff --git a/src/common/state/index.ts b/src/common/state/index.ts index e9b4177..8dfde7b 100644 --- a/src/common/state/index.ts +++ b/src/common/state/index.ts @@ -15,7 +15,7 @@ export function getHostUrl(settings: ConnectionSettings) { } export async function maybeMigrateState() { - const updated = migrateState(await browser.storage.local.get(null)); + const updated = migrateState(await browser.storage.local.get(null), null); await browser.storage.local.clear(); return browser.storage.local.set(updated); } diff --git a/src/common/state/migrations/8.ts b/src/common/state/migrations/8.ts new file mode 100644 index 0000000..b0291dd --- /dev/null +++ b/src/common/state/migrations/8.ts @@ -0,0 +1,51 @@ +import type { OmitStrict } from "../../types"; + +import type { State as State_7, Settings as Settings_7 } from "./7"; + +export { + VisibleTaskSettings, + TaskSortType, + CachedTasks, + NotificationSettings, + Logging, + BadgeDisplayType, +} from "./7"; + +export interface StateVersion { + stateVersion: 8; +} + +export interface ConnectionSettings { + hostname: string; + port: number; + username: string; + password: string | undefined; + rememberPassword: boolean; + otpCode: string; + deviceId: string; + deviceName: string; +} + +export interface Settings extends OmitStrict { + connection: ConnectionSettings; +} + +export interface State extends StateVersion, OmitStrict { + settings: Settings; +} + +export function migrate(state: State_7): State { + return { + ...state, + stateVersion: 8, + settings: { + ...state.settings, + connection: { + ...state.settings.connection, + otpCode: "", + deviceId: "", + deviceName: "", + }, + }, + }; +} diff --git a/src/common/state/migrations/latest.ts b/src/common/state/migrations/latest.ts index 78ea929..0edb661 100644 --- a/src/common/state/migrations/latest.ts +++ b/src/common/state/migrations/latest.ts @@ -9,4 +9,4 @@ export { Logging, StateVersion, BadgeDisplayType, -} from "./7"; +} from "./8"; diff --git a/src/common/state/migrations/update.ts b/src/common/state/migrations/update.ts index f44c09f..ce97516 100644 --- a/src/common/state/migrations/update.ts +++ b/src/common/state/migrations/update.ts @@ -6,8 +6,9 @@ import { migrate as migrate3to4 } from "./4"; import { migrate as migrate4to5 } from "./5"; import { migrate as migrate5to6 } from "./6"; import { migrate as migrate6to7 } from "./7"; +import { migrate as migrate7to8 } from "./8"; -const LATEST_STATE_VERSION: StateVersion["stateVersion"] = 7; +const LATEST_STATE_VERSION: StateVersion["stateVersion"] = 8; const MIGRATIONS: ((state: any) => any)[] = [ migrate0to1, migrate1to2, @@ -16,6 +17,7 @@ const MIGRATIONS: ((state: any) => any)[] = [ migrate4to5, migrate5to6, migrate6to7, + migrate7to8, ]; interface AnyStateVersion { @@ -42,8 +44,19 @@ function getStartingVersion(state: any) { } } -export function migrateState(state: any | null): State { +function getTargetVersion(state: any) { + if (state == null) { + return LATEST_STATE_VERSION; + } else if (isVersioned(state)) { + return state.stateVersion; + } else { + return LATEST_STATE_VERSION; + } +} + +export function migrateState(state: any | null, target: any | null): State { let version = getStartingVersion(state); + let target_version = getTargetVersion(target); if (version > LATEST_STATE_VERSION) { // If the user has downgraded the extension for some reason, throw out their state. There isn't @@ -52,7 +65,7 @@ export function migrateState(state: any | null): State { version = 0; } - MIGRATIONS.slice(version).forEach((migration) => { + MIGRATIONS.slice(version, target_version).forEach((migration) => { state = migration(state); }); diff --git a/src/settings/ConnectionSettings.tsx b/src/settings/ConnectionSettings.tsx index c8b8b16..f5b050e 100644 --- a/src/settings/ConnectionSettings.tsx +++ b/src/settings/ConnectionSettings.tsx @@ -117,6 +117,20 @@ export class ConnectionSettings extends React.PureComponent { +
  • + {browser.i18n.getMessage("OTP_Code")} +
    + { + this.setSetting("otpCode", e.currentTarget.value); + }} + /> +
    +
  • +