From bcc47bfb13ae8773de0fca9442aa679da324a79d Mon Sep 17 00:00:00 2001 From: sacha Date: Mon, 1 Dec 2025 14:53:39 +0100 Subject: [PATCH 1/3] Add listSessionDevices method --- src/main/__tests__/sessionDevices.spec.ts | 68 +++++++++++++++++++++++ src/main/main.ts | 9 ++- src/main/models.ts | 14 +++++ src/main/profileClient.ts | 8 ++- 4 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 src/main/__tests__/sessionDevices.spec.ts diff --git a/src/main/__tests__/sessionDevices.spec.ts b/src/main/__tests__/sessionDevices.spec.ts new file mode 100644 index 0000000..f93abff --- /dev/null +++ b/src/main/__tests__/sessionDevices.spec.ts @@ -0,0 +1,68 @@ +import fetchMock from 'jest-fetch-mock' +import { defineWindowProperty, headers, mockWindowCrypto } from './helpers/testHelpers' +import { popNextRandomString } from './helpers/randomStringMock' +import { createDefaultTestClient } from './helpers/clientFactory' + +beforeAll(() => { + fetchMock.enableMocks() + defineWindowProperty('location') + defineWindowProperty('crypto', mockWindowCrypto) +}) + +beforeEach(() => { + document.body.innerHTML = '' + jest.resetAllMocks() + fetchMock.resetMocks() + popNextRandomString() +}) + +test('list session devices', async () => { + // Given + const { client, domain } = createDefaultTestClient() + const accessToken = '456' + + const listSessionDevicesCall = fetchMock.mockResponseOnce( + JSON.stringify({ + sessionDevices: [ + { + id: "UUID", + ip: "192.168.65.1", + operatingSystem: "Android", + userAgentName: "Chrome", + deviceClass: "Phone", + deviceName: "Google Nexus 6", + firstConnection: "date1", + lastConnection: "date2" + } + ] + }) + ) + + const result = await client.listSessionDevices(accessToken) + + expect(listSessionDevicesCall).toHaveBeenCalledWith( + `https://${domain}/identity/v1/session-devices`, + { + method: 'GET', + headers: expect.objectContaining({ + ...headers.accessToken(accessToken) + }) + } + ) + + expect(result).toEqual({ + sessionDevices: [ + { + id: "UUID", + ip: "192.168.65.1", + operatingSystem: "Android", + userAgentName: "Chrome", + deviceClass: "Phone", + deviceName: "Google Nexus 6", + firstConnection: "date1", + lastConnection: "date2" + } + ] + }) + +}) diff --git a/src/main/main.ts b/src/main/main.ts index 949b9d3..fd03469 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -6,7 +6,8 @@ import { PasswordlessResponse, MFA, OrchestrationToken, - PasswordStrength + PasswordStrength, + SessionDeviceListResponse } from './models' import OAuthClient, { LoginWithPasswordParams, @@ -96,6 +97,7 @@ export type Client = { getSignupData: (signupToken: string) => Promise getUser: (params: GetUserParams) => Promise listMfaCredentials: (accessToken: string) => Promise + listSessionDevices: (accessToken: string) => Promise listTrustedDevices: (accessToken: string) => Promise listWebAuthnDevices: (accessToken: string) => Promise loginFromSession: (options?: AuthOptions) => Promise @@ -264,6 +266,10 @@ export function createClient(creationConfig: Config): Client { return apiClients.then((clients) => clients.mfa.listMfaCredentials(accessToken)) } + function listSessionDevices(accessToken: string) { + return apiClients.then((clients) => clients.profile.listSessionDevices(accessToken)) + } + function listWebAuthnDevices(accessToken: string) { return apiClients.then((clients) => clients.webAuthn.listWebAuthnDevices(accessToken)) } @@ -450,6 +456,7 @@ export function createClient(creationConfig: Config): Client { getSignupData, getUser, listMfaCredentials, + listSessionDevices, listTrustedDevices, listWebAuthnDevices, loginFromSession, diff --git a/src/main/models.ts b/src/main/models.ts index 34195b5..a1602c9 100644 --- a/src/main/models.ts +++ b/src/main/models.ts @@ -396,6 +396,20 @@ export type Consent = { status: ConsentStatus } +export type SessionDevice = { + id: string, + ip?: string, + operatingSystem?: string, + userAgentName?: string, + deviceClass?: string, + deviceName?: string, + firstConnection: string, + lastConnection: string +} + +export type SessionDeviceListResponse = { + sessionDevices: SessionDevice[] +} /** * This type represents the settings of a ReachFive account's stored in the backend. */ diff --git a/src/main/profileClient.ts b/src/main/profileClient.ts index 26c7a4a..e047e01 100644 --- a/src/main/profileClient.ts +++ b/src/main/profileClient.ts @@ -1,7 +1,7 @@ import type { CaptchaParams } from './captcha' import type { HttpClient } from './httpClient' import type { IdentityEventManager } from './identityEventManager' -import type { ApiClientConfig } from './main' +import { ApiClientConfig, SessionDeviceListResponse } from './main' import type { OpenIdUser, Profile } from './models' export type UpdateEmailParams = { @@ -112,6 +112,7 @@ export default class ProfileClient { private sendEmailVerificationUrl: string private sendPhoneNumberVerificationUrl: string + private sessionDevicesUrl: string private signupDataUrl: string private unlinkUrl: string private updateEmailUrl: string @@ -129,6 +130,7 @@ export default class ProfileClient { this.sendEmailVerificationUrl = '/send-email-verification' this.sendPhoneNumberVerificationUrl = '/send-phone-number-verification' + this.sessionDevicesUrl = '/session-devices' this.signupDataUrl = '/signup/data' this.unlinkUrl = '/unlink' this.updateEmailUrl = '/update-email' @@ -140,6 +142,10 @@ export default class ProfileClient { this.verifyEmailUrl = '/verify-email' } + listSessionDevices(accessToken: string): Promise { + return this.http.get(this.sessionDevicesUrl, { accessToken }) + } + getSignupData(signupToken: string): Promise { return this.http.get(this.signupDataUrl, { query: { From 4b3025203aad36ae986c914ea8dc33612ac4247d Mon Sep 17 00:00:00 2001 From: sacha Date: Mon, 15 Dec 2025 16:59:41 +0100 Subject: [PATCH 2/3] Add remove SessionDevice method --- src/main/__tests__/sessionDevices.spec.ts | 24 +++++++++++++++++++++++ src/main/main.ts | 7 +++++++ src/main/profileClient.ts | 11 +++++++++++ 3 files changed, 42 insertions(+) diff --git a/src/main/__tests__/sessionDevices.spec.ts b/src/main/__tests__/sessionDevices.spec.ts index f93abff..793aafa 100644 --- a/src/main/__tests__/sessionDevices.spec.ts +++ b/src/main/__tests__/sessionDevices.spec.ts @@ -64,5 +64,29 @@ test('list session devices', async () => { } ] }) +}) + +test('remove session device', async () => { + const { client, domain } = createDefaultTestClient() + const accessToken = 'accesstoken' + const sessionDeviceId = 'UUID' + + const removeSessionDeviceCall = fetchMock.mockResponseOnce( + JSON.stringify('') + ) + + const result = await client.removeSessionDevice({accessToken, sessionDeviceId}) + + expect(removeSessionDeviceCall).toHaveBeenCalledWith( + `https://${domain}/identity/v1/session-devices/${sessionDeviceId}`, + { + method: 'DELETE', + headers: expect.objectContaining({ + ...headers.accessToken(accessToken) + }) + } + ) + expect(result).toEqual('') }) + diff --git a/src/main/main.ts b/src/main/main.ts index fd03469..4438110 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -47,6 +47,7 @@ import ProfileClient, { EmailVerificationParams, GetUserParams, PhoneNumberVerificationParams, + RemoveSessionDeviceParams, RequestAccountRecoveryParams, RequestPasswordResetParams, UnlinkParams, @@ -114,6 +115,7 @@ export type Client = { remoteSettings: Promise removeMfaEmail: (params: RemoveMfaEmailParams) => Promise removeMfaPhoneNumber: (params: RemoveMfaPhoneNumberParams) => Promise + removeSessionDevice: (params: RemoveSessionDeviceParams) => Promise removeTrustedDevice: (params: DeleteTrustedDeviceParams) => Promise removeWebAuthnDevice: (accessToken: string, deviceId: string) => Promise requestAccountRecovery: (params: RequestAccountRecoveryParams) => Promise @@ -341,6 +343,10 @@ export function createClient(creationConfig: Config): Client { return apiClients.then((clients) => clients.mfa.removeMfaPhoneNumber(params)) } + function removeSessionDevice(params: RemoveSessionDeviceParams) { + return apiClients.then((clients) => clients.profile.removeSessionDevice(params)) + } + function removeTrustedDevice(params: DeleteTrustedDeviceParams) { return apiClients.then((clients) => clients.mfa.deleteTrustedDevices(params)) } @@ -473,6 +479,7 @@ export function createClient(creationConfig: Config): Client { remoteSettings, removeMfaEmail, removeMfaPhoneNumber, + removeSessionDevice, removeTrustedDevice, removeWebAuthnDevice, requestAccountRecovery, diff --git a/src/main/profileClient.ts b/src/main/profileClient.ts index e047e01..305c69a 100644 --- a/src/main/profileClient.ts +++ b/src/main/profileClient.ts @@ -58,6 +58,12 @@ type EmailVerificationCodeUpdatePasswordParams = { verificationCode: string password: string } + +export type RemoveSessionDeviceParams = { + accessToken: string + sessionDeviceId: string +} + type SmsVerificationCodeUpdatePasswordParams = { accessToken?: string phoneNumber: string @@ -146,6 +152,11 @@ export default class ProfileClient { return this.http.get(this.sessionDevicesUrl, { accessToken }) } + removeSessionDevice(params: RemoveSessionDeviceParams): Promise { + const { accessToken, sessionDeviceId } = params + return this.http.remove(`${this.sessionDevicesUrl}/${sessionDeviceId}`, {accessToken}) + } + getSignupData(signupToken: string): Promise { return this.http.get(this.signupDataUrl, { query: { From a5919d70e653885a7dc721b2eb7727ea4aa77f0a Mon Sep 17 00:00:00 2001 From: sacha Date: Mon, 15 Dec 2025 17:03:28 +0100 Subject: [PATCH 3/3] Revert "Add remove SessionDevice method" This reverts commit 4b3025203aad36ae986c914ea8dc33612ac4247d. --- src/main/__tests__/sessionDevices.spec.ts | 24 ----------------------- src/main/main.ts | 7 ------- src/main/profileClient.ts | 11 ----------- 3 files changed, 42 deletions(-) diff --git a/src/main/__tests__/sessionDevices.spec.ts b/src/main/__tests__/sessionDevices.spec.ts index 793aafa..f93abff 100644 --- a/src/main/__tests__/sessionDevices.spec.ts +++ b/src/main/__tests__/sessionDevices.spec.ts @@ -64,29 +64,5 @@ test('list session devices', async () => { } ] }) -}) - -test('remove session device', async () => { - const { client, domain } = createDefaultTestClient() - const accessToken = 'accesstoken' - const sessionDeviceId = 'UUID' - - const removeSessionDeviceCall = fetchMock.mockResponseOnce( - JSON.stringify('') - ) - - const result = await client.removeSessionDevice({accessToken, sessionDeviceId}) - - expect(removeSessionDeviceCall).toHaveBeenCalledWith( - `https://${domain}/identity/v1/session-devices/${sessionDeviceId}`, - { - method: 'DELETE', - headers: expect.objectContaining({ - ...headers.accessToken(accessToken) - }) - } - ) - expect(result).toEqual('') }) - diff --git a/src/main/main.ts b/src/main/main.ts index 4438110..fd03469 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -47,7 +47,6 @@ import ProfileClient, { EmailVerificationParams, GetUserParams, PhoneNumberVerificationParams, - RemoveSessionDeviceParams, RequestAccountRecoveryParams, RequestPasswordResetParams, UnlinkParams, @@ -115,7 +114,6 @@ export type Client = { remoteSettings: Promise removeMfaEmail: (params: RemoveMfaEmailParams) => Promise removeMfaPhoneNumber: (params: RemoveMfaPhoneNumberParams) => Promise - removeSessionDevice: (params: RemoveSessionDeviceParams) => Promise removeTrustedDevice: (params: DeleteTrustedDeviceParams) => Promise removeWebAuthnDevice: (accessToken: string, deviceId: string) => Promise requestAccountRecovery: (params: RequestAccountRecoveryParams) => Promise @@ -343,10 +341,6 @@ export function createClient(creationConfig: Config): Client { return apiClients.then((clients) => clients.mfa.removeMfaPhoneNumber(params)) } - function removeSessionDevice(params: RemoveSessionDeviceParams) { - return apiClients.then((clients) => clients.profile.removeSessionDevice(params)) - } - function removeTrustedDevice(params: DeleteTrustedDeviceParams) { return apiClients.then((clients) => clients.mfa.deleteTrustedDevices(params)) } @@ -479,7 +473,6 @@ export function createClient(creationConfig: Config): Client { remoteSettings, removeMfaEmail, removeMfaPhoneNumber, - removeSessionDevice, removeTrustedDevice, removeWebAuthnDevice, requestAccountRecovery, diff --git a/src/main/profileClient.ts b/src/main/profileClient.ts index 305c69a..e047e01 100644 --- a/src/main/profileClient.ts +++ b/src/main/profileClient.ts @@ -58,12 +58,6 @@ type EmailVerificationCodeUpdatePasswordParams = { verificationCode: string password: string } - -export type RemoveSessionDeviceParams = { - accessToken: string - sessionDeviceId: string -} - type SmsVerificationCodeUpdatePasswordParams = { accessToken?: string phoneNumber: string @@ -152,11 +146,6 @@ export default class ProfileClient { return this.http.get(this.sessionDevicesUrl, { accessToken }) } - removeSessionDevice(params: RemoveSessionDeviceParams): Promise { - const { accessToken, sessionDeviceId } = params - return this.http.remove(`${this.sessionDevicesUrl}/${sessionDeviceId}`, {accessToken}) - } - getSignupData(signupToken: string): Promise { return this.http.get(this.signupDataUrl, { query: {