From 734c38578a4c7533e0e6759d199cc1858654879e Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Mon, 21 Jul 2025 23:43:12 -0300 Subject: [PATCH 1/8] chore: send server password param to delete account endpoint --- .../src/Domain/Client/User/UserApiService.ts | 8 +++-- .../Client/User/UserApiServiceInterface.ts | 5 ++- .../Request/User/UserDeletionRequestParams.ts | 1 + .../Protection/ProtectionClientInterface.ts | 7 +++- .../services/src/Domain/User/UserService.ts | 18 +++++----- .../Services/Protection/ProtectionService.ts | 33 ++++++++++++++++--- 6 files changed, 56 insertions(+), 16 deletions(-) diff --git a/packages/api/src/Domain/Client/User/UserApiService.ts b/packages/api/src/Domain/Client/User/UserApiService.ts index 98b112fb67f..a77c52a5322 100644 --- a/packages/api/src/Domain/Client/User/UserApiService.ts +++ b/packages/api/src/Domain/Client/User/UserApiService.ts @@ -27,12 +27,16 @@ export class UserApiService implements UserApiServiceInterface { this.operationsInProgress = new Map() } - async deleteAccount(userUuid: string): Promise> { + async deleteAccount(dto: { + userUuid: string + serverPassword: string + }): Promise> { this.lockOperation(UserApiOperations.DeletingAccount) try { const response = await this.userServer.deleteAccount({ - userUuid: userUuid, + userUuid: dto.userUuid, + serverPassword: dto.serverPassword, }) this.unlockOperation(UserApiOperations.DeletingAccount) diff --git a/packages/api/src/Domain/Client/User/UserApiServiceInterface.ts b/packages/api/src/Domain/Client/User/UserApiServiceInterface.ts index a574dd86509..34927a7fa25 100644 --- a/packages/api/src/Domain/Client/User/UserApiServiceInterface.ts +++ b/packages/api/src/Domain/Client/User/UserApiServiceInterface.ts @@ -22,5 +22,8 @@ export interface UserApiServiceInterface { requestType: UserRequestType }): Promise> - deleteAccount(userUuid: string): Promise> + deleteAccount(dto: { + userUuid: string + serverPassword: string | undefined + }): Promise> } diff --git a/packages/api/src/Domain/Request/User/UserDeletionRequestParams.ts b/packages/api/src/Domain/Request/User/UserDeletionRequestParams.ts index 21b77caca56..e5b670891af 100644 --- a/packages/api/src/Domain/Request/User/UserDeletionRequestParams.ts +++ b/packages/api/src/Domain/Request/User/UserDeletionRequestParams.ts @@ -1,4 +1,5 @@ export type UserDeletionRequestParams = { userUuid: string + serverPassword: string [additionalParam: string]: unknown } diff --git a/packages/services/src/Domain/Protection/ProtectionClientInterface.ts b/packages/services/src/Domain/Protection/ProtectionClientInterface.ts index 8f8aad6e3e9..4d5e3f8195d 100644 --- a/packages/services/src/Domain/Protection/ProtectionClientInterface.ts +++ b/packages/services/src/Domain/Protection/ProtectionClientInterface.ts @@ -1,6 +1,6 @@ import { ApplicationServiceInterface } from './../Service/ApplicationServiceInterface' import { DecryptedItem, DecryptedItemInterface, FileItem, SNNote } from '@standardnotes/models' -import { ChallengeInterface, ChallengeReason } from '../Challenge' +import { ChallengeInterface, ChallengeReason, ChallengeResponseInterface } from '../Challenge' import { MobileUnlockTiming } from './MobileUnlockTiming' import { TimingDisplayOption } from './TimingDisplayOption' import { ProtectionEvent } from './ProtectionEvent' @@ -28,6 +28,10 @@ export interface ProtectionsClientInterface extends ApplicationServiceInterface< reason: ChallengeReason, dto: { fallBackToAccountPassword: boolean; requireAccountPassword: boolean; forcePrompt: boolean }, ): Promise + authorizeActionWithChallengeResponse( + reason: ChallengeReason, + dto: { fallBackToAccountPassword: boolean; requireAccountPassword: boolean; forcePrompt: boolean }, + ): Promise<{ success: boolean; challengeResponse?: ChallengeResponseInterface }> authorizeAddingPasscode(): Promise authorizeRemovingPasscode(): Promise authorizeChangingPasscode(): Promise @@ -37,6 +41,7 @@ export interface ProtectionsClientInterface extends ApplicationServiceInterface< authorizeSearchingProtectedNotesText(): Promise authorizeBackupCreation(): Promise authorizeMfaDisable(): Promise + authorizeAccountDeletion(): Promise<{ success: boolean; challengeResponse?: ChallengeResponseInterface }> protectItems(items: I[]): Promise unprotectItems(items: I[], reason: ChallengeReason): Promise diff --git a/packages/services/src/Domain/User/UserService.ts b/packages/services/src/Domain/User/UserService.ts index 1eb8b4b96d0..8c3d7262578 100644 --- a/packages/services/src/Domain/User/UserService.ts +++ b/packages/services/src/Domain/User/UserService.ts @@ -237,13 +237,9 @@ export class UserService error: boolean message?: string }> { - if ( - !(await this.protections.authorizeAction(ChallengeReason.DeleteAccount, { - fallBackToAccountPassword: true, - requireAccountPassword: true, - forcePrompt: false, - })) - ) { + const { success, challengeResponse } = await this.protections.authorizeAccountDeletion() + + if (!success) { return { error: true, message: Messages.INVALID_PASSWORD, @@ -251,7 +247,13 @@ export class UserService } const uuid = this.sessions.getSureUser().uuid - const response = await this.userApi.deleteAccount(uuid) + const password = challengeResponse?.getValueForType(ChallengeValidation.AccountPassword).value as string + const currentRootKey = await this.encryption.computeRootKey( + password, + this.encryption.getRootKeyParams() as SNRootKeyParams, + ) + const serverPassword = currentRootKey.serverPassword + const response = await this.userApi.deleteAccount({ userUuid: uuid, serverPassword: serverPassword }) if (isErrorResponse(response)) { return { error: true, diff --git a/packages/snjs/lib/Services/Protection/ProtectionService.ts b/packages/snjs/lib/Services/Protection/ProtectionService.ts index 0f90920b13a..366ffc73fa5 100644 --- a/packages/snjs/lib/Services/Protection/ProtectionService.ts +++ b/packages/snjs/lib/Services/Protection/ProtectionService.ts @@ -34,6 +34,7 @@ import { import { ContentType } from '@standardnotes/domain-core' import { isValidProtectionSessionLength } from './isValidProtectionSessionLength' import { UnprotectedAccessSecondsDuration } from './UnprotectedAccessSecondsDuration' +import { ChallengeResponse } from '../Challenge' /** * Enforces certain actions to require extra authentication, @@ -278,6 +279,14 @@ export class ProtectionService }) } + async authorizeAccountDeletion(): Promise<{ success: boolean; challengeResponse?: ChallengeResponse }> { + return this.authorizeActionWithChallengeResponse(ChallengeReason.DeleteAccount, { + fallBackToAccountPassword: true, + requireAccountPassword: true, + forcePrompt: true, + }) + } + async authorizeAction( reason: ChallengeReason, dto: { fallBackToAccountPassword: boolean; requireAccountPassword: boolean; forcePrompt: boolean }, @@ -285,6 +294,13 @@ export class ProtectionService return this.validateOrRenewSession(reason, dto) } + async authorizeActionWithChallengeResponse( + reason: ChallengeReason, + dto: { fallBackToAccountPassword: boolean; requireAccountPassword: boolean; forcePrompt: boolean }, + ): Promise<{ success: boolean; challengeResponse?: ChallengeResponse }> { + return this.validateOrRenewSessionWithChallengeResponse(reason, dto) + } + getMobilePasscodeTimingOptions(): TimingDisplayOption[] { return [ { @@ -353,8 +369,16 @@ export class ProtectionService reason: ChallengeReason, { fallBackToAccountPassword = true, requireAccountPassword = false, forcePrompt = false } = {}, ): Promise { + const response = await this.validateOrRenewSessionWithChallengeResponse(reason, { fallBackToAccountPassword, requireAccountPassword, forcePrompt }) + return response.success + } + + private async validateOrRenewSessionWithChallengeResponse( + reason: ChallengeReason, + { fallBackToAccountPassword = true, requireAccountPassword = false, forcePrompt = false } = {}, + ): Promise<{ success: boolean; challengeResponse?: ChallengeResponse }> { if (this.getSessionExpiryDate() > new Date() && !forcePrompt) { - return true + return { success: true } } const prompts: ChallengePrompt[] = [] @@ -378,9 +402,10 @@ export class ProtectionService if (fallBackToAccountPassword && this.encryption.hasAccount()) { prompts.push(new ChallengePrompt(ChallengeValidation.AccountPassword)) } else { - return true + return { success: true } } } + const lastSessionLength = this.getLastSessionLength() const chosenSessionLength = isValidProtectionSessionLength(lastSessionLength) ? lastSessionLength @@ -407,9 +432,9 @@ export class ProtectionService } else { this.setSessionLength(length as UnprotectedAccessSecondsDuration) } - return true + return { success: true, challengeResponse: response } } else { - return false + return { success: false } } } From 2637c9d86c4e4440a7e4cb8157b81edf32c5949c Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Wed, 23 Jul 2025 11:48:50 -0300 Subject: [PATCH 2/8] chore: send server password param to disable mfa endpoint --- .../Protection/ProtectionClientInterface.ts | 2 +- .../Application/Dependencies/Dependencies.ts | 1 + packages/snjs/lib/Services/Api/ApiService.ts | 5 ++++- packages/snjs/lib/Services/Mfa/MfaService.ts | 17 +++++++++++++++-- .../Services/Protection/ProtectionService.ts | 4 ++-- .../lib/Services/Settings/SNSettingsService.ts | 4 ++-- .../lib/Services/Settings/SettingsGateway.ts | 4 ++-- .../Settings/SettingsServerInterface.ts | 2 +- 8 files changed, 28 insertions(+), 11 deletions(-) diff --git a/packages/services/src/Domain/Protection/ProtectionClientInterface.ts b/packages/services/src/Domain/Protection/ProtectionClientInterface.ts index 4d5e3f8195d..3437938ca20 100644 --- a/packages/services/src/Domain/Protection/ProtectionClientInterface.ts +++ b/packages/services/src/Domain/Protection/ProtectionClientInterface.ts @@ -40,7 +40,7 @@ export interface ProtectionsClientInterface extends ApplicationServiceInterface< authorizeAutolockIntervalChange(): Promise authorizeSearchingProtectedNotesText(): Promise authorizeBackupCreation(): Promise - authorizeMfaDisable(): Promise + authorizeMfaDisable(): Promise<{ success: boolean; challengeResponse?: ChallengeResponseInterface }> authorizeAccountDeletion(): Promise<{ success: boolean; challengeResponse?: ChallengeResponseInterface }> protectItems(items: I[]): Promise diff --git a/packages/snjs/lib/Application/Dependencies/Dependencies.ts b/packages/snjs/lib/Application/Dependencies/Dependencies.ts index c65c204c546..c4efce00c73 100644 --- a/packages/snjs/lib/Application/Dependencies/Dependencies.ts +++ b/packages/snjs/lib/Application/Dependencies/Dependencies.ts @@ -1231,6 +1231,7 @@ export class Dependencies { this.get(TYPES.Crypto), this.get(TYPES.FeaturesService), this.get(TYPES.ProtectionService), + this.get(TYPES.EncryptionService), this.get(TYPES.InternalEventBus), ) }) diff --git a/packages/snjs/lib/Services/Api/ApiService.ts b/packages/snjs/lib/Services/Api/ApiService.ts index 18d4533ced8..9b1424f3acc 100644 --- a/packages/snjs/lib/Services/Api/ApiService.ts +++ b/packages/snjs/lib/Services/Api/ApiService.ts @@ -616,12 +616,15 @@ export class LegacyApiService }) } - async deleteSetting(userUuid: UuidString, settingName: string): Promise> { + async deleteSetting(userUuid: UuidString, settingName: string, serverPassword?: string): Promise> { return this.tokenRefreshableRequest({ verb: HttpVerb.Delete, url: joinPaths(this.host, Paths.v1.setting(userUuid, settingName)), authentication: this.getSessionAccessToken(), fallbackErrorMessage: API_MESSAGE_FAILED_UPDATE_SETTINGS, + params: { + serverPassword, + }, }) } diff --git a/packages/snjs/lib/Services/Mfa/MfaService.ts b/packages/snjs/lib/Services/Mfa/MfaService.ts index 495af062f7f..89ad998d467 100644 --- a/packages/snjs/lib/Services/Mfa/MfaService.ts +++ b/packages/snjs/lib/Services/Mfa/MfaService.ts @@ -6,9 +6,12 @@ import { InternalEventBusInterface, MfaServiceInterface, ProtectionsClientInterface, + EncryptionService, SignInStrings, + ChallengeValidation, } from '@standardnotes/services' import { SettingName } from '@standardnotes/domain-core' +import { SNRootKeyParams } from '@standardnotes/encryption' export class MfaService extends AbstractService implements MfaServiceInterface { constructor( @@ -16,6 +19,7 @@ export class MfaService extends AbstractService implements MfaServiceInterface { private crypto: PureCryptoInterface, private featuresService: FeaturesService, private protections: ProtectionsClientInterface, + private encryption: EncryptionService, protected override internalEventBus: InternalEventBusInterface, ) { super(internalEventBus) @@ -55,11 +59,20 @@ export class MfaService extends AbstractService implements MfaServiceInterface { } async disableMfa(): Promise { - if (!(await this.protections.authorizeMfaDisable())) { + const { success, challengeResponse } = await this.protections.authorizeMfaDisable() + + if (!success) { return } - return await this.settingsService.deleteSetting(SettingName.create(SettingName.NAMES.MfaSecret).getValue()) + const password = challengeResponse?.getValueForType(ChallengeValidation.AccountPassword).value as string + const currentRootKey = await this.encryption.computeRootKey( + password, + this.encryption.getRootKeyParams() as SNRootKeyParams, + ) + const serverPassword = currentRootKey.serverPassword + + return await this.settingsService.deleteSetting(SettingName.create(SettingName.NAMES.MfaSecret).getValue(), serverPassword) } override deinit(): void { diff --git a/packages/snjs/lib/Services/Protection/ProtectionService.ts b/packages/snjs/lib/Services/Protection/ProtectionService.ts index 366ffc73fa5..6fbc3a0f782 100644 --- a/packages/snjs/lib/Services/Protection/ProtectionService.ts +++ b/packages/snjs/lib/Services/Protection/ProtectionService.ts @@ -247,8 +247,8 @@ export class ProtectionService }) } - async authorizeMfaDisable(): Promise { - return this.authorizeAction(ChallengeReason.DisableMfa, { + async authorizeMfaDisable(): Promise<{ success: boolean; challengeResponse?: ChallengeResponse }> { + return this.authorizeActionWithChallengeResponse(ChallengeReason.DisableMfa, { fallBackToAccountPassword: true, requireAccountPassword: true, forcePrompt: false, diff --git a/packages/snjs/lib/Services/Settings/SNSettingsService.ts b/packages/snjs/lib/Services/Settings/SNSettingsService.ts index 47113914dc5..aac1a380726 100644 --- a/packages/snjs/lib/Services/Settings/SNSettingsService.ts +++ b/packages/snjs/lib/Services/Settings/SNSettingsService.ts @@ -50,8 +50,8 @@ export class SettingsService extends AbstractService implements SettingsClientIn return this.provider.getDoesSensitiveSettingExist(name) } - async deleteSetting(name: SettingName) { - return this.provider.deleteSetting(name) + async deleteSetting(name: SettingName, serverPassword?: string) { + return this.provider.deleteSetting(name, serverPassword) } getEmailBackupFrequencyOptionLabel(frequency: EmailBackupFrequency): string { diff --git a/packages/snjs/lib/Services/Settings/SettingsGateway.ts b/packages/snjs/lib/Services/Settings/SettingsGateway.ts index 4cdb7930b78..463bcfb1039 100644 --- a/packages/snjs/lib/Services/Settings/SettingsGateway.ts +++ b/packages/snjs/lib/Services/Settings/SettingsGateway.ts @@ -109,8 +109,8 @@ export class SettingsGateway { } } - async deleteSetting(name: SettingName): Promise { - const response = await this.settingsApi.deleteSetting(this.userUuid, name.value) + async deleteSetting(name: SettingName, serverPassword?: string): Promise { + const response = await this.settingsApi.deleteSetting(this.userUuid, name.value, serverPassword) if (isErrorResponse(response)) { throw new Error(getErrorFromErrorResponse(response).message) } diff --git a/packages/snjs/lib/Services/Settings/SettingsServerInterface.ts b/packages/snjs/lib/Services/Settings/SettingsServerInterface.ts index db3a253dfb0..532fd024190 100644 --- a/packages/snjs/lib/Services/Settings/SettingsServerInterface.ts +++ b/packages/snjs/lib/Services/Settings/SettingsServerInterface.ts @@ -28,5 +28,5 @@ export interface SettingsServerInterface { sensitive: boolean, ): Promise> - deleteSetting(userUuid: UuidString, settingName: string): Promise> + deleteSetting(userUuid: UuidString, settingName: string, serverPassword?: string): Promise> } From 922a4f19a938ca9c2250a2879834edc3eaac383e Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Thu, 24 Jul 2025 21:41:02 -0300 Subject: [PATCH 3/8] chore: modify tests --- packages/snjs/mocha/auth.test.js | 48 ++++++++++++++++++- packages/snjs/mocha/mfa_service.test.js | 61 +++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/packages/snjs/mocha/auth.test.js b/packages/snjs/mocha/auth.test.js index dc3f7e141a6..9c8e843524f 100644 --- a/packages/snjs/mocha/auth.test.js +++ b/packages/snjs/mocha/auth.test.js @@ -549,6 +549,24 @@ describe('basic auth', function () { expect(sendChallengeSpy.callCount).to.equal(1) }).timeout(Factory.TenSecondTimeout) + it('should send server password when deleting account', async function () { + Factory.handlePasswordChallenges(context.application, context.password) + + const userApiService = context.application.dependencies.get(TYPES.UserApiService) + const deleteAccountSpy = sinon.spy(userApiService, 'deleteAccount') + + await context.application.user.deleteAccount() + + expect(deleteAccountSpy.callCount).to.equal(1) + const deleteAccountCall = deleteAccountSpy.getCall(0) + const callArgs = deleteAccountCall.args[0] + + expect(callArgs).to.have.property('serverPassword') + expect(callArgs.serverPassword).to.not.be.undefined + expect(typeof callArgs.serverPassword).to.equal('string') + expect(callArgs.serverPassword.length).to.be.above(0) + }).timeout(Factory.TenSecondTimeout) + it('deleting account should sign out current user', async function () { Factory.handlePasswordChallenges(context.application, context.password) @@ -567,12 +585,40 @@ describe('basic auth', function () { const response = await context.application.dependencies .get(TYPES.UserApiService) - .deleteAccount(registerResponse.user.uuid) + .deleteAccount({ + userUuid: registerResponse.user.uuid, + serverPassword: 'dummy-password' + }) expect(response.status).to.equal(401) expect(response.data.error.message).to.equal('Operation not allowed.') await secondContext.deinit() }) + + it('should not allow deleting account if server password is not sent', async function () { + Factory.handlePasswordChallenges(context.application, context.password) + + const response = await context.application.dependencies + .get(TYPES.UserApiService) + .deleteAccount({ + userUuid: context.application.user.uuid, + }) + + expect(response.status).to.equal(400) + }).timeout(Factory.TenSecondTimeout) + + it('should not allow deleting account if server password is incorrect', async function () { + Factory.handlePasswordChallenges(context.application, context.password) + + const response = await context.application.dependencies + .get(TYPES.UserApiService) + .deleteAccount({ + userUuid: context.application.user.uuid, + serverPassword: 'wrong-password' + }) + + expect(response.status).to.equal(400) + }).timeout(Factory.TenSecondTimeout) }) }) diff --git a/packages/snjs/mocha/mfa_service.test.js b/packages/snjs/mocha/mfa_service.test.js index 0940388a57b..f09ac1f0576 100644 --- a/packages/snjs/mocha/mfa_service.test.js +++ b/packages/snjs/mocha/mfa_service.test.js @@ -65,6 +65,7 @@ describe('mfa service', () => { const token = await application.mfa.getOtpToken(secret) sinon.spy(application.challenges, 'sendChallenge') + await application.mfa.enableMfa(secret, token) await application.mfa.disableMfa() @@ -73,4 +74,64 @@ describe('mfa service', () => { expect(challenge.prompts).to.have.lengthOf(2) expect(challenge.prompts[0].validation).to.equal(ChallengeValidation.AccountPassword) }).timeout(Factory.TenSecondTimeout) + + it('sends server password when disabling mfa', async () => { + await registerApp(application) + + Factory.handlePasswordChallenges(application, accountPassword) + const secret = await application.mfa.generateMfaSecret() + const token = await application.mfa.getOtpToken(secret) + + await application.mfa.enableMfa(secret, token) + + sinon.spy(application.settings.settingsApi, 'deleteSetting') + + await application.mfa.disableMfa() + + const deleteSettingCall = application.settings.settingsApi.deleteSetting.getCall(0) + const [serverPassword] = deleteSettingCall.args + expect(typeof serverPassword).to.equal('string') + expect(serverPassword.length).to.be.above(0) + }).timeout(Factory.TenSecondTimeout) + + it('should not allow disabling mfa if server password is not sent', async function () { + await registerApp(application) + + Factory.handlePasswordChallenges(application, accountPassword) + + const secret = await application.mfa.generateMfaSecret() + const token = await application.mfa.getOtpToken(secret) + + await application.mfa.enableMfa(secret, token) + + const response = await application.dependencies + .get(TYPES.SettingsApiService) + .deleteSetting({ + userUuid: application.user.uuid, + settingName: 'MFA_SECRET', + }) + + expect(response.status).to.equal(400) + }).timeout(Factory.TenSecondTimeout) + + it('should not allow disabling mfa if server password is incorrect', async function () { + await registerApp(application) + + Factory.handlePasswordChallenges(application, accountPassword) + + const secret = await application.mfa.generateMfaSecret() + const token = await application.mfa.getOtpToken(secret) + + await application.mfa.enableMfa(secret, token) + + const response = await application.dependencies + .get(TYPES.SettingsApiService) + .deleteSetting({ + userUuid: application.user.uuid, + settingName: 'MFA_SECRET', + serverPassword: 'wrong-password' + }) + + expect(response.status).to.equal(400) + }).timeout(Factory.TenSecondTimeout) }) From 629986017c3e5504838a8ca4549892445322225c Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Thu, 24 Jul 2025 23:27:27 -0300 Subject: [PATCH 4/8] chore: force challenge prompt for mfa disable --- packages/snjs/lib/Services/Protection/ProtectionService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/snjs/lib/Services/Protection/ProtectionService.ts b/packages/snjs/lib/Services/Protection/ProtectionService.ts index 6fbc3a0f782..2422c258239 100644 --- a/packages/snjs/lib/Services/Protection/ProtectionService.ts +++ b/packages/snjs/lib/Services/Protection/ProtectionService.ts @@ -251,7 +251,7 @@ export class ProtectionService return this.authorizeActionWithChallengeResponse(ChallengeReason.DisableMfa, { fallBackToAccountPassword: true, requireAccountPassword: true, - forcePrompt: false, + forcePrompt: true, }) } From bcce25d7cb3dab442bc8627873976a93c767121f Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Mon, 28 Jul 2025 11:08:06 -0300 Subject: [PATCH 5/8] chore: fix eslint errors --- packages/snjs/lib/Services/Api/ApiService.ts | 6 +++++- packages/snjs/lib/Services/Mfa/MfaService.ts | 7 +++++-- .../snjs/lib/Services/Protection/ProtectionService.ts | 8 ++++++-- .../snjs/lib/Services/Settings/SettingsServerInterface.ts | 6 +++++- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/snjs/lib/Services/Api/ApiService.ts b/packages/snjs/lib/Services/Api/ApiService.ts index 9b1424f3acc..bd65972b250 100644 --- a/packages/snjs/lib/Services/Api/ApiService.ts +++ b/packages/snjs/lib/Services/Api/ApiService.ts @@ -616,7 +616,11 @@ export class LegacyApiService }) } - async deleteSetting(userUuid: UuidString, settingName: string, serverPassword?: string): Promise> { + async deleteSetting( + userUuid: UuidString, + settingName: string, + serverPassword?: string, + ): Promise> { return this.tokenRefreshableRequest({ verb: HttpVerb.Delete, url: joinPaths(this.host, Paths.v1.setting(userUuid, settingName)), diff --git a/packages/snjs/lib/Services/Mfa/MfaService.ts b/packages/snjs/lib/Services/Mfa/MfaService.ts index 89ad998d467..5929aa60c13 100644 --- a/packages/snjs/lib/Services/Mfa/MfaService.ts +++ b/packages/snjs/lib/Services/Mfa/MfaService.ts @@ -60,7 +60,7 @@ export class MfaService extends AbstractService implements MfaServiceInterface { async disableMfa(): Promise { const { success, challengeResponse } = await this.protections.authorizeMfaDisable() - + if (!success) { return } @@ -72,7 +72,10 @@ export class MfaService extends AbstractService implements MfaServiceInterface { ) const serverPassword = currentRootKey.serverPassword - return await this.settingsService.deleteSetting(SettingName.create(SettingName.NAMES.MfaSecret).getValue(), serverPassword) + return await this.settingsService.deleteSetting( + SettingName.create(SettingName.NAMES.MfaSecret).getValue(), + serverPassword, + ) } override deinit(): void { diff --git a/packages/snjs/lib/Services/Protection/ProtectionService.ts b/packages/snjs/lib/Services/Protection/ProtectionService.ts index 2422c258239..de827eb2ffe 100644 --- a/packages/snjs/lib/Services/Protection/ProtectionService.ts +++ b/packages/snjs/lib/Services/Protection/ProtectionService.ts @@ -369,7 +369,11 @@ export class ProtectionService reason: ChallengeReason, { fallBackToAccountPassword = true, requireAccountPassword = false, forcePrompt = false } = {}, ): Promise { - const response = await this.validateOrRenewSessionWithChallengeResponse(reason, { fallBackToAccountPassword, requireAccountPassword, forcePrompt }) + const response = await this.validateOrRenewSessionWithChallengeResponse(reason, { + fallBackToAccountPassword, + requireAccountPassword, + forcePrompt, + }) return response.success } @@ -405,7 +409,7 @@ export class ProtectionService return { success: true } } } - + const lastSessionLength = this.getLastSessionLength() const chosenSessionLength = isValidProtectionSessionLength(lastSessionLength) ? lastSessionLength diff --git a/packages/snjs/lib/Services/Settings/SettingsServerInterface.ts b/packages/snjs/lib/Services/Settings/SettingsServerInterface.ts index 532fd024190..887f5095c0d 100644 --- a/packages/snjs/lib/Services/Settings/SettingsServerInterface.ts +++ b/packages/snjs/lib/Services/Settings/SettingsServerInterface.ts @@ -28,5 +28,9 @@ export interface SettingsServerInterface { sensitive: boolean, ): Promise> - deleteSetting(userUuid: UuidString, settingName: string, serverPassword?: string): Promise> + deleteSetting( + userUuid: UuidString, + settingName: string, + serverPassword?: string, + ): Promise> } From 7ab89d4b8d0a0c707968aa5dd36070c5de99a3eb Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Fri, 1 Aug 2025 11:11:04 -0300 Subject: [PATCH 6/8] chore: add server passsword to get recovery codes --- .../src/Domain/Client/Auth/AuthApiService.ts | 6 +++-- .../Client/Auth/AuthApiServiceInterface.ts | 2 +- .../GenerateRecoveryCodesRequestParams.ts | 3 +++ packages/api/src/Domain/Request/index.ts | 1 + .../api/src/Domain/Server/Auth/AuthServer.ts | 12 +++++++--- .../Domain/Server/Auth/AuthServerInterface.ts | 10 ++++++-- .../src/Domain/Auth/AuthClientInterface.ts | 2 +- .../services/src/Domain/Auth/AuthManager.ts | 4 ++-- .../Application/Dependencies/Dependencies.ts | 1 + .../GetRecoveryCodes/GetRecoveryCodes.spec.ts | 14 +++++++---- .../GetRecoveryCodes/GetRecoveryCodes.ts | 23 ++++++++++++++++--- .../GetRecoveryCodes/GetRecoveryCodesDTO.ts | 3 +++ packages/snjs/lib/Services/Api/ApiService.ts | 9 +++++++- .../Services/Settings/SNSettingsService.ts | 4 ++-- .../Settings/SettingsClientInterface.ts | 4 ++-- .../lib/Services/Settings/SettingsGateway.ts | 4 ++-- .../Settings/SettingsServerInterface.ts | 6 ++++- .../RecoveryCodeBanner/RecoveryCodeBanner.tsx | 6 ++--- 18 files changed, 84 insertions(+), 30 deletions(-) create mode 100644 packages/api/src/Domain/Request/Recovery/GenerateRecoveryCodesRequestParams.ts create mode 100644 packages/snjs/lib/Domain/UseCase/GetRecoveryCodes/GetRecoveryCodesDTO.ts diff --git a/packages/api/src/Domain/Client/Auth/AuthApiService.ts b/packages/api/src/Domain/Client/Auth/AuthApiService.ts index 8a01135239c..84f16090855 100644 --- a/packages/api/src/Domain/Client/Auth/AuthApiService.ts +++ b/packages/api/src/Domain/Client/Auth/AuthApiService.ts @@ -22,7 +22,9 @@ export class AuthApiService implements AuthApiServiceInterface { this.operationsInProgress = new Map() } - async generateRecoveryCodes(): Promise> { + async generateRecoveryCodes(dto: { + serverPassword: string + }): Promise> { if (this.operationsInProgress.get(AuthApiOperations.GenerateRecoveryCodes)) { throw new ApiCallError(ErrorMessage.GenericInProgress) } @@ -30,7 +32,7 @@ export class AuthApiService implements AuthApiServiceInterface { this.operationsInProgress.set(AuthApiOperations.GenerateRecoveryCodes, true) try { - const response = await this.authServer.generateRecoveryCodes() + const response = await this.authServer.generateRecoveryCodes({ serverPassword: dto.serverPassword }) return response } catch (error) { diff --git a/packages/api/src/Domain/Client/Auth/AuthApiServiceInterface.ts b/packages/api/src/Domain/Client/Auth/AuthApiServiceInterface.ts index a2bb31cd0ab..804a32717c2 100644 --- a/packages/api/src/Domain/Client/Auth/AuthApiServiceInterface.ts +++ b/packages/api/src/Domain/Client/Auth/AuthApiServiceInterface.ts @@ -6,7 +6,7 @@ import { } from '../../Response' export interface AuthApiServiceInterface { - generateRecoveryCodes(): Promise> + generateRecoveryCodes(dto: { serverPassword: string }): Promise> recoveryKeyParams(dto: { username: string codeChallenge: string diff --git a/packages/api/src/Domain/Request/Recovery/GenerateRecoveryCodesRequestParams.ts b/packages/api/src/Domain/Request/Recovery/GenerateRecoveryCodesRequestParams.ts new file mode 100644 index 00000000000..9642097cd65 --- /dev/null +++ b/packages/api/src/Domain/Request/Recovery/GenerateRecoveryCodesRequestParams.ts @@ -0,0 +1,3 @@ +export interface GenerateRecoveryCodesRequestParams { + serverPassword: string +} diff --git a/packages/api/src/Domain/Request/index.ts b/packages/api/src/Domain/Request/index.ts index dbad01c5e70..e74c1a39e76 100644 --- a/packages/api/src/Domain/Request/index.ts +++ b/packages/api/src/Domain/Request/index.ts @@ -2,6 +2,7 @@ export * from './Authenticator/DeleteAuthenticatorRequestParams' export * from './Authenticator/GenerateAuthenticatorAuthenticationOptionsRequestParams' export * from './Authenticator/ListAuthenticatorsRequestParams' export * from './Authenticator/VerifyAuthenticatorRegistrationResponseRequestParams' +export * from './Recovery/GenerateRecoveryCodesRequestParams' export * from './Recovery/RecoveryKeyParamsRequestParams' export * from './Recovery/SignInWithRecoveryCodesRequestParams' export * from './Revision/DeleteRevisionRequestParams' diff --git a/packages/api/src/Domain/Server/Auth/AuthServer.ts b/packages/api/src/Domain/Server/Auth/AuthServer.ts index e53e83c49f4..40c20973160 100644 --- a/packages/api/src/Domain/Server/Auth/AuthServer.ts +++ b/packages/api/src/Domain/Server/Auth/AuthServer.ts @@ -1,5 +1,9 @@ import { HttpServiceInterface } from '../../Http/HttpServiceInterface' -import { RecoveryKeyParamsRequestParams, SignInWithRecoveryCodesRequestParams } from '../../Request' +import { + GenerateRecoveryCodesRequestParams, + RecoveryKeyParamsRequestParams, + SignInWithRecoveryCodesRequestParams, +} from '../../Request' import { HttpResponse } from '@standardnotes/responses' import { GenerateRecoveryCodesResponseBody, @@ -12,8 +16,10 @@ import { Paths } from './Paths' export class AuthServer implements AuthServerInterface { constructor(private httpService: HttpServiceInterface) {} - async generateRecoveryCodes(): Promise> { - return this.httpService.post(Paths.v1.generateRecoveryCodes) + async generateRecoveryCodes( + params: GenerateRecoveryCodesRequestParams, + ): Promise> { + return this.httpService.post(Paths.v1.generateRecoveryCodes, params) } async recoveryKeyParams( diff --git a/packages/api/src/Domain/Server/Auth/AuthServerInterface.ts b/packages/api/src/Domain/Server/Auth/AuthServerInterface.ts index e0d38a8a864..336135f4d91 100644 --- a/packages/api/src/Domain/Server/Auth/AuthServerInterface.ts +++ b/packages/api/src/Domain/Server/Auth/AuthServerInterface.ts @@ -1,5 +1,9 @@ import { HttpResponse } from '@standardnotes/responses' -import { RecoveryKeyParamsRequestParams, SignInWithRecoveryCodesRequestParams } from '../../Request' +import { + GenerateRecoveryCodesRequestParams, + RecoveryKeyParamsRequestParams, + SignInWithRecoveryCodesRequestParams, +} from '../../Request' import { GenerateRecoveryCodesResponseBody, RecoveryKeyParamsResponseBody, @@ -7,7 +11,9 @@ import { } from '../../Response' export interface AuthServerInterface { - generateRecoveryCodes(): Promise> + generateRecoveryCodes( + params: GenerateRecoveryCodesRequestParams, + ): Promise> recoveryKeyParams(params: RecoveryKeyParamsRequestParams): Promise> signInWithRecoveryCodes( params: SignInWithRecoveryCodesRequestParams, diff --git a/packages/services/src/Domain/Auth/AuthClientInterface.ts b/packages/services/src/Domain/Auth/AuthClientInterface.ts index e471442e8ef..a3ed7a81858 100644 --- a/packages/services/src/Domain/Auth/AuthClientInterface.ts +++ b/packages/services/src/Domain/Auth/AuthClientInterface.ts @@ -2,7 +2,7 @@ import { AnyKeyParamsContent } from '@standardnotes/common' import { SessionBody } from '@standardnotes/responses' export interface AuthClientInterface { - generateRecoveryCodes(): Promise + generateRecoveryCodes(dto: { serverPassword: string }): Promise recoveryKeyParams(dto: { username: string codeChallenge: string diff --git a/packages/services/src/Domain/Auth/AuthManager.ts b/packages/services/src/Domain/Auth/AuthManager.ts index 5dcb48bc5cf..1d3784f15a0 100644 --- a/packages/services/src/Domain/Auth/AuthManager.ts +++ b/packages/services/src/Domain/Auth/AuthManager.ts @@ -14,9 +14,9 @@ export class AuthManager extends AbstractService implements AuthClientInterface super(internalEventBus) } - async generateRecoveryCodes(): Promise { + async generateRecoveryCodes(dto: { serverPassword: string }): Promise { try { - const result = await this.authApiService.generateRecoveryCodes() + const result = await this.authApiService.generateRecoveryCodes(dto) if (isErrorResponse(result)) { return false diff --git a/packages/snjs/lib/Application/Dependencies/Dependencies.ts b/packages/snjs/lib/Application/Dependencies/Dependencies.ts index c4efce00c73..69197f564e6 100644 --- a/packages/snjs/lib/Application/Dependencies/Dependencies.ts +++ b/packages/snjs/lib/Application/Dependencies/Dependencies.ts @@ -1026,6 +1026,7 @@ export class Dependencies { return new GetRecoveryCodes( this.get(TYPES.AuthManager), this.get(TYPES.SettingsService), + this.get(TYPES.EncryptionService), ) }) diff --git a/packages/snjs/lib/Domain/UseCase/GetRecoveryCodes/GetRecoveryCodes.spec.ts b/packages/snjs/lib/Domain/UseCase/GetRecoveryCodes/GetRecoveryCodes.spec.ts index 8a9b4bbae9b..5b0fa80c9cc 100644 --- a/packages/snjs/lib/Domain/UseCase/GetRecoveryCodes/GetRecoveryCodes.spec.ts +++ b/packages/snjs/lib/Domain/UseCase/GetRecoveryCodes/GetRecoveryCodes.spec.ts @@ -1,4 +1,4 @@ -import { AuthClientInterface } from '@standardnotes/services' +import { AuthClientInterface, EncryptionService } from '@standardnotes/services' import { SettingsClientInterface } from '@Lib/Services/Settings/SettingsClientInterface' import { GetRecoveryCodes } from './GetRecoveryCodes' @@ -6,8 +6,9 @@ import { GetRecoveryCodes } from './GetRecoveryCodes' describe('GetRecoveryCodes', () => { let authClient: AuthClientInterface let settingsClient: SettingsClientInterface + let encryption: EncryptionService - const createUseCase = () => new GetRecoveryCodes(authClient, settingsClient) + const createUseCase = () => new GetRecoveryCodes(authClient, settingsClient, encryption) beforeEach(() => { authClient = {} as jest.Mocked @@ -15,12 +16,15 @@ describe('GetRecoveryCodes', () => { settingsClient = {} as jest.Mocked settingsClient.getSetting = jest.fn().mockResolvedValue('existing-recovery-codes') + + encryption = {} as jest.Mocked + encryption.computeRootKey = jest.fn().mockResolvedValue({ serverPassword: 'test-server-password' }) }) it('should return existing recovery code if they exist', async () => { const useCase = createUseCase() - const result = await useCase.execute() + const result = await useCase.execute({ password: 'test-password' }) expect(result.getValue()).toBe('existing-recovery-codes') }) @@ -30,7 +34,7 @@ describe('GetRecoveryCodes', () => { const useCase = createUseCase() - const result = await useCase.execute() + const result = await useCase.execute({ password: 'test-password' }) expect(result.getValue()).toBe('recovery-codes') }) @@ -41,7 +45,7 @@ describe('GetRecoveryCodes', () => { const useCase = createUseCase() - const result = await useCase.execute() + const result = await useCase.execute({ password: 'test-password' }) expect(result.isFailed()).toBe(true) }) diff --git a/packages/snjs/lib/Domain/UseCase/GetRecoveryCodes/GetRecoveryCodes.ts b/packages/snjs/lib/Domain/UseCase/GetRecoveryCodes/GetRecoveryCodes.ts index 94a9257f27f..ea167ba58eb 100644 --- a/packages/snjs/lib/Domain/UseCase/GetRecoveryCodes/GetRecoveryCodes.ts +++ b/packages/snjs/lib/Domain/UseCase/GetRecoveryCodes/GetRecoveryCodes.ts @@ -1,23 +1,40 @@ -import { AuthClientInterface } from '@standardnotes/services' +import { AuthClientInterface, EncryptionService } from '@standardnotes/services' import { Result, SettingName, UseCaseInterface } from '@standardnotes/domain-core' import { SettingsClientInterface } from '@Lib/Services/Settings/SettingsClientInterface' +import { GetRecoveryCodesDTO } from './GetRecoveryCodesDTO' +import { SNRootKeyParams } from '@standardnotes/encryption' export class GetRecoveryCodes implements UseCaseInterface { constructor( private authClient: AuthClientInterface, private settingsClient: SettingsClientInterface, + private encryption: EncryptionService, ) {} - async execute(): Promise> { + async execute(dto: GetRecoveryCodesDTO): Promise> { + if (!dto.password) { + return Result.fail('Password is required to get recovery code.') + } + const currentRootKey = await this.encryption.computeRootKey( + dto.password, + this.encryption.getRootKeyParams() as SNRootKeyParams, + ) + const serverPassword = currentRootKey.serverPassword + + if (!serverPassword) { + return Result.fail('Could not compute server password') + } + const existingRecoveryCodes = await this.settingsClient.getSetting( SettingName.create(SettingName.NAMES.RecoveryCodes).getValue(), + serverPassword, ) if (existingRecoveryCodes !== undefined) { return Result.ok(existingRecoveryCodes) } - const generatedRecoveryCodes = await this.authClient.generateRecoveryCodes() + const generatedRecoveryCodes = await this.authClient.generateRecoveryCodes({ serverPassword }) if (generatedRecoveryCodes === false) { return Result.fail('Could not generate recovery code') } diff --git a/packages/snjs/lib/Domain/UseCase/GetRecoveryCodes/GetRecoveryCodesDTO.ts b/packages/snjs/lib/Domain/UseCase/GetRecoveryCodes/GetRecoveryCodesDTO.ts new file mode 100644 index 00000000000..0f260fb19f8 --- /dev/null +++ b/packages/snjs/lib/Domain/UseCase/GetRecoveryCodes/GetRecoveryCodesDTO.ts @@ -0,0 +1,3 @@ +export interface GetRecoveryCodesDTO { + password?: string +} diff --git a/packages/snjs/lib/Services/Api/ApiService.ts b/packages/snjs/lib/Services/Api/ApiService.ts index bd65972b250..2678cbe0f44 100644 --- a/packages/snjs/lib/Services/Api/ApiService.ts +++ b/packages/snjs/lib/Services/Api/ApiService.ts @@ -578,12 +578,19 @@ export class LegacyApiService }) } - async getSetting(userUuid: UuidString, settingName: string): Promise> { + async getSetting( + userUuid: UuidString, + settingName: string, + serverPassword?: string, + ): Promise> { return await this.tokenRefreshableRequest({ verb: HttpVerb.Get, url: joinPaths(this.host, Paths.v1.setting(userUuid, settingName.toLowerCase())), authentication: this.getSessionAccessToken(), fallbackErrorMessage: API_MESSAGE_FAILED_GET_SETTINGS, + params: { + serverPassword, + }, }) } diff --git a/packages/snjs/lib/Services/Settings/SNSettingsService.ts b/packages/snjs/lib/Services/Settings/SNSettingsService.ts index aac1a380726..4b403a832be 100644 --- a/packages/snjs/lib/Services/Settings/SNSettingsService.ts +++ b/packages/snjs/lib/Services/Settings/SNSettingsService.ts @@ -30,8 +30,8 @@ export class SettingsService extends AbstractService implements SettingsClientIn return this.provider.listSettings() } - async getSetting(name: SettingName) { - return this.provider.getSetting(name) + async getSetting(name: SettingName, serverPassword?: string) { + return this.provider.getSetting(name, serverPassword) } async getSubscriptionSetting(name: SettingName) { diff --git a/packages/snjs/lib/Services/Settings/SettingsClientInterface.ts b/packages/snjs/lib/Services/Settings/SettingsClientInterface.ts index a9e3613738a..0e1373f366a 100644 --- a/packages/snjs/lib/Services/Settings/SettingsClientInterface.ts +++ b/packages/snjs/lib/Services/Settings/SettingsClientInterface.ts @@ -5,13 +5,13 @@ import { SettingName } from '@standardnotes/domain-core' export interface SettingsClientInterface { listSettings(): Promise - getSetting(name: SettingName): Promise + getSetting(name: SettingName, serverPassword?: string): Promise getDoesSensitiveSettingExist(name: SettingName): Promise updateSetting(name: SettingName, payload: string, sensitive?: boolean): Promise - deleteSetting(name: SettingName): Promise + deleteSetting(name: SettingName, serverPassword?: string): Promise getEmailBackupFrequencyOptionLabel(frequency: EmailBackupFrequency): string } diff --git a/packages/snjs/lib/Services/Settings/SettingsGateway.ts b/packages/snjs/lib/Services/Settings/SettingsGateway.ts index 463bcfb1039..8a7c5c57710 100644 --- a/packages/snjs/lib/Services/Settings/SettingsGateway.ts +++ b/packages/snjs/lib/Services/Settings/SettingsGateway.ts @@ -45,8 +45,8 @@ export class SettingsGateway { return settings } - async getSetting(name: SettingName): Promise { - const response = await this.settingsApi.getSetting(this.userUuid, name.value) + async getSetting(name: SettingName, serverPassword?: string): Promise { + const response = await this.settingsApi.getSetting(this.userUuid, name.value, serverPassword) if (response.status === HttpStatusCode.BadRequest) { return undefined diff --git a/packages/snjs/lib/Services/Settings/SettingsServerInterface.ts b/packages/snjs/lib/Services/Settings/SettingsServerInterface.ts index 887f5095c0d..7e7af662ffe 100644 --- a/packages/snjs/lib/Services/Settings/SettingsServerInterface.ts +++ b/packages/snjs/lib/Services/Settings/SettingsServerInterface.ts @@ -17,7 +17,11 @@ export interface SettingsServerInterface { sensitive: boolean, ): Promise> - getSetting(userUuid: UuidString, settingName: string): Promise> + getSetting( + userUuid: UuidString, + settingName: string, + serverPassword?: string, + ): Promise> getSubscriptionSetting(userUuid: UuidString, settingName: string): Promise> diff --git a/packages/web/src/javascripts/Components/RecoveryCodeBanner/RecoveryCodeBanner.tsx b/packages/web/src/javascripts/Components/RecoveryCodeBanner/RecoveryCodeBanner.tsx index 3c6dd1911c3..64588b82fd8 100644 --- a/packages/web/src/javascripts/Components/RecoveryCodeBanner/RecoveryCodeBanner.tsx +++ b/packages/web/src/javascripts/Components/RecoveryCodeBanner/RecoveryCodeBanner.tsx @@ -10,13 +10,13 @@ const RecoveryCodeBanner = ({ application }: { application: WebApplication }) => const [errorMessage, setErrorMessage] = useState() const onClickShow = async () => { - const authorized = await application.challenges.promptForAccountPassword() + const password = await application.challenges.promptForAccountPassword() - if (!authorized) { + if (!password) { return } - const recoveryCodeOrError = await application.getRecoveryCodes.execute() + const recoveryCodeOrError = await application.getRecoveryCodes.execute({ password }) if (recoveryCodeOrError.isFailed()) { setErrorMessage(recoveryCodeOrError.getError()) return From af925f794904b9c4e9260ee77238632893e92071 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Fri, 1 Aug 2025 11:34:29 -0300 Subject: [PATCH 7/8] chore: fix tests --- .../lib/Domain/UseCase/GetRecoveryCodes/GetRecoveryCodes.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/snjs/lib/Domain/UseCase/GetRecoveryCodes/GetRecoveryCodes.spec.ts b/packages/snjs/lib/Domain/UseCase/GetRecoveryCodes/GetRecoveryCodes.spec.ts index 5b0fa80c9cc..c4940b04111 100644 --- a/packages/snjs/lib/Domain/UseCase/GetRecoveryCodes/GetRecoveryCodes.spec.ts +++ b/packages/snjs/lib/Domain/UseCase/GetRecoveryCodes/GetRecoveryCodes.spec.ts @@ -19,6 +19,7 @@ describe('GetRecoveryCodes', () => { encryption = {} as jest.Mocked encryption.computeRootKey = jest.fn().mockResolvedValue({ serverPassword: 'test-server-password' }) + encryption.getRootKeyParams = jest.fn().mockReturnValue({ algorithm: 'test-algorithm' }) }) it('should return existing recovery code if they exist', async () => { From 16e1cc4b0e826854dcda949f89c8aeb14d283857 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Tue, 19 Aug 2025 15:10:53 -0300 Subject: [PATCH 8/8] chore: pass server password as header --- .../api/src/Domain/Client/Auth/AuthApiService.ts | 4 +++- .../api/src/Domain/Client/User/UserApiService.ts | 10 ++++++---- packages/api/src/Domain/Http/HttpService.ts | 3 +++ .../Request/User/UserDeletionRequestParams.ts | 1 - packages/api/src/Domain/Server/Auth/AuthServer.ts | 13 ++++--------- .../src/Domain/Server/Auth/AuthServerInterface.ts | 11 +++-------- packages/api/src/Domain/Server/User/UserServer.ts | 8 ++++++-- .../src/Domain/Server/User/UserServerInterface.ts | 6 +++++- packages/snjs/lib/Services/Api/ApiService.ts | 10 ++++------ 9 files changed, 34 insertions(+), 32 deletions(-) diff --git a/packages/api/src/Domain/Client/Auth/AuthApiService.ts b/packages/api/src/Domain/Client/Auth/AuthApiService.ts index 84f16090855..00033762ca1 100644 --- a/packages/api/src/Domain/Client/Auth/AuthApiService.ts +++ b/packages/api/src/Domain/Client/Auth/AuthApiService.ts @@ -32,7 +32,9 @@ export class AuthApiService implements AuthApiServiceInterface { this.operationsInProgress.set(AuthApiOperations.GenerateRecoveryCodes, true) try { - const response = await this.authServer.generateRecoveryCodes({ serverPassword: dto.serverPassword }) + const response = await this.authServer.generateRecoveryCodes({ + headers: [{ key: 'x-server-password', value: dto.serverPassword }], + }) return response } catch (error) { diff --git a/packages/api/src/Domain/Client/User/UserApiService.ts b/packages/api/src/Domain/Client/User/UserApiService.ts index a77c52a5322..894fee7a209 100644 --- a/packages/api/src/Domain/Client/User/UserApiService.ts +++ b/packages/api/src/Domain/Client/User/UserApiService.ts @@ -34,10 +34,12 @@ export class UserApiService implements UserApiServiceInterface { this.lockOperation(UserApiOperations.DeletingAccount) try { - const response = await this.userServer.deleteAccount({ - userUuid: dto.userUuid, - serverPassword: dto.serverPassword, - }) + const response = await this.userServer.deleteAccount( + { + userUuid: dto.userUuid, + }, + { headers: [{ key: 'x-server-password', value: dto.serverPassword }] }, + ) this.unlockOperation(UserApiOperations.DeletingAccount) diff --git a/packages/api/src/Domain/Http/HttpService.ts b/packages/api/src/Domain/Http/HttpService.ts index 3d10bdddd29..891e583b34f 100644 --- a/packages/api/src/Domain/Http/HttpService.ts +++ b/packages/api/src/Domain/Http/HttpService.ts @@ -91,6 +91,7 @@ export class HttpService implements HttpServiceInterface { params, verb: HttpVerb.Get, authentication: options?.authentication ?? this.getSessionAccessToken(), + customHeaders: options?.headers, }) } @@ -123,6 +124,7 @@ export class HttpService implements HttpServiceInterface { params, verb: HttpVerb.Put, authentication: options?.authentication ?? this.getSessionAccessToken(), + customHeaders: options?.headers, }) } @@ -141,6 +143,7 @@ export class HttpService implements HttpServiceInterface { params, verb: HttpVerb.Delete, authentication: options?.authentication ?? this.getSessionAccessToken(), + customHeaders: options?.headers, }) } diff --git a/packages/api/src/Domain/Request/User/UserDeletionRequestParams.ts b/packages/api/src/Domain/Request/User/UserDeletionRequestParams.ts index e5b670891af..21b77caca56 100644 --- a/packages/api/src/Domain/Request/User/UserDeletionRequestParams.ts +++ b/packages/api/src/Domain/Request/User/UserDeletionRequestParams.ts @@ -1,5 +1,4 @@ export type UserDeletionRequestParams = { userUuid: string - serverPassword: string [additionalParam: string]: unknown } diff --git a/packages/api/src/Domain/Server/Auth/AuthServer.ts b/packages/api/src/Domain/Server/Auth/AuthServer.ts index 40c20973160..77dff53364f 100644 --- a/packages/api/src/Domain/Server/Auth/AuthServer.ts +++ b/packages/api/src/Domain/Server/Auth/AuthServer.ts @@ -1,9 +1,5 @@ import { HttpServiceInterface } from '../../Http/HttpServiceInterface' -import { - GenerateRecoveryCodesRequestParams, - RecoveryKeyParamsRequestParams, - SignInWithRecoveryCodesRequestParams, -} from '../../Request' +import { RecoveryKeyParamsRequestParams, SignInWithRecoveryCodesRequestParams } from '../../Request' import { HttpResponse } from '@standardnotes/responses' import { GenerateRecoveryCodesResponseBody, @@ -12,14 +8,13 @@ import { } from '../../Response' import { AuthServerInterface } from './AuthServerInterface' import { Paths } from './Paths' +import { HttpRequestOptions } from '../../Http/HttpRequestOptions' export class AuthServer implements AuthServerInterface { constructor(private httpService: HttpServiceInterface) {} - async generateRecoveryCodes( - params: GenerateRecoveryCodesRequestParams, - ): Promise> { - return this.httpService.post(Paths.v1.generateRecoveryCodes, params) + async generateRecoveryCodes(options?: HttpRequestOptions): Promise> { + return this.httpService.post(Paths.v1.generateRecoveryCodes, undefined, options) } async recoveryKeyParams( diff --git a/packages/api/src/Domain/Server/Auth/AuthServerInterface.ts b/packages/api/src/Domain/Server/Auth/AuthServerInterface.ts index 336135f4d91..46fd1950b34 100644 --- a/packages/api/src/Domain/Server/Auth/AuthServerInterface.ts +++ b/packages/api/src/Domain/Server/Auth/AuthServerInterface.ts @@ -1,19 +1,14 @@ import { HttpResponse } from '@standardnotes/responses' -import { - GenerateRecoveryCodesRequestParams, - RecoveryKeyParamsRequestParams, - SignInWithRecoveryCodesRequestParams, -} from '../../Request' +import { RecoveryKeyParamsRequestParams, SignInWithRecoveryCodesRequestParams } from '../../Request' import { GenerateRecoveryCodesResponseBody, RecoveryKeyParamsResponseBody, SignInWithRecoveryCodesResponseBody, } from '../../Response' +import { HttpRequestOptions } from '../../Http/HttpRequestOptions' export interface AuthServerInterface { - generateRecoveryCodes( - params: GenerateRecoveryCodesRequestParams, - ): Promise> + generateRecoveryCodes(options?: HttpRequestOptions): Promise> recoveryKeyParams(params: RecoveryKeyParamsRequestParams): Promise> signInWithRecoveryCodes( params: SignInWithRecoveryCodesRequestParams, diff --git a/packages/api/src/Domain/Server/User/UserServer.ts b/packages/api/src/Domain/Server/User/UserServer.ts index cafe325141e..fed5c21bc9b 100644 --- a/packages/api/src/Domain/Server/User/UserServer.ts +++ b/packages/api/src/Domain/Server/User/UserServer.ts @@ -8,12 +8,16 @@ import { UserRegistrationResponseBody } from '../../Response/User/UserRegistrati import { Paths } from './Paths' import { UserServerInterface } from './UserServerInterface' import { UserUpdateRequestParams } from '../../Request/User/UserUpdateRequestParams' +import { HttpRequestOptions } from '../../Http/HttpRequestOptions' export class UserServer implements UserServerInterface { constructor(private httpService: HttpServiceInterface) {} - async deleteAccount(params: UserDeletionRequestParams): Promise> { - return this.httpService.delete(Paths.v1.deleteAccount(params.userUuid), params) + async deleteAccount( + params: UserDeletionRequestParams, + options?: HttpRequestOptions, + ): Promise> { + return this.httpService.delete(Paths.v1.deleteAccount(params.userUuid), params, options) } async register(params: UserRegistrationRequestParams): Promise> { diff --git a/packages/api/src/Domain/Server/User/UserServerInterface.ts b/packages/api/src/Domain/Server/User/UserServerInterface.ts index 1ce40c19dac..1b2147756e4 100644 --- a/packages/api/src/Domain/Server/User/UserServerInterface.ts +++ b/packages/api/src/Domain/Server/User/UserServerInterface.ts @@ -5,9 +5,13 @@ import { UserDeletionResponseBody } from '../../Response/User/UserDeletionRespon import { UserRegistrationResponseBody } from '../../Response/User/UserRegistrationResponseBody' import { UserUpdateResponse } from '../../Response/User/UserUpdateResponse' import { UserUpdateRequestParams } from '../../Request/User/UserUpdateRequestParams' +import { HttpRequestOptions } from '../../Http/HttpRequestOptions' export interface UserServerInterface { register(params: UserRegistrationRequestParams): Promise> - deleteAccount(params: UserDeletionRequestParams): Promise> + deleteAccount( + params: UserDeletionRequestParams, + options?: HttpRequestOptions, + ): Promise> update(params: UserUpdateRequestParams): Promise> } diff --git a/packages/snjs/lib/Services/Api/ApiService.ts b/packages/snjs/lib/Services/Api/ApiService.ts index 2678cbe0f44..9cccbec7f0e 100644 --- a/packages/snjs/lib/Services/Api/ApiService.ts +++ b/packages/snjs/lib/Services/Api/ApiService.ts @@ -583,14 +583,13 @@ export class LegacyApiService settingName: string, serverPassword?: string, ): Promise> { + const customHeaders = serverPassword ? [{ key: 'x-server-password', value: serverPassword }] : undefined return await this.tokenRefreshableRequest({ verb: HttpVerb.Get, url: joinPaths(this.host, Paths.v1.setting(userUuid, settingName.toLowerCase())), authentication: this.getSessionAccessToken(), fallbackErrorMessage: API_MESSAGE_FAILED_GET_SETTINGS, - params: { - serverPassword, - }, + customHeaders, }) } @@ -628,14 +627,13 @@ export class LegacyApiService settingName: string, serverPassword?: string, ): Promise> { + const customHeaders = serverPassword ? [{ key: 'x-server-password', value: serverPassword }] : undefined return this.tokenRefreshableRequest({ verb: HttpVerb.Delete, url: joinPaths(this.host, Paths.v1.setting(userUuid, settingName)), authentication: this.getSessionAccessToken(), fallbackErrorMessage: API_MESSAGE_FAILED_UPDATE_SETTINGS, - params: { - serverPassword, - }, + customHeaders, }) }