feat: add sending user requests to process (#1908)
* feat: add sending user requests to process * fix(snjs): yarn lock * fix(snjs): imports * fix: specs
This commit is contained in:
@@ -8,6 +8,8 @@ import {
|
||||
UserApiService,
|
||||
UserApiServiceInterface,
|
||||
UserRegistrationResponseBody,
|
||||
UserRequestServer,
|
||||
UserRequestServerInterface,
|
||||
UserServer,
|
||||
UserServerInterface,
|
||||
WebSocketApiService,
|
||||
@@ -52,6 +54,15 @@ import {
|
||||
WorkspaceClientInterface,
|
||||
WorkspaceManager,
|
||||
ChallengePrompt,
|
||||
Challenge,
|
||||
ErrorAlertStrings,
|
||||
SessionsClientInterface,
|
||||
ProtectionsClientInterface,
|
||||
UserService,
|
||||
ProtocolUpgradeStrings,
|
||||
CredentialsChangeFunctionResponse,
|
||||
SessionStrings,
|
||||
AccountEvent,
|
||||
} from '@standardnotes/services'
|
||||
import { FilesClientInterface } from '@standardnotes/files'
|
||||
import { ComputePrivateUsername } from '@standardnotes/encryption'
|
||||
@@ -68,7 +79,7 @@ import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
|
||||
import { SnjsVersion } from './../Version'
|
||||
import { SNLog } from '../Log'
|
||||
import { Challenge, ChallengeResponse, ListedClientInterface } from '../Services'
|
||||
import { ChallengeResponse, ListedClientInterface } from '../Services'
|
||||
import { ApplicationConstructorOptions, FullyResolvedApplicationOptions } from './Options/ApplicationOptions'
|
||||
import { ApplicationOptionsDefaults } from './Options/Defaults'
|
||||
|
||||
@@ -112,6 +123,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
private apiService!: InternalServices.SNApiService
|
||||
private declare userApiService: UserApiServiceInterface
|
||||
private declare userServer: UserServerInterface
|
||||
private declare userRequestServer: UserRequestServerInterface
|
||||
private declare subscriptionApiService: SubscriptionApiServiceInterface
|
||||
private declare subscriptionServer: SubscriptionServerInterface
|
||||
private declare subscriptionManager: SubscriptionClientInterface
|
||||
@@ -132,7 +144,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
private keyRecoveryService!: InternalServices.SNKeyRecoveryService
|
||||
private preferencesService!: InternalServices.SNPreferencesService
|
||||
private featuresService!: InternalServices.SNFeaturesService
|
||||
private userService!: InternalServices.UserService
|
||||
private userService!: UserService
|
||||
private webSocketsService!: InternalServices.SNWebSocketsService
|
||||
private settingsService!: InternalServices.SNSettingsService
|
||||
private mfaService!: InternalServices.SNMfaService
|
||||
@@ -235,7 +247,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
return this.itemManager
|
||||
}
|
||||
|
||||
public get protections(): InternalServices.ProtectionsClientInterface {
|
||||
public get protections(): ProtectionsClientInterface {
|
||||
return this.protectionService
|
||||
}
|
||||
|
||||
@@ -255,7 +267,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
return this.mutatorService
|
||||
}
|
||||
|
||||
public get sessions(): InternalServices.SessionsClientInterface {
|
||||
public get sessions(): SessionsClientInterface {
|
||||
return this.sessionManager
|
||||
}
|
||||
|
||||
@@ -347,8 +359,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
await this.diskStorageService.decryptStorage()
|
||||
} catch (_error) {
|
||||
void this.alertService.alert(
|
||||
InternalServices.ErrorAlertStrings.StorageDecryptErrorBody,
|
||||
InternalServices.ErrorAlertStrings.StorageDecryptErrorTitle,
|
||||
ErrorAlertStrings.StorageDecryptErrorBody,
|
||||
ErrorAlertStrings.StorageDecryptErrorTitle,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -628,12 +640,12 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
const result = await this.userService.performProtocolUpgrade()
|
||||
if (result.success) {
|
||||
if (this.hasAccount()) {
|
||||
void this.alertService.alert(InternalServices.ProtocolUpgradeStrings.SuccessAccount)
|
||||
void this.alertService.alert(ProtocolUpgradeStrings.SuccessAccount)
|
||||
} else {
|
||||
void this.alertService.alert(InternalServices.ProtocolUpgradeStrings.SuccessPasscodeOnly)
|
||||
void this.alertService.alert(ProtocolUpgradeStrings.SuccessPasscodeOnly)
|
||||
}
|
||||
} else if (result.error) {
|
||||
void this.alertService.alert(InternalServices.ProtocolUpgradeStrings.Fail)
|
||||
void this.alertService.alert(ProtocolUpgradeStrings.Fail)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -848,7 +860,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
currentPassword: string,
|
||||
passcode?: string,
|
||||
origination = Common.KeyParamsOrigination.EmailChange,
|
||||
): Promise<InternalServices.CredentialsChangeFunctionResponse> {
|
||||
): Promise<CredentialsChangeFunctionResponse> {
|
||||
return this.userService.changeCredentials({
|
||||
currentPassword,
|
||||
newEmail,
|
||||
@@ -864,7 +876,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
passcode?: string,
|
||||
origination = Common.KeyParamsOrigination.PasswordChange,
|
||||
validateNewPasswordStrength = true,
|
||||
): Promise<InternalServices.CredentialsChangeFunctionResponse> {
|
||||
): Promise<CredentialsChangeFunctionResponse> {
|
||||
return this.userService.changeCredentials({
|
||||
currentPassword,
|
||||
newPassword,
|
||||
@@ -886,7 +898,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
/** Keep a reference to the soon-to-be-cleared alertService */
|
||||
const alertService = this.alertService
|
||||
await this.user.signOut(true)
|
||||
void alertService.alert(InternalServices.SessionStrings.CurrentSessionRevoked)
|
||||
void alertService.alert(SessionStrings.CurrentSessionRevoked)
|
||||
}
|
||||
|
||||
public async validateAccountPassword(password: string): Promise<boolean> {
|
||||
@@ -1064,6 +1076,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
this.createApiService()
|
||||
this.createHttpService()
|
||||
this.createUserServer()
|
||||
this.createUserRequestServer()
|
||||
this.createUserApiService()
|
||||
this.createSubscriptionServer()
|
||||
this.createSubscriptionApiService()
|
||||
@@ -1111,6 +1124,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
;(this.apiService as unknown) = undefined
|
||||
;(this.userApiService as unknown) = undefined
|
||||
;(this.userServer as unknown) = undefined
|
||||
;(this.userRequestServer as unknown) = undefined
|
||||
;(this.subscriptionApiService as unknown) = undefined
|
||||
;(this.subscriptionServer as unknown) = undefined
|
||||
;(this.subscriptionManager as unknown) = undefined
|
||||
@@ -1261,7 +1275,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
}
|
||||
|
||||
private createUserService(): void {
|
||||
this.userService = new InternalServices.UserService(
|
||||
this.userService = new UserService(
|
||||
this.sessionManager,
|
||||
this.syncService,
|
||||
this.diskStorageService,
|
||||
@@ -1270,17 +1284,17 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
this.alertService,
|
||||
this.challengeService,
|
||||
this.protectionService,
|
||||
this.apiService,
|
||||
this.userApiService,
|
||||
this.internalEventBus,
|
||||
)
|
||||
this.serviceObservers.push(
|
||||
this.userService.addEventObserver(async (event, data) => {
|
||||
switch (event) {
|
||||
case InternalServices.AccountEvent.SignedInOrRegistered: {
|
||||
case AccountEvent.SignedInOrRegistered: {
|
||||
void this.notifyEvent(ApplicationEvent.SignedIn)
|
||||
break
|
||||
}
|
||||
case InternalServices.AccountEvent.SignedOut: {
|
||||
case AccountEvent.SignedOut: {
|
||||
await this.notifyEvent(ApplicationEvent.SignedOut)
|
||||
await this.prepareForDeinit()
|
||||
this.deinit(this.getDeinitMode(), data?.source || DeinitSource.SignOut)
|
||||
@@ -1308,13 +1322,17 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
}
|
||||
|
||||
private createUserApiService() {
|
||||
this.userApiService = new UserApiService(this.userServer)
|
||||
this.userApiService = new UserApiService(this.userServer, this.userRequestServer)
|
||||
}
|
||||
|
||||
private createUserServer() {
|
||||
this.userServer = new UserServer(this.httpService)
|
||||
}
|
||||
|
||||
private createUserRequestServer() {
|
||||
this.userRequestServer = new UserRequestServer(this.httpService)
|
||||
}
|
||||
|
||||
private createSubscriptionApiService() {
|
||||
this.subscriptionApiService = new SubscriptionApiService(this.subscriptionServer)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { InfoStrings } from '../Strings/Info'
|
||||
import { NoteType } from '@standardnotes/features'
|
||||
import { InfoStrings } from '@standardnotes/services'
|
||||
import {
|
||||
NoteMutator,
|
||||
SNNote,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { AnyKeyParamsContent } from '@standardnotes/common'
|
||||
import { SNLog } from '@Lib/Log'
|
||||
import { EncryptedPayload, EncryptedTransferPayload, isErrorDecryptingPayload } from '@standardnotes/models'
|
||||
import { Challenge } from '../Services/Challenge'
|
||||
import { KeychainRecoveryStrings, SessionStrings } from '../Services/Api/Messages'
|
||||
import { PreviousSnjsVersion1_0_0, PreviousSnjsVersion2_0_0, SnjsVersion } from '../Version'
|
||||
import { Migration } from '@Lib/Migrations/Migration'
|
||||
import {
|
||||
@@ -12,6 +10,9 @@ import {
|
||||
ChallengeValidation,
|
||||
ChallengeReason,
|
||||
ChallengePrompt,
|
||||
KeychainRecoveryStrings,
|
||||
SessionStrings,
|
||||
Challenge,
|
||||
} from '@standardnotes/services'
|
||||
import { isNullOrUndefined } from '@standardnotes/utils'
|
||||
import { CreateReader } from './StorageReaders/Functions'
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { Challenge } from '../Services/Challenge'
|
||||
import { MigrationServices } from './MigrationServices'
|
||||
import { ApplicationStage, ChallengeValidation, ChallengeReason, ChallengePrompt } from '@standardnotes/services'
|
||||
import {
|
||||
ApplicationStage,
|
||||
ChallengeValidation,
|
||||
ChallengeReason,
|
||||
ChallengePrompt,
|
||||
Challenge,
|
||||
} from '@standardnotes/services'
|
||||
|
||||
type StageHandler = () => Promise<void>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SNRootKey } from '@standardnotes/encryption'
|
||||
import { Challenge, ChallengeService } from '../Challenge'
|
||||
import { ChallengeService } from '../Challenge'
|
||||
import { ListedService } from '../Listed/ListedService'
|
||||
import { ActionResponse, HttpResponse } from '@standardnotes/responses'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
ChallengeReason,
|
||||
ChallengePrompt,
|
||||
EncryptionService,
|
||||
Challenge,
|
||||
} from '@standardnotes/services'
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,28 +13,44 @@ import {
|
||||
MetaReceivedData,
|
||||
DiagnosticInfo,
|
||||
KeyValueStoreInterface,
|
||||
API_MESSAGE_GENERIC_SYNC_FAIL,
|
||||
API_MESSAGE_GENERIC_TOKEN_REFRESH_FAIL,
|
||||
API_MESSAGE_CHANGE_CREDENTIALS_IN_PROGRESS,
|
||||
API_MESSAGE_FAILED_ACCESS_PURCHASE,
|
||||
API_MESSAGE_FAILED_CREATE_FILE_TOKEN,
|
||||
API_MESSAGE_FAILED_DELETE_REVISION,
|
||||
API_MESSAGE_FAILED_GET_SETTINGS,
|
||||
API_MESSAGE_FAILED_LISTED_REGISTRATION,
|
||||
API_MESSAGE_FAILED_OFFLINE_ACTIVATION,
|
||||
API_MESSAGE_FAILED_OFFLINE_FEATURES,
|
||||
API_MESSAGE_FAILED_SUBSCRIPTION_INFO,
|
||||
API_MESSAGE_FAILED_UPDATE_SETTINGS,
|
||||
API_MESSAGE_GENERIC_CHANGE_CREDENTIALS_FAIL,
|
||||
API_MESSAGE_GENERIC_INTEGRITY_CHECK_FAIL,
|
||||
API_MESSAGE_GENERIC_INVALID_LOGIN,
|
||||
API_MESSAGE_GENERIC_SINGLE_ITEM_SYNC_FAIL,
|
||||
API_MESSAGE_INVALID_SESSION,
|
||||
API_MESSAGE_LOGIN_IN_PROGRESS,
|
||||
API_MESSAGE_TOKEN_REFRESH_IN_PROGRESS,
|
||||
} from '@standardnotes/services'
|
||||
import { FilesApiInterface } from '@standardnotes/files'
|
||||
|
||||
import { ServerSyncPushContextualPayload, SNFeatureRepo, FileContent } from '@standardnotes/models'
|
||||
import * as Responses from '@standardnotes/responses'
|
||||
import { API_MESSAGE_FAILED_OFFLINE_ACTIVATION } from '@Lib/Services/Api/Messages'
|
||||
import { HttpParams, HttpRequest, HttpVerb, SNHttpService } from './HttpService'
|
||||
import { isUrlFirstParty, TRUSTED_FEATURE_HOSTS } from '@Lib/Hosts'
|
||||
import { Paths } from './Paths'
|
||||
import { Session } from '../Session/Sessions/Session'
|
||||
import { TokenSession } from '../Session/Sessions/TokenSession'
|
||||
import { DiskStorageService } from '../Storage/DiskStorageService'
|
||||
import { UserServerInterface } from '../User/UserServerInterface'
|
||||
import { HttpResponseMeta } from '@standardnotes/api'
|
||||
import { UuidString } from '../../Types/UuidString'
|
||||
import * as messages from '@Lib/Services/Api/Messages'
|
||||
import merge from 'lodash/merge'
|
||||
import { SettingsServerInterface } from '../Settings/SettingsServerInterface'
|
||||
import { Strings } from '@Lib/Strings'
|
||||
import { SNRootKeyParams } from '@standardnotes/encryption'
|
||||
import { ApiEndpointParam, ClientDisplayableError, CreateValetTokenPayload } from '@standardnotes/responses'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { HttpResponseMeta } from '@standardnotes/api'
|
||||
|
||||
/** Legacy api version field to be specified in params when calling v0 APIs. */
|
||||
const V0_API_VERSION = '20200115'
|
||||
@@ -48,7 +64,6 @@ export class SNApiService
|
||||
FilesApiInterface,
|
||||
IntegrityApiInterface,
|
||||
ItemsServerInterface,
|
||||
UserServerInterface,
|
||||
SettingsServerInterface
|
||||
{
|
||||
private session?: Session
|
||||
@@ -232,7 +247,7 @@ export class SNApiService
|
||||
return this.request({
|
||||
verb: HttpVerb.Post,
|
||||
url: joinPaths(this.host, Paths.v2.keyParams),
|
||||
fallbackErrorMessage: messages.API_MESSAGE_GENERIC_INVALID_LOGIN,
|
||||
fallbackErrorMessage: API_MESSAGE_GENERIC_INVALID_LOGIN,
|
||||
params,
|
||||
/** A session is optional here, if valid, endpoint bypasses 2FA and returns additional params */
|
||||
authentication: this.session?.authorizationValue,
|
||||
@@ -245,7 +260,7 @@ export class SNApiService
|
||||
ephemeral: boolean
|
||||
}): Promise<Responses.SignInResponse | Responses.HttpResponse> {
|
||||
if (this.authenticating) {
|
||||
return this.createErrorResponse(messages.API_MESSAGE_LOGIN_IN_PROGRESS) as Responses.SignInResponse
|
||||
return this.createErrorResponse(API_MESSAGE_LOGIN_IN_PROGRESS) as Responses.SignInResponse
|
||||
}
|
||||
this.authenticating = true
|
||||
const url = joinPaths(this.host, Paths.v2.signIn)
|
||||
@@ -260,7 +275,7 @@ export class SNApiService
|
||||
verb: HttpVerb.Post,
|
||||
url,
|
||||
params,
|
||||
fallbackErrorMessage: messages.API_MESSAGE_GENERIC_INVALID_LOGIN,
|
||||
fallbackErrorMessage: API_MESSAGE_GENERIC_INVALID_LOGIN,
|
||||
})
|
||||
|
||||
this.authenticating = false
|
||||
@@ -285,7 +300,7 @@ export class SNApiService
|
||||
newEmail?: string
|
||||
}): Promise<Responses.ChangeCredentialsResponse | Responses.HttpResponse> {
|
||||
if (this.changing) {
|
||||
return this.createErrorResponse(messages.API_MESSAGE_CHANGE_CREDENTIALS_IN_PROGRESS)
|
||||
return this.createErrorResponse(API_MESSAGE_CHANGE_CREDENTIALS_IN_PROGRESS)
|
||||
}
|
||||
const preprocessingError = this.preprocessingError()
|
||||
if (preprocessingError) {
|
||||
@@ -309,10 +324,7 @@ export class SNApiService
|
||||
params,
|
||||
})
|
||||
}
|
||||
return this.errorResponseWithFallbackMessage(
|
||||
errorResponse,
|
||||
messages.API_MESSAGE_GENERIC_CHANGE_CREDENTIALS_FAIL,
|
||||
)
|
||||
return this.errorResponseWithFallbackMessage(errorResponse, API_MESSAGE_GENERIC_CHANGE_CREDENTIALS_FAIL)
|
||||
})
|
||||
|
||||
this.processResponse(response)
|
||||
@@ -321,17 +333,6 @@ export class SNApiService
|
||||
return response
|
||||
}
|
||||
|
||||
public async deleteAccount(userUuid: string): Promise<Responses.HttpResponse | Responses.MinimalHttpResponse> {
|
||||
const url = joinPaths(this.host, Paths.v1.deleteAccount(userUuid))
|
||||
const response = await this.request({
|
||||
verb: HttpVerb.Delete,
|
||||
url,
|
||||
authentication: this.session?.authorizationValue,
|
||||
fallbackErrorMessage: messages.ServerErrorStrings.DeleteAccountError,
|
||||
})
|
||||
return response
|
||||
}
|
||||
|
||||
async sync(
|
||||
payloads: ServerSyncPushContextualPayload[],
|
||||
lastSyncToken: string,
|
||||
@@ -360,7 +361,7 @@ export class SNApiService
|
||||
params,
|
||||
})
|
||||
}
|
||||
return this.errorResponseWithFallbackMessage(errorResponse, messages.API_MESSAGE_GENERIC_SYNC_FAIL)
|
||||
return this.errorResponseWithFallbackMessage(errorResponse, API_MESSAGE_GENERIC_SYNC_FAIL)
|
||||
})
|
||||
this.processResponse(response)
|
||||
|
||||
@@ -405,7 +406,7 @@ export class SNApiService
|
||||
})
|
||||
.catch((errorResponse) => {
|
||||
this.preprocessAuthenticatedErrorResponse(errorResponse)
|
||||
return this.errorResponseWithFallbackMessage(errorResponse, messages.API_MESSAGE_GENERIC_TOKEN_REFRESH_FAIL)
|
||||
return this.errorResponseWithFallbackMessage(errorResponse, API_MESSAGE_GENERIC_TOKEN_REFRESH_FAIL)
|
||||
})
|
||||
this.refreshingSession = false
|
||||
return result
|
||||
@@ -427,7 +428,7 @@ export class SNApiService
|
||||
url,
|
||||
})
|
||||
}
|
||||
return this.errorResponseWithFallbackMessage(errorResponse, messages.API_MESSAGE_GENERIC_SYNC_FAIL)
|
||||
return this.errorResponseWithFallbackMessage(errorResponse, API_MESSAGE_GENERIC_SYNC_FAIL)
|
||||
})
|
||||
this.processResponse(response)
|
||||
|
||||
@@ -451,7 +452,7 @@ export class SNApiService
|
||||
url,
|
||||
})
|
||||
}
|
||||
return this.errorResponseWithFallbackMessage(errorResponse, messages.API_MESSAGE_GENERIC_SYNC_FAIL)
|
||||
return this.errorResponseWithFallbackMessage(errorResponse, API_MESSAGE_GENERIC_SYNC_FAIL)
|
||||
})
|
||||
this.processResponse(response)
|
||||
return response
|
||||
@@ -473,7 +474,7 @@ export class SNApiService
|
||||
url,
|
||||
})
|
||||
}
|
||||
return this.errorResponseWithFallbackMessage(errorResponse, messages.API_MESSAGE_GENERIC_SYNC_FAIL)
|
||||
return this.errorResponseWithFallbackMessage(errorResponse, API_MESSAGE_GENERIC_SYNC_FAIL)
|
||||
})
|
||||
this.processResponse(response)
|
||||
return response
|
||||
@@ -498,7 +499,7 @@ export class SNApiService
|
||||
url,
|
||||
})
|
||||
}
|
||||
return this.errorResponseWithFallbackMessage(errorResponse, messages.API_MESSAGE_GENERIC_SYNC_FAIL)
|
||||
return this.errorResponseWithFallbackMessage(errorResponse, API_MESSAGE_GENERIC_SYNC_FAIL)
|
||||
})
|
||||
this.processResponse(response)
|
||||
return response
|
||||
@@ -516,7 +517,7 @@ export class SNApiService
|
||||
url,
|
||||
})
|
||||
}
|
||||
return this.errorResponseWithFallbackMessage(errorResponse, messages.API_MESSAGE_GENERIC_SYNC_FAIL)
|
||||
return this.errorResponseWithFallbackMessage(errorResponse, API_MESSAGE_GENERIC_SYNC_FAIL)
|
||||
})
|
||||
this.processResponse(response)
|
||||
return response
|
||||
@@ -546,7 +547,7 @@ export class SNApiService
|
||||
return await this.tokenRefreshableRequest<Responses.ListSettingsResponse>({
|
||||
verb: HttpVerb.Get,
|
||||
url: joinPaths(this.host, Paths.v1.settings(userUuid)),
|
||||
fallbackErrorMessage: messages.API_MESSAGE_FAILED_GET_SETTINGS,
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_GET_SETTINGS,
|
||||
authentication: this.session?.authorizationValue,
|
||||
})
|
||||
}
|
||||
@@ -566,7 +567,7 @@ export class SNApiService
|
||||
verb: HttpVerb.Put,
|
||||
url: joinPaths(this.host, Paths.v1.settings(userUuid)),
|
||||
authentication: this.session?.authorizationValue,
|
||||
fallbackErrorMessage: messages.API_MESSAGE_FAILED_UPDATE_SETTINGS,
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_UPDATE_SETTINGS,
|
||||
params,
|
||||
})
|
||||
}
|
||||
@@ -576,7 +577,7 @@ export class SNApiService
|
||||
verb: HttpVerb.Get,
|
||||
url: joinPaths(this.host, Paths.v1.setting(userUuid, settingName.toLowerCase() as SettingName)),
|
||||
authentication: this.session?.authorizationValue,
|
||||
fallbackErrorMessage: messages.API_MESSAGE_FAILED_GET_SETTINGS,
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_GET_SETTINGS,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -591,7 +592,7 @@ export class SNApiService
|
||||
Paths.v1.subscriptionSetting(userUuid, settingName.toLowerCase() as SubscriptionSettingName),
|
||||
),
|
||||
authentication: this.session?.authorizationValue,
|
||||
fallbackErrorMessage: messages.API_MESSAGE_FAILED_GET_SETTINGS,
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_GET_SETTINGS,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -600,7 +601,7 @@ export class SNApiService
|
||||
verb: HttpVerb.Delete,
|
||||
url: joinPaths(this.host, Paths.v1.setting(userUuid, settingName)),
|
||||
authentication: this.session?.authorizationValue,
|
||||
fallbackErrorMessage: messages.API_MESSAGE_FAILED_UPDATE_SETTINGS,
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_UPDATE_SETTINGS,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -612,7 +613,7 @@ export class SNApiService
|
||||
const response = await this.tokenRefreshableRequest({
|
||||
verb: HttpVerb.Delete,
|
||||
url,
|
||||
fallbackErrorMessage: messages.API_MESSAGE_FAILED_DELETE_REVISION,
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_DELETE_REVISION,
|
||||
authentication: this.session?.authorizationValue,
|
||||
})
|
||||
return response
|
||||
@@ -623,7 +624,7 @@ export class SNApiService
|
||||
verb: HttpVerb.Get,
|
||||
url,
|
||||
external: true,
|
||||
fallbackErrorMessage: messages.API_MESSAGE_GENERIC_INVALID_LOGIN,
|
||||
fallbackErrorMessage: API_MESSAGE_GENERIC_INVALID_LOGIN,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -633,7 +634,7 @@ export class SNApiService
|
||||
verb: HttpVerb.Get,
|
||||
url,
|
||||
authentication: this.session?.authorizationValue,
|
||||
fallbackErrorMessage: messages.API_MESSAGE_FAILED_SUBSCRIPTION_INFO,
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_SUBSCRIPTION_INFO,
|
||||
})
|
||||
return response
|
||||
}
|
||||
@@ -645,7 +646,7 @@ export class SNApiService
|
||||
const response = await this.request({
|
||||
verb: HttpVerb.Get,
|
||||
url,
|
||||
fallbackErrorMessage: messages.API_MESSAGE_FAILED_SUBSCRIPTION_INFO,
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_SUBSCRIPTION_INFO,
|
||||
})
|
||||
return response
|
||||
}
|
||||
@@ -656,7 +657,7 @@ export class SNApiService
|
||||
verb: HttpVerb.Post,
|
||||
url,
|
||||
authentication: this.session?.authorizationValue,
|
||||
fallbackErrorMessage: messages.API_MESSAGE_FAILED_ACCESS_PURCHASE,
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_ACCESS_PURCHASE,
|
||||
})
|
||||
return (response as Responses.PostSubscriptionTokensResponse).data?.token
|
||||
}
|
||||
@@ -680,7 +681,7 @@ export class SNApiService
|
||||
const response: Responses.HttpResponse | Responses.GetOfflineFeaturesResponse = await this.request({
|
||||
verb: HttpVerb.Get,
|
||||
url: featuresUrl,
|
||||
fallbackErrorMessage: messages.API_MESSAGE_FAILED_OFFLINE_FEATURES,
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_OFFLINE_FEATURES,
|
||||
customHeaders: [{ key: 'x-offline-token', value: extensionKey }],
|
||||
})
|
||||
|
||||
@@ -702,7 +703,7 @@ export class SNApiService
|
||||
return await this.tokenRefreshableRequest<Responses.ListedRegistrationResponse>({
|
||||
verb: HttpVerb.Post,
|
||||
url: joinPaths(this.host, Paths.v1.listedRegistration(this.user.uuid)),
|
||||
fallbackErrorMessage: messages.API_MESSAGE_FAILED_LISTED_REGISTRATION,
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_LISTED_REGISTRATION,
|
||||
authentication: this.session?.authorizationValue,
|
||||
})
|
||||
}
|
||||
@@ -723,7 +724,7 @@ export class SNApiService
|
||||
verb: HttpVerb.Post,
|
||||
url: url,
|
||||
authentication: this.session?.authorizationValue,
|
||||
fallbackErrorMessage: messages.API_MESSAGE_FAILED_CREATE_FILE_TOKEN,
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_CREATE_FILE_TOKEN,
|
||||
params,
|
||||
})
|
||||
|
||||
@@ -856,7 +857,7 @@ export class SNApiService
|
||||
params: {
|
||||
integrityPayloads,
|
||||
},
|
||||
fallbackErrorMessage: messages.API_MESSAGE_GENERIC_INTEGRITY_CHECK_FAIL,
|
||||
fallbackErrorMessage: API_MESSAGE_GENERIC_INTEGRITY_CHECK_FAIL,
|
||||
authentication: this.session?.authorizationValue,
|
||||
})
|
||||
}
|
||||
@@ -865,17 +866,17 @@ export class SNApiService
|
||||
return await this.tokenRefreshableRequest<Responses.GetSingleItemResponse>({
|
||||
verb: HttpVerb.Get,
|
||||
url: joinPaths(this.host, Paths.v1.getSingleItem(itemUuid)),
|
||||
fallbackErrorMessage: messages.API_MESSAGE_GENERIC_SINGLE_ITEM_SYNC_FAIL,
|
||||
fallbackErrorMessage: API_MESSAGE_GENERIC_SINGLE_ITEM_SYNC_FAIL,
|
||||
authentication: this.session?.authorizationValue,
|
||||
})
|
||||
}
|
||||
|
||||
private preprocessingError() {
|
||||
if (this.refreshingSession) {
|
||||
return this.createErrorResponse(messages.API_MESSAGE_TOKEN_REFRESH_IN_PROGRESS)
|
||||
return this.createErrorResponse(API_MESSAGE_TOKEN_REFRESH_IN_PROGRESS)
|
||||
}
|
||||
if (!this.session) {
|
||||
return this.createErrorResponse(messages.API_MESSAGE_INVALID_SESSION)
|
||||
return this.createErrorResponse(API_MESSAGE_INVALID_SESSION)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { API_MESSAGE_RATE_LIMITED, UNKNOWN_ERROR } from './Messages'
|
||||
import { HttpResponse, StatusCode } from '@standardnotes/responses'
|
||||
import { isString } from '@standardnotes/utils'
|
||||
import { SnjsVersion } from '@Lib/Version'
|
||||
import { AbstractService, InternalEventBusInterface } from '@standardnotes/services'
|
||||
import {
|
||||
AbstractService,
|
||||
API_MESSAGE_RATE_LIMITED,
|
||||
InternalEventBusInterface,
|
||||
UNKNOWN_ERROR,
|
||||
} from '@standardnotes/services'
|
||||
import { Environment } from '@standardnotes/models'
|
||||
|
||||
export enum HttpVerb {
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
import { ProtocolVersion } from '@standardnotes/common'
|
||||
|
||||
export const API_MESSAGE_GENERIC_INVALID_LOGIN = 'A server error occurred while trying to sign in. Please try again.'
|
||||
export const API_MESSAGE_GENERIC_REGISTRATION_FAIL =
|
||||
'A server error occurred while trying to register. Please try again.'
|
||||
export const API_MESSAGE_GENERIC_CHANGE_CREDENTIALS_FAIL =
|
||||
'Something went wrong while changing your credentials. Your credentials were not changed. Please try again.'
|
||||
export const API_MESSAGE_GENERIC_SYNC_FAIL = 'Could not connect to server.'
|
||||
|
||||
export const ServerErrorStrings = {
|
||||
DeleteAccountError: 'Your account was unable to be deleted due to an error. Please try your request again.',
|
||||
}
|
||||
|
||||
export const API_MESSAGE_GENERIC_INTEGRITY_CHECK_FAIL = 'Could not check your data integrity with the server.'
|
||||
|
||||
export const API_MESSAGE_GENERIC_SINGLE_ITEM_SYNC_FAIL = 'Could not retrieve item.'
|
||||
|
||||
export const API_MESSAGE_REGISTRATION_IN_PROGRESS = 'An existing registration request is already in progress.'
|
||||
export const API_MESSAGE_LOGIN_IN_PROGRESS = 'An existing sign in request is already in progress.'
|
||||
export const API_MESSAGE_CHANGE_CREDENTIALS_IN_PROGRESS =
|
||||
'An existing change credentials request is already in progress.'
|
||||
|
||||
export const API_MESSAGE_FALLBACK_LOGIN_FAIL = 'Invalid email or password.'
|
||||
|
||||
export const API_MESSAGE_GENERIC_TOKEN_REFRESH_FAIL =
|
||||
'A server error occurred while trying to refresh your session. Please try again.'
|
||||
|
||||
export const API_MESSAGE_TOKEN_REFRESH_IN_PROGRESS =
|
||||
'Your account session is being renewed with the server. Please try your request again.'
|
||||
|
||||
export const API_MESSAGE_RATE_LIMITED = 'Too many successive server requests. Please wait a few minutes and try again.'
|
||||
|
||||
export const API_MESSAGE_INVALID_SESSION = 'Please sign in to an account in order to continue with your request.'
|
||||
|
||||
export const API_MESSAGE_FAILED_GET_SETTINGS = 'Failed to get settings.'
|
||||
export const API_MESSAGE_FAILED_UPDATE_SETTINGS = 'Failed to update settings.'
|
||||
export const API_MESSAGE_FAILED_LISTED_REGISTRATION = 'Unable to register for Listed. Please try again later.'
|
||||
|
||||
export const API_MESSAGE_FAILED_CREATE_FILE_TOKEN = 'Failed to create file token.'
|
||||
|
||||
export const API_MESSAGE_FAILED_SUBSCRIPTION_INFO = "Failed to get subscription's information."
|
||||
|
||||
export const API_MESSAGE_FAILED_ACCESS_PURCHASE = 'Failed to access purchase flow.'
|
||||
|
||||
export const API_MESSAGE_FAILED_DELETE_REVISION = 'Failed to delete revision.'
|
||||
|
||||
export const API_MESSAGE_FAILED_OFFLINE_FEATURES = 'Failed to get offline features.'
|
||||
export const API_MESSAGE_UNTRUSTED_EXTENSIONS_WARNING = `The extension you are attempting to install comes from an
|
||||
untrusted source. Untrusted extensions may lower the security of your data. Do you want to continue?`
|
||||
export const API_MESSAGE_FAILED_DOWNLOADING_EXTENSION = `Error downloading package details. Please check the
|
||||
extension link and try again.`
|
||||
export const API_MESSAGE_FAILED_OFFLINE_ACTIVATION =
|
||||
'An unknown issue occurred during offline activation. Please try again.'
|
||||
|
||||
export const INVALID_EXTENSION_URL = 'Invalid extension URL.'
|
||||
|
||||
export const UNSUPPORTED_PROTOCOL_VERSION =
|
||||
'This version of the application does not support your newer account type. Please upgrade to the latest version of Standard Notes to sign in.'
|
||||
|
||||
export const EXPIRED_PROTOCOL_VERSION =
|
||||
'The protocol version associated with your account is outdated and no longer supported by this application. Please visit standardnotes.com/help/security for more information.'
|
||||
|
||||
export const UNSUPPORTED_KEY_DERIVATION =
|
||||
'Your account was created on a platform with higher security capabilities than this browser supports. If we attempted to generate your login keys here, it would take hours. Please use a browser with more up to date security capabilities, like Google Chrome or Firefox, to log in.'
|
||||
|
||||
export const INVALID_PASSWORD_COST =
|
||||
'Unable to sign in due to insecure password parameters. Please visit standardnotes.com/help/security for more information.'
|
||||
export const INVALID_PASSWORD = 'Invalid password.'
|
||||
|
||||
export const OUTDATED_PROTOCOL_ALERT_IGNORE = 'Sign In'
|
||||
export const UPGRADING_ENCRYPTION = "Upgrading your account's encryption version…"
|
||||
|
||||
export const SETTING_PASSCODE = 'Setting passcode…'
|
||||
export const CHANGING_PASSCODE = 'Changing passcode…'
|
||||
export const REMOVING_PASSCODE = 'Removing passcode…'
|
||||
|
||||
export const DO_NOT_CLOSE_APPLICATION = 'Do not close the application until this process completes.'
|
||||
|
||||
export const UNKNOWN_ERROR = 'Unknown error.'
|
||||
|
||||
export function InsufficientPasswordMessage(minimum: number): string {
|
||||
return `Your password must be at least ${minimum} characters in length. For your security, please choose a longer password or, ideally, a passphrase, and try again.`
|
||||
}
|
||||
|
||||
export function StrictSignInFailed(current: ProtocolVersion, latest: ProtocolVersion): string {
|
||||
return `Strict Sign In has refused the server's sign-in parameters. The latest account version is ${latest}, but the server is reporting a version of ${current} for your account. If you'd like to proceed with sign in anyway, please disable Strict Sign In and try again.`
|
||||
}
|
||||
|
||||
export const CredentialsChangeStrings = {
|
||||
PasscodeRequired: 'Your passcode is required to process your credentials change.',
|
||||
Failed: 'Unable to change your credentials due to a sync error. Please try again.',
|
||||
}
|
||||
|
||||
export const RegisterStrings = {
|
||||
PasscodeRequired: 'Your passcode is required in order to register for an account.',
|
||||
}
|
||||
|
||||
export const SignInStrings = {
|
||||
PasscodeRequired: 'Your passcode is required in order to sign in to your account.',
|
||||
IncorrectMfa: 'Incorrect two-factor authentication code. Please try again.',
|
||||
SignInCanceledMissingMfa: 'Your sign in request has been canceled.',
|
||||
}
|
||||
|
||||
export const ProtocolUpgradeStrings = {
|
||||
SuccessAccount:
|
||||
"Your encryption version has been successfully upgraded. You may be asked to enter your credentials again on other devices you're signed into.",
|
||||
SuccessPasscodeOnly: 'Your encryption version has been successfully upgraded.',
|
||||
Fail: 'Unable to upgrade encryption version. Please try again.',
|
||||
UpgradingPasscode: 'Upgrading local encryption...',
|
||||
}
|
||||
|
||||
export const ChallengeModalTitle = {
|
||||
Generic: 'Authentication Required',
|
||||
Migration: 'Storage Update',
|
||||
}
|
||||
|
||||
export const SessionStrings = {
|
||||
EnterEmailAndPassword: 'Please enter your account email and password.',
|
||||
RecoverSession(email?: string): string {
|
||||
return email
|
||||
? `Your credentials are needed for ${email} to refresh your session with the server.`
|
||||
: 'Your credentials are needed to refresh your session with the server.'
|
||||
},
|
||||
SessionRestored: 'Your session has been successfully restored.',
|
||||
EnterMfa: 'Please enter your two-factor authentication code.',
|
||||
MfaInputPlaceholder: 'Two-factor authentication code',
|
||||
EmailInputPlaceholder: 'Email',
|
||||
PasswordInputPlaceholder: 'Password',
|
||||
KeychainRecoveryErrorTitle: 'Invalid Credentials',
|
||||
KeychainRecoveryError:
|
||||
'The email or password you entered is incorrect.\n\nPlease note that this sign-in request is made against the default server. If you are using a custom server, you must uninstall the app then reinstall, and sign back into your account.',
|
||||
RevokeTitle: 'Revoke this session?',
|
||||
RevokeConfirmButton: 'Revoke',
|
||||
RevokeCancelButton: 'Cancel',
|
||||
RevokeText:
|
||||
'The associated app will be signed out and all data removed ' +
|
||||
'from the device when it is next launched. You can sign back in on that ' +
|
||||
'device at any time.',
|
||||
CurrentSessionRevoked: 'Your session has been revoked and all local data has been removed ' + 'from this device.',
|
||||
}
|
||||
|
||||
export const ChallengeStrings = {
|
||||
UnlockApplication: 'Authentication is required to unlock the application',
|
||||
NoteAccess: 'Authentication is required to view this note',
|
||||
FileAccess: 'Authentication is required to access this file',
|
||||
ImportFile: 'Authentication is required to import a backup file',
|
||||
AddPasscode: 'Authentication is required to add a passcode',
|
||||
RemovePasscode: 'Authentication is required to remove your passcode',
|
||||
ChangePasscode: 'Authentication is required to change your passcode',
|
||||
ChangeAutolockInterval: 'Authentication is required to change autolock timer duration',
|
||||
RevokeSession: 'Authentication is required to revoke a session',
|
||||
EnterAccountPassword: 'Enter your account password',
|
||||
EnterLocalPasscode: 'Enter your application passcode',
|
||||
EnterPasscodeForMigration:
|
||||
'Your application passcode is required to perform an upgrade of your local data storage structure.',
|
||||
EnterPasscodeForRootResave: 'Enter your application passcode to continue',
|
||||
EnterCredentialsForProtocolUpgrade: 'Enter your credentials to perform encryption upgrade',
|
||||
EnterCredentialsForDecryptedBackupDownload: 'Enter your credentials to download a decrypted backup',
|
||||
AccountPasswordPlaceholder: 'Account Password',
|
||||
LocalPasscodePlaceholder: 'Application Passcode',
|
||||
DecryptEncryptedFile: 'Enter the account password associated with the import file',
|
||||
ExportBackup: 'Authentication is required to export a backup',
|
||||
DisableBiometrics: 'Authentication is required to disable biometrics',
|
||||
UnprotectNote: 'Authentication is required to unprotect a note',
|
||||
UnprotectFile: 'Authentication is required to unprotect a file',
|
||||
SearchProtectedNotesText: 'Authentication is required to search protected contents',
|
||||
SelectProtectedNote: 'Authentication is required to select a protected note',
|
||||
DisableMfa: 'Authentication is required to disable two-factor authentication',
|
||||
DeleteAccount: 'Authentication is required to delete your account',
|
||||
ListedAuthorization: 'Authentication is required to approve this note for Listed',
|
||||
}
|
||||
|
||||
export const ErrorAlertStrings = {
|
||||
MissingSessionTitle: 'Missing Session',
|
||||
MissingSessionBody:
|
||||
'We were unable to load your server session. This represents an inconsistency with your application state. Please take an opportunity to backup your data, then sign out and sign back in to resolve this issue.',
|
||||
|
||||
StorageDecryptErrorTitle: 'Storage Error',
|
||||
StorageDecryptErrorBody:
|
||||
"We were unable to decrypt your local storage. Please restart the app and try again. If you're unable to resolve this issue, and you have an account, you may try uninstalling the app then reinstalling, then signing back into your account. Otherwise, please contact help@standardnotes.org for support.",
|
||||
}
|
||||
|
||||
export const KeychainRecoveryStrings = {
|
||||
Title: 'Restore Keychain',
|
||||
Text: "We've detected that your keychain has been wiped. This can happen when restoring your device from a backup. Please enter your account password to restore your account keys.",
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
export * from './ApiService'
|
||||
export * from './HttpService'
|
||||
export * from './Messages'
|
||||
export * from './Paths'
|
||||
export * from '../Session/Sessions/Session'
|
||||
export * from '../Session/SessionManager'
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
import { ChallengeModalTitle, ChallengeStrings } from '../Api/Messages'
|
||||
import { assertUnreachable } from '@standardnotes/utils'
|
||||
import { ChallengeValidation, ChallengeReason, ChallengeInterface, ChallengePrompt } from '@standardnotes/services'
|
||||
|
||||
/**
|
||||
* A challenge is a stateless description of what the client needs to provide
|
||||
* in order to proceed.
|
||||
*/
|
||||
export class Challenge implements ChallengeInterface {
|
||||
public readonly id = Math.random()
|
||||
|
||||
constructor(
|
||||
public readonly prompts: ChallengePrompt[],
|
||||
public readonly reason: ChallengeReason,
|
||||
public readonly cancelable: boolean,
|
||||
public readonly _heading?: string,
|
||||
public readonly _subheading?: string,
|
||||
) {
|
||||
Object.freeze(this)
|
||||
}
|
||||
|
||||
/** Outside of the modal, this is the title of the modal itself */
|
||||
get modalTitle(): string {
|
||||
switch (this.reason) {
|
||||
case ChallengeReason.Migration:
|
||||
return ChallengeModalTitle.Migration
|
||||
default:
|
||||
return ChallengeModalTitle.Generic
|
||||
}
|
||||
}
|
||||
|
||||
/** Inside of the modal, this is the H1 */
|
||||
get heading(): string | undefined {
|
||||
if (this._heading) {
|
||||
return this._heading
|
||||
} else {
|
||||
switch (this.reason) {
|
||||
case ChallengeReason.ApplicationUnlock:
|
||||
return ChallengeStrings.UnlockApplication
|
||||
case ChallengeReason.Migration:
|
||||
return ChallengeStrings.EnterLocalPasscode
|
||||
case ChallengeReason.ResaveRootKey:
|
||||
return ChallengeStrings.EnterPasscodeForRootResave
|
||||
case ChallengeReason.ProtocolUpgrade:
|
||||
return ChallengeStrings.EnterCredentialsForProtocolUpgrade
|
||||
case ChallengeReason.AccessProtectedNote:
|
||||
return ChallengeStrings.NoteAccess
|
||||
case ChallengeReason.AccessProtectedFile:
|
||||
return ChallengeStrings.FileAccess
|
||||
case ChallengeReason.ImportFile:
|
||||
return ChallengeStrings.ImportFile
|
||||
case ChallengeReason.AddPasscode:
|
||||
return ChallengeStrings.AddPasscode
|
||||
case ChallengeReason.RemovePasscode:
|
||||
return ChallengeStrings.RemovePasscode
|
||||
case ChallengeReason.ChangePasscode:
|
||||
return ChallengeStrings.ChangePasscode
|
||||
case ChallengeReason.ChangeAutolockInterval:
|
||||
return ChallengeStrings.ChangeAutolockInterval
|
||||
case ChallengeReason.CreateDecryptedBackupWithProtectedItems:
|
||||
return ChallengeStrings.EnterCredentialsForDecryptedBackupDownload
|
||||
case ChallengeReason.RevokeSession:
|
||||
return ChallengeStrings.RevokeSession
|
||||
case ChallengeReason.DecryptEncryptedFile:
|
||||
return ChallengeStrings.DecryptEncryptedFile
|
||||
case ChallengeReason.ExportBackup:
|
||||
return ChallengeStrings.ExportBackup
|
||||
case ChallengeReason.DisableBiometrics:
|
||||
return ChallengeStrings.DisableBiometrics
|
||||
case ChallengeReason.UnprotectNote:
|
||||
return ChallengeStrings.UnprotectNote
|
||||
case ChallengeReason.UnprotectFile:
|
||||
return ChallengeStrings.UnprotectFile
|
||||
case ChallengeReason.SearchProtectedNotesText:
|
||||
return ChallengeStrings.SearchProtectedNotesText
|
||||
case ChallengeReason.SelectProtectedNote:
|
||||
return ChallengeStrings.SelectProtectedNote
|
||||
case ChallengeReason.DisableMfa:
|
||||
return ChallengeStrings.DisableMfa
|
||||
case ChallengeReason.DeleteAccount:
|
||||
return ChallengeStrings.DeleteAccount
|
||||
case ChallengeReason.AuthorizeNoteForListed:
|
||||
return ChallengeStrings.ListedAuthorization
|
||||
case ChallengeReason.Custom:
|
||||
return ''
|
||||
default:
|
||||
return assertUnreachable(this.reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Inside of the modal, this is the H2 */
|
||||
get subheading(): string | undefined {
|
||||
if (this._subheading) {
|
||||
return this._subheading
|
||||
}
|
||||
|
||||
switch (this.reason) {
|
||||
case ChallengeReason.Migration:
|
||||
return ChallengeStrings.EnterPasscodeForMigration
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
hasPromptForValidationType(type: ChallengeValidation): boolean {
|
||||
for (const prompt of this.prompts) {
|
||||
if (prompt.validation === type) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Challenge } from './Challenge'
|
||||
import { Challenge, ChallengeValue, ChallengeArtifacts } from '@standardnotes/services'
|
||||
import { ChallengeResponse } from './ChallengeResponse'
|
||||
import { removeFromArray } from '@standardnotes/utils'
|
||||
import { ValueCallback } from './ChallengeService'
|
||||
import { ChallengeValue, ChallengeArtifacts } from '@standardnotes/services'
|
||||
|
||||
/**
|
||||
* A challenge operation stores user-submitted values and callbacks.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isNullOrUndefined } from '@standardnotes/utils'
|
||||
import { Challenge } from './Challenge'
|
||||
import {
|
||||
Challenge,
|
||||
ChallengeResponseInterface,
|
||||
ChallengeValidation,
|
||||
ChallengeValue,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
AbstractService,
|
||||
ChallengeServiceInterface,
|
||||
InternalEventBusInterface,
|
||||
Challenge,
|
||||
ChallengeArtifacts,
|
||||
ChallengeReason,
|
||||
ChallengeValidation,
|
||||
@@ -17,7 +18,6 @@ import {
|
||||
} from '@standardnotes/services'
|
||||
import { ChallengeResponse } from './ChallengeResponse'
|
||||
import { ChallengeOperation } from './ChallengeOperation'
|
||||
import { Challenge } from './Challenge'
|
||||
|
||||
type ChallengeValidationResponse = {
|
||||
valid: boolean
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './Challenge'
|
||||
export * from './ChallengeOperation'
|
||||
export * from './ChallengeResponse'
|
||||
export * from './ChallengeService'
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
import { ItemInterface, SNComponent, SNFeatureRepo } from '@standardnotes/models'
|
||||
import { SNSyncService } from '../Sync/SyncService'
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
import {
|
||||
ItemManager,
|
||||
AlertService,
|
||||
SNApiService,
|
||||
UserService,
|
||||
SNSessionManager,
|
||||
DiskStorageService,
|
||||
StorageKey,
|
||||
} from '@Lib/index'
|
||||
import { SNFeaturesService } from '@Lib/Services/Features'
|
||||
import { ContentType, RoleName } from '@standardnotes/common'
|
||||
import { FeatureDescription, FeatureIdentifier, GetFeatures } from '@standardnotes/features'
|
||||
@@ -17,7 +8,16 @@ import { SNWebSocketsService } from '../Api/WebsocketsService'
|
||||
import { SNSettingsService } from '../Settings'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { convertTimestampToMilliseconds } from '@standardnotes/utils'
|
||||
import { FeatureStatus, InternalEventBusInterface } from '@standardnotes/services'
|
||||
import {
|
||||
AlertService,
|
||||
FeatureStatus,
|
||||
InternalEventBusInterface,
|
||||
StorageKey,
|
||||
UserService,
|
||||
} from '@standardnotes/services'
|
||||
import { SNApiService, SNSessionManager } from '../Api'
|
||||
import { ItemManager } from '../Items'
|
||||
import { DiskStorageService } from '../Storage/DiskStorageService'
|
||||
|
||||
describe('featuresService', () => {
|
||||
let storageService: DiskStorageService
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { AccountEvent, UserService } from '../User/UserService'
|
||||
import { SNApiService } from '../Api/ApiService'
|
||||
import {
|
||||
arraysEqual,
|
||||
@@ -24,12 +23,15 @@ import { TRUSTED_CUSTOM_EXTENSIONS_HOSTS, TRUSTED_FEATURE_HOSTS } from '@Lib/Hos
|
||||
import { UserRolesChangedEvent } from '@standardnotes/domain-events'
|
||||
import { UuidString } from '@Lib/Types/UuidString'
|
||||
import * as FeaturesImports from '@standardnotes/features'
|
||||
import * as Messages from '@Lib/Services/Api/Messages'
|
||||
import * as Models from '@standardnotes/models'
|
||||
import {
|
||||
AbstractService,
|
||||
AccountEvent,
|
||||
AlertService,
|
||||
ApiServiceEvent,
|
||||
API_MESSAGE_FAILED_DOWNLOADING_EXTENSION,
|
||||
API_MESSAGE_FAILED_OFFLINE_ACTIVATION,
|
||||
API_MESSAGE_UNTRUSTED_EXTENSIONS_WARNING,
|
||||
ApplicationStage,
|
||||
ButtonType,
|
||||
DiagnosticInfo,
|
||||
@@ -39,10 +41,12 @@ import {
|
||||
InternalEventBusInterface,
|
||||
InternalEventHandlerInterface,
|
||||
InternalEventInterface,
|
||||
INVALID_EXTENSION_URL,
|
||||
MetaReceivedData,
|
||||
OfflineSubscriptionEntitlements,
|
||||
SetOfflineFeaturesFunctionResponse,
|
||||
StorageKey,
|
||||
UserService,
|
||||
} from '@standardnotes/services'
|
||||
import { FeatureIdentifier } from '@standardnotes/features'
|
||||
|
||||
@@ -250,7 +254,7 @@ export class SNFeaturesService
|
||||
void this.syncService.sync()
|
||||
return this.downloadOfflineFeatures(offlineRepo)
|
||||
} catch (err) {
|
||||
return new ClientDisplayableError(Messages.API_MESSAGE_FAILED_OFFLINE_ACTIVATION)
|
||||
return new ClientDisplayableError(API_MESSAGE_FAILED_OFFLINE_ACTIVATION)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,7 +284,7 @@ export class SNFeaturesService
|
||||
extensionKey,
|
||||
}
|
||||
} catch (error) {
|
||||
return new ClientDisplayableError(Messages.API_MESSAGE_FAILED_OFFLINE_ACTIVATION)
|
||||
return new ClientDisplayableError(API_MESSAGE_FAILED_OFFLINE_ACTIVATION)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -631,7 +635,7 @@ export class SNFeaturesService
|
||||
const { host } = new URL(url)
|
||||
if (!trustedCustomExtensionsUrls.includes(host)) {
|
||||
const didConfirm = await this.alertService.confirm(
|
||||
Messages.API_MESSAGE_UNTRUSTED_EXTENSIONS_WARNING,
|
||||
API_MESSAGE_UNTRUSTED_EXTENSIONS_WARNING,
|
||||
'Install extension from an untrusted source?',
|
||||
'Proceed to install',
|
||||
ButtonType.Danger,
|
||||
@@ -644,7 +648,7 @@ export class SNFeaturesService
|
||||
return this.performDownloadExternalFeature(url)
|
||||
}
|
||||
} catch (err) {
|
||||
void this.alertService.alert(Messages.INVALID_EXTENSION_URL)
|
||||
void this.alertService.alert(INVALID_EXTENSION_URL)
|
||||
}
|
||||
|
||||
return undefined
|
||||
@@ -653,7 +657,7 @@ export class SNFeaturesService
|
||||
private async performDownloadExternalFeature(url: string): Promise<Models.SNComponent | undefined> {
|
||||
const response = await this.apiService.downloadFeatureUrl(url)
|
||||
if (response.error) {
|
||||
await this.alertService.alert(Messages.API_MESSAGE_FAILED_DOWNLOADING_EXTENSION)
|
||||
await this.alertService.alert(API_MESSAGE_FAILED_DOWNLOADING_EXTENSION)
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -683,14 +687,14 @@ export class SNFeaturesService
|
||||
|
||||
const nativeFeature = FeaturesImports.FindNativeFeature(rawFeature.identifier)
|
||||
if (nativeFeature) {
|
||||
await this.alertService.alert(Messages.API_MESSAGE_FAILED_DOWNLOADING_EXTENSION)
|
||||
await this.alertService.alert(API_MESSAGE_FAILED_DOWNLOADING_EXTENSION)
|
||||
return
|
||||
}
|
||||
|
||||
if (rawFeature.url) {
|
||||
for (const nativeFeature of FeaturesImports.GetFeatures()) {
|
||||
if (rawFeature.url.includes(nativeFeature.identifier)) {
|
||||
await this.alertService.alert(Messages.API_MESSAGE_FAILED_DOWNLOADING_EXTENSION)
|
||||
await this.alertService.alert(API_MESSAGE_FAILED_DOWNLOADING_EXTENSION)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { KeyRecoveryOperation } from './KeyRecoveryOperation'
|
||||
import { SNRootKeyParams, SNRootKey, KeyParamsFromApiResponse, KeyRecoveryStrings } from '@standardnotes/encryption'
|
||||
import { UserService } from '../User/UserService'
|
||||
import {
|
||||
isErrorDecryptingPayload,
|
||||
EncryptedPayloadInterface,
|
||||
@@ -14,7 +13,7 @@ import {
|
||||
import { SNSyncService } from '../Sync/SyncService'
|
||||
import { DiskStorageService } from '../Storage/DiskStorageService'
|
||||
import { PayloadManager } from '../Payloads/PayloadManager'
|
||||
import { Challenge, ChallengeService } from '../Challenge'
|
||||
import { ChallengeService } from '../Challenge'
|
||||
import { SNApiService } from '@Lib/Services/Api/ApiService'
|
||||
import { ContentType, Uuid } from '@standardnotes/common'
|
||||
import { ItemManager } from '../Items/ItemManager'
|
||||
@@ -32,6 +31,8 @@ import {
|
||||
ChallengeReason,
|
||||
ChallengePrompt,
|
||||
EncryptionService,
|
||||
Challenge,
|
||||
UserService,
|
||||
} from '@standardnotes/services'
|
||||
import {
|
||||
UndecryptableItemsStorage,
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
|
||||
import { SNSettingsService } from '../Settings'
|
||||
import * as messages from '../Api/Messages'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { SNFeaturesService } from '../Features/FeaturesService'
|
||||
import { FeatureIdentifier } from '@standardnotes/features'
|
||||
import { AbstractService, InternalEventBusInterface } from '@standardnotes/services'
|
||||
import { AbstractService, InternalEventBusInterface, SignInStrings } from '@standardnotes/services'
|
||||
|
||||
export class SNMfaService extends AbstractService {
|
||||
constructor(
|
||||
@@ -38,7 +37,7 @@ export class SNMfaService extends AbstractService {
|
||||
const otpTokenValid = otpToken != undefined && otpToken === (await this.getOtpToken(secret))
|
||||
|
||||
if (!otpTokenValid) {
|
||||
throw new Error(messages.SignInStrings.IncorrectMfa)
|
||||
throw new Error(SignInStrings.IncorrectMfa)
|
||||
}
|
||||
|
||||
return this.saveMfaSetting(secret)
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
ChallengePrompt,
|
||||
ChallengeReason,
|
||||
MutatorClientInterface,
|
||||
Challenge,
|
||||
InfoStrings,
|
||||
} from '@standardnotes/services'
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
@@ -18,7 +20,7 @@ import { SNProtectionService } from '../Protection/ProtectionService'
|
||||
import { SNSyncService } from '../Sync'
|
||||
import { Strings } from '../../Strings'
|
||||
import { TagsToFoldersMigrationApplicator } from '@Lib/Migrations/Applicators/TagsToFolders'
|
||||
import { Challenge, ChallengeService } from '../Challenge'
|
||||
import { ChallengeService } from '../Challenge'
|
||||
import {
|
||||
BackupFile,
|
||||
BackupFileDecryptedContextualPayload,
|
||||
@@ -170,7 +172,13 @@ export class MutatorService extends AbstractService implements MutatorClientInte
|
||||
items: I[],
|
||||
reason: ChallengeReason,
|
||||
): Promise<I[] | undefined> {
|
||||
if (!(await this.protectionService.authorizeAction(reason))) {
|
||||
if (
|
||||
!(await this.protectionService.authorizeAction(reason, {
|
||||
fallBackToAccountPassword: true,
|
||||
requireAccountPassword: false,
|
||||
forcePrompt: false,
|
||||
}))
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -314,13 +322,13 @@ export class MutatorService extends AbstractService implements MutatorClientInte
|
||||
|
||||
const supportedVersions = this.encryption.supportedVersions()
|
||||
if (!supportedVersions.includes(version)) {
|
||||
return { error: new ClientDisplayableError(Strings.Info.UnsupportedBackupFileVersion) }
|
||||
return { error: new ClientDisplayableError(InfoStrings.UnsupportedBackupFileVersion) }
|
||||
}
|
||||
|
||||
const userVersion = this.encryption.getUserVersion()
|
||||
if (userVersion && compareVersions(version, userVersion) === 1) {
|
||||
/** File was made with a greater version than the user's account */
|
||||
return { error: new ClientDisplayableError(Strings.Info.BackupFileMoreRecentThanAccount) }
|
||||
return { error: new ClientDisplayableError(InfoStrings.BackupFileMoreRecentThanAccount) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { ChallengeReason } from '@standardnotes/services'
|
||||
import { DecryptedItem } from '@standardnotes/models'
|
||||
import { TimingDisplayOption, MobileUnlockTiming } from './MobileUnlockTiming'
|
||||
|
||||
export interface ProtectionsClientInterface {
|
||||
authorizeProtectedActionForItems<T extends DecryptedItem>(files: T[], challengeReason: ChallengeReason): Promise<T[]>
|
||||
authorizeItemAccess(item: DecryptedItem): Promise<boolean>
|
||||
getMobileBiometricsTiming(): MobileUnlockTiming | undefined
|
||||
getMobilePasscodeTiming(): MobileUnlockTiming | undefined
|
||||
setMobileBiometricsTiming(timing: MobileUnlockTiming): void
|
||||
setMobilePasscodeTiming(timing: MobileUnlockTiming): void
|
||||
setMobileScreenshotPrivacyEnabled(isEnabled: boolean): void
|
||||
getMobileScreenshotPrivacyEnabled(): boolean
|
||||
getMobilePasscodeTimingOptions(): TimingDisplayOption[]
|
||||
getMobileBiometricsTimingOptions(): TimingDisplayOption[]
|
||||
hasBiometricsEnabled(): boolean
|
||||
enableBiometrics(): boolean
|
||||
disableBiometrics(): Promise<boolean>
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
export enum MobileUnlockTiming {
|
||||
Immediately = 'immediately',
|
||||
OnQuit = 'on-quit',
|
||||
}
|
||||
|
||||
export type TimingDisplayOption = {
|
||||
title: string
|
||||
key: MobileUnlockTiming
|
||||
selected: boolean
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Challenge } from './../Challenge/Challenge'
|
||||
import { ChallengeService } from './../Challenge/ChallengeService'
|
||||
import { SNLog } from '@Lib/Log'
|
||||
import { DecryptedItem } from '@standardnotes/models'
|
||||
@@ -11,14 +10,16 @@ import {
|
||||
ApplicationStage,
|
||||
StorageKey,
|
||||
DiagnosticInfo,
|
||||
Challenge,
|
||||
ChallengeReason,
|
||||
ChallengePrompt,
|
||||
ChallengeValidation,
|
||||
EncryptionService,
|
||||
MobileUnlockTiming,
|
||||
TimingDisplayOption,
|
||||
ProtectionsClientInterface,
|
||||
} from '@standardnotes/services'
|
||||
import { ProtectionsClientInterface } from './ClientInterface'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { MobileUnlockTiming, TimingDisplayOption } from './MobileUnlockTiming'
|
||||
|
||||
export enum ProtectionEvent {
|
||||
UnprotectedSessionBegan = 'UnprotectedSessionBegan',
|
||||
@@ -176,62 +177,95 @@ export class SNProtectionService extends AbstractService<ProtectionEvent> implem
|
||||
item.content_type === ContentType.Note
|
||||
? ChallengeReason.AccessProtectedNote
|
||||
: ChallengeReason.AccessProtectedFile,
|
||||
{ fallBackToAccountPassword: true, requireAccountPassword: false, forcePrompt: false },
|
||||
)
|
||||
}
|
||||
|
||||
authorizeAddingPasscode(): Promise<boolean> {
|
||||
return this.authorizeAction(ChallengeReason.AddPasscode)
|
||||
return this.authorizeAction(ChallengeReason.AddPasscode, {
|
||||
fallBackToAccountPassword: true,
|
||||
requireAccountPassword: false,
|
||||
forcePrompt: false,
|
||||
})
|
||||
}
|
||||
|
||||
authorizeChangingPasscode(): Promise<boolean> {
|
||||
return this.authorizeAction(ChallengeReason.ChangePasscode)
|
||||
return this.authorizeAction(ChallengeReason.ChangePasscode, {
|
||||
fallBackToAccountPassword: true,
|
||||
requireAccountPassword: false,
|
||||
forcePrompt: false,
|
||||
})
|
||||
}
|
||||
|
||||
authorizeRemovingPasscode(): Promise<boolean> {
|
||||
return this.authorizeAction(ChallengeReason.RemovePasscode)
|
||||
return this.authorizeAction(ChallengeReason.RemovePasscode, {
|
||||
fallBackToAccountPassword: true,
|
||||
requireAccountPassword: false,
|
||||
forcePrompt: false,
|
||||
})
|
||||
}
|
||||
|
||||
authorizeSearchingProtectedNotesText(): Promise<boolean> {
|
||||
return this.authorizeAction(ChallengeReason.SearchProtectedNotesText)
|
||||
return this.authorizeAction(ChallengeReason.SearchProtectedNotesText, {
|
||||
fallBackToAccountPassword: true,
|
||||
requireAccountPassword: false,
|
||||
forcePrompt: false,
|
||||
})
|
||||
}
|
||||
|
||||
authorizeFileImport(): Promise<boolean> {
|
||||
return this.authorizeAction(ChallengeReason.ImportFile)
|
||||
return this.authorizeAction(ChallengeReason.ImportFile, {
|
||||
fallBackToAccountPassword: true,
|
||||
requireAccountPassword: false,
|
||||
forcePrompt: false,
|
||||
})
|
||||
}
|
||||
|
||||
async authorizeBackupCreation(): Promise<boolean> {
|
||||
return this.authorizeAction(ChallengeReason.ExportBackup, {
|
||||
fallBackToAccountPassword: true,
|
||||
requireAccountPassword: false,
|
||||
forcePrompt: false,
|
||||
})
|
||||
}
|
||||
|
||||
async authorizeMfaDisable(): Promise<boolean> {
|
||||
return this.authorizeAction(ChallengeReason.DisableMfa, {
|
||||
fallBackToAccountPassword: true,
|
||||
requireAccountPassword: true,
|
||||
forcePrompt: false,
|
||||
})
|
||||
}
|
||||
|
||||
async authorizeAutolockIntervalChange(): Promise<boolean> {
|
||||
return this.authorizeAction(ChallengeReason.ChangeAutolockInterval)
|
||||
return this.authorizeAction(ChallengeReason.ChangeAutolockInterval, {
|
||||
fallBackToAccountPassword: true,
|
||||
requireAccountPassword: false,
|
||||
forcePrompt: false,
|
||||
})
|
||||
}
|
||||
|
||||
async authorizeSessionRevoking(): Promise<boolean> {
|
||||
return this.authorizeAction(ChallengeReason.RevokeSession)
|
||||
return this.authorizeAction(ChallengeReason.RevokeSession, {
|
||||
fallBackToAccountPassword: true,
|
||||
requireAccountPassword: false,
|
||||
forcePrompt: false,
|
||||
})
|
||||
}
|
||||
|
||||
async authorizeListedPublishing(): Promise<boolean> {
|
||||
return this.authorizeAction(ChallengeReason.AuthorizeNoteForListed, { forcePrompt: true })
|
||||
return this.authorizeAction(ChallengeReason.AuthorizeNoteForListed, {
|
||||
fallBackToAccountPassword: true,
|
||||
requireAccountPassword: false,
|
||||
forcePrompt: true,
|
||||
})
|
||||
}
|
||||
|
||||
async authorizeAction(
|
||||
reason: ChallengeReason,
|
||||
{ fallBackToAccountPassword = true, requireAccountPassword = false, forcePrompt = false } = {},
|
||||
dto: { fallBackToAccountPassword: boolean; requireAccountPassword: boolean; forcePrompt: boolean },
|
||||
): Promise<boolean> {
|
||||
return this.validateOrRenewSession(reason, {
|
||||
requireAccountPassword,
|
||||
fallBackToAccountPassword,
|
||||
forcePrompt,
|
||||
})
|
||||
return this.validateOrRenewSession(reason, dto)
|
||||
}
|
||||
|
||||
getMobilePasscodeTimingOptions(): TimingDisplayOption[] {
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
export * from './ClientInterface'
|
||||
export * from './ProtectionService'
|
||||
export * from './MobileUnlockTiming'
|
||||
|
||||
@@ -10,6 +10,18 @@ import {
|
||||
ChallengeReason,
|
||||
ChallengePromptTitle,
|
||||
EncryptionService,
|
||||
SessionsClientInterface,
|
||||
SessionManagerResponse,
|
||||
SessionStrings,
|
||||
SignInStrings,
|
||||
INVALID_PASSWORD_COST,
|
||||
API_MESSAGE_FALLBACK_LOGIN_FAIL,
|
||||
API_MESSAGE_GENERIC_SYNC_FAIL,
|
||||
EXPIRED_PROTOCOL_VERSION,
|
||||
StrictSignInFailed,
|
||||
UNSUPPORTED_KEY_DERIVATION,
|
||||
UNSUPPORTED_PROTOCOL_VERSION,
|
||||
Challenge,
|
||||
} from '@standardnotes/services'
|
||||
import { Base64String } from '@standardnotes/sncrypto-common'
|
||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
@@ -17,11 +29,9 @@ import { CopyPayloadWithContentOverride } from '@standardnotes/models'
|
||||
import { isNullOrUndefined } from '@standardnotes/utils'
|
||||
import { JwtSession } from './Sessions/JwtSession'
|
||||
import { KeyParamsFromApiResponse, SNRootKeyParams, SNRootKey, CreateNewRootKey } from '@standardnotes/encryption'
|
||||
import { SessionStrings, SignInStrings } from '../Api/Messages'
|
||||
import { RemoteSession, RawStorageValue } from './Sessions/Types'
|
||||
import { Session } from './Sessions/Session'
|
||||
import { SessionFromRawStorageValue } from './Sessions/Generator'
|
||||
import { SessionsClientInterface } from './SessionsClientInterface'
|
||||
import { ShareToken } from './ShareToken'
|
||||
import { SNApiService } from '../Api/ApiService'
|
||||
import { DiskStorageService } from '../Storage/DiskStorageService'
|
||||
@@ -31,9 +41,8 @@ import { Subscription } from '@standardnotes/security'
|
||||
import { TokenSession } from './Sessions/TokenSession'
|
||||
import { UuidString } from '@Lib/Types/UuidString'
|
||||
import * as Common from '@standardnotes/common'
|
||||
import * as Messages from '../Api/Messages'
|
||||
import * as Responses from '@standardnotes/responses'
|
||||
import { Challenge, ChallengeService } from '../Challenge'
|
||||
import { ChallengeService } from '../Challenge'
|
||||
import {
|
||||
ApiCallError,
|
||||
ErrorMessage,
|
||||
@@ -46,12 +55,6 @@ import {
|
||||
export const MINIMUM_PASSWORD_LENGTH = 8
|
||||
export const MissingAccountParams = 'missing-params'
|
||||
|
||||
type SessionManagerResponse = {
|
||||
response: Responses.HttpResponse
|
||||
rootKey?: SNRootKey
|
||||
keyParams?: Common.AnyKeyParamsContent
|
||||
}
|
||||
|
||||
const cleanedEmailString = (email: string) => {
|
||||
return email.trim().toLowerCase()
|
||||
}
|
||||
@@ -338,7 +341,7 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
||||
const keyParams = KeyParamsFromApiResponse(response as Responses.KeyParamsResponse, email)
|
||||
if (!keyParams || !keyParams.version) {
|
||||
return {
|
||||
response: this.apiService.createErrorResponse(Messages.API_MESSAGE_FALLBACK_LOGIN_FAIL),
|
||||
response: this.apiService.createErrorResponse(API_MESSAGE_FALLBACK_LOGIN_FAIL),
|
||||
}
|
||||
}
|
||||
return { keyParams, response, mfaKeyPath, mfaCode }
|
||||
@@ -388,11 +391,11 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
||||
if (!this.protocolService.supportedVersions().includes(keyParams.version)) {
|
||||
if (this.protocolService.isVersionNewerThanLibraryVersion(keyParams.version)) {
|
||||
return {
|
||||
response: this.apiService.createErrorResponse(Messages.UNSUPPORTED_PROTOCOL_VERSION),
|
||||
response: this.apiService.createErrorResponse(UNSUPPORTED_PROTOCOL_VERSION),
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
response: this.apiService.createErrorResponse(Messages.EXPIRED_PROTOCOL_VERSION),
|
||||
response: this.apiService.createErrorResponse(EXPIRED_PROTOCOL_VERSION),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -402,7 +405,7 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
||||
const minimum = this.protocolService.costMinimumForVersion(keyParams.version)
|
||||
if (keyParams.content002.pw_cost < minimum) {
|
||||
return {
|
||||
response: this.apiService.createErrorResponse(Messages.INVALID_PASSWORD_COST),
|
||||
response: this.apiService.createErrorResponse(INVALID_PASSWORD_COST),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,14 +418,14 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
||||
|
||||
if (!confirmed) {
|
||||
return {
|
||||
response: this.apiService.createErrorResponse(Messages.API_MESSAGE_FALLBACK_LOGIN_FAIL),
|
||||
response: this.apiService.createErrorResponse(API_MESSAGE_FALLBACK_LOGIN_FAIL),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.protocolService.platformSupportsKeyDerivation(keyParams)) {
|
||||
return {
|
||||
response: this.apiService.createErrorResponse(Messages.UNSUPPORTED_KEY_DERIVATION),
|
||||
response: this.apiService.createErrorResponse(UNSUPPORTED_KEY_DERIVATION),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,9 +436,7 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
||||
if (!isNullOrUndefined(minAllowedVersion)) {
|
||||
if (!Common.leftVersionGreaterThanOrEqualToRight(keyParams.version, minAllowedVersion)) {
|
||||
return {
|
||||
response: this.apiService.createErrorResponse(
|
||||
Messages.StrictSignInFailed(keyParams.version, minAllowedVersion),
|
||||
),
|
||||
response: this.apiService.createErrorResponse(StrictSignInFailed(keyParams.version, minAllowedVersion)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -532,7 +533,7 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
||||
public async revokeAllOtherSessions(): Promise<void> {
|
||||
const response = await this.getSessionsList()
|
||||
if (response.error != undefined || response.data == undefined) {
|
||||
throw new Error(response.error?.message ?? Messages.API_MESSAGE_GENERIC_SYNC_FAIL)
|
||||
throw new Error(response.error?.message ?? API_MESSAGE_GENERIC_SYNC_FAIL)
|
||||
}
|
||||
const sessions = response.data as RemoteSession[]
|
||||
const otherSessions = sessions.filter((session) => !session.current)
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { ClientDisplayableError, User } from '@standardnotes/responses'
|
||||
import { Base64String } from '@standardnotes/sncrypto-common'
|
||||
|
||||
export interface SessionsClientInterface {
|
||||
createDemoShareToken(): Promise<Base64String | ClientDisplayableError>
|
||||
populateSessionFromDemoShareToken(token: Base64String): Promise<void>
|
||||
getUser(): User | undefined
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './SessionManager'
|
||||
export * from './Sessions'
|
||||
export * from './SessionsClientInterface'
|
||||
export * from './ShareToken'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { SettingsList } from './SettingsList'
|
||||
import { SettingName, SensitiveSettingName, SubscriptionSettingName } from '@standardnotes/settings'
|
||||
import * as messages from '../Api/Messages'
|
||||
import { API_MESSAGE_INVALID_SESSION } from '@standardnotes/services'
|
||||
import { StatusCode, User } from '@standardnotes/responses'
|
||||
import { SettingsServerInterface } from './SettingsServerInterface'
|
||||
|
||||
@@ -25,7 +25,7 @@ export class SettingsGateway {
|
||||
private get userUuid() {
|
||||
const user = this.getUser()
|
||||
if (user == undefined || user.uuid == undefined) {
|
||||
throw new Error(messages.API_MESSAGE_INVALID_SESSION)
|
||||
throw new Error(API_MESSAGE_INVALID_SESSION)
|
||||
}
|
||||
return user.uuid
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { HttpResponse, MinimalHttpResponse } from '@standardnotes/responses'
|
||||
|
||||
export interface UserServerInterface {
|
||||
deleteAccount(userUuid: string): Promise<HttpResponse | MinimalHttpResponse>
|
||||
}
|
||||
@@ -1,587 +0,0 @@
|
||||
import { Challenge } from '../Challenge'
|
||||
import { ChallengeService } from '../Challenge/ChallengeService'
|
||||
import { SNRootKey, SNRootKeyParams } from '@standardnotes/encryption'
|
||||
import { HttpResponse, SignInResponse, User } from '@standardnotes/responses'
|
||||
import { ItemManager } from '@Lib/Services/Items/ItemManager'
|
||||
import { KeyParamsOrigination } from '@standardnotes/common'
|
||||
import {
|
||||
AbstractService,
|
||||
AlertService,
|
||||
ChallengePrompt,
|
||||
ChallengeReason,
|
||||
ChallengeValidation,
|
||||
DeinitSource,
|
||||
InternalEventBusInterface,
|
||||
UserClientInterface,
|
||||
StoragePersistencePolicies,
|
||||
EncryptionService,
|
||||
} from '@standardnotes/services'
|
||||
import { SNApiService } from './../Api/ApiService'
|
||||
import { SNProtectionService } from '../Protection/ProtectionService'
|
||||
import { SNSessionManager, MINIMUM_PASSWORD_LENGTH } from '../Session/SessionManager'
|
||||
import { DiskStorageService } from '@Lib/Services/Storage/DiskStorageService'
|
||||
import { SNSyncService } from '../Sync/SyncService'
|
||||
import { Strings } from '../../Strings/index'
|
||||
import { UuidGenerator } from '@standardnotes/utils'
|
||||
import * as Messages from '../Api/Messages'
|
||||
import { UserRegistrationResponseBody } from '@standardnotes/api'
|
||||
|
||||
const MINIMUM_PASSCODE_LENGTH = 1
|
||||
|
||||
export type CredentialsChangeFunctionResponse = { error?: { message: string } }
|
||||
export type AccountServiceResponse = HttpResponse
|
||||
|
||||
export enum AccountEvent {
|
||||
SignedInOrRegistered = 'SignedInOrRegistered',
|
||||
SignedOut = 'SignedOut',
|
||||
}
|
||||
|
||||
type AccountEventData = {
|
||||
source: DeinitSource
|
||||
}
|
||||
|
||||
export class UserService extends AbstractService<AccountEvent, AccountEventData> implements UserClientInterface {
|
||||
private signingIn = false
|
||||
private registering = false
|
||||
|
||||
constructor(
|
||||
private sessionManager: SNSessionManager,
|
||||
private syncService: SNSyncService,
|
||||
private storageService: DiskStorageService,
|
||||
private itemManager: ItemManager,
|
||||
private protocolService: EncryptionService,
|
||||
private alertService: AlertService,
|
||||
private challengeService: ChallengeService,
|
||||
private protectionService: SNProtectionService,
|
||||
private apiService: SNApiService,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(internalEventBus)
|
||||
}
|
||||
|
||||
public override deinit(): void {
|
||||
super.deinit()
|
||||
;(this.sessionManager as unknown) = undefined
|
||||
;(this.syncService as unknown) = undefined
|
||||
;(this.storageService as unknown) = undefined
|
||||
;(this.itemManager as unknown) = undefined
|
||||
;(this.protocolService as unknown) = undefined
|
||||
;(this.alertService as unknown) = undefined
|
||||
;(this.challengeService as unknown) = undefined
|
||||
;(this.protectionService as unknown) = undefined
|
||||
;(this.apiService as unknown) = undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mergeLocal Whether to merge existing offline data into account. If false,
|
||||
* any pre-existing data will be fully deleted upon success.
|
||||
*/
|
||||
public async register(
|
||||
email: string,
|
||||
password: string,
|
||||
ephemeral = false,
|
||||
mergeLocal = true,
|
||||
): Promise<UserRegistrationResponseBody> {
|
||||
if (this.protocolService.hasAccount()) {
|
||||
throw Error('Tried to register when an account already exists.')
|
||||
}
|
||||
|
||||
if (this.registering) {
|
||||
throw Error('Already registering.')
|
||||
}
|
||||
|
||||
this.registering = true
|
||||
|
||||
try {
|
||||
this.lockSyncing()
|
||||
const response = await this.sessionManager.register(email, password, ephemeral)
|
||||
|
||||
this.syncService.resetSyncState()
|
||||
|
||||
await this.storageService.setPersistencePolicy(
|
||||
ephemeral ? StoragePersistencePolicies.Ephemeral : StoragePersistencePolicies.Default,
|
||||
)
|
||||
|
||||
if (mergeLocal) {
|
||||
await this.syncService.markAllItemsAsNeedingSyncAndPersist()
|
||||
} else {
|
||||
await this.itemManager.removeAllItemsFromMemory()
|
||||
await this.clearDatabase()
|
||||
}
|
||||
|
||||
await this.notifyEvent(AccountEvent.SignedInOrRegistered)
|
||||
|
||||
this.unlockSyncing()
|
||||
this.registering = false
|
||||
|
||||
await this.syncService.downloadFirstSync(300)
|
||||
void this.protocolService.decryptErroredPayloads()
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
this.unlockSyncing()
|
||||
this.registering = false
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mergeLocal Whether to merge existing offline data into account.
|
||||
* If false, any pre-existing data will be fully deleted upon success.
|
||||
*/
|
||||
public async signIn(
|
||||
email: string,
|
||||
password: string,
|
||||
strict = false,
|
||||
ephemeral = false,
|
||||
mergeLocal = true,
|
||||
awaitSync = false,
|
||||
): Promise<AccountServiceResponse> {
|
||||
if (this.protocolService.hasAccount()) {
|
||||
throw Error('Tried to sign in when an account already exists.')
|
||||
}
|
||||
|
||||
if (this.signingIn) {
|
||||
throw Error('Already signing in.')
|
||||
}
|
||||
|
||||
this.signingIn = true
|
||||
|
||||
try {
|
||||
/** Prevent a timed sync from occuring while signing in. */
|
||||
this.lockSyncing()
|
||||
|
||||
const result = await this.sessionManager.signIn(email, password, strict, ephemeral)
|
||||
|
||||
if (!result.response.error) {
|
||||
this.syncService.resetSyncState()
|
||||
|
||||
await this.storageService.setPersistencePolicy(
|
||||
ephemeral ? StoragePersistencePolicies.Ephemeral : StoragePersistencePolicies.Default,
|
||||
)
|
||||
|
||||
if (mergeLocal) {
|
||||
await this.syncService.markAllItemsAsNeedingSyncAndPersist()
|
||||
} else {
|
||||
void this.itemManager.removeAllItemsFromMemory()
|
||||
await this.clearDatabase()
|
||||
}
|
||||
|
||||
await this.notifyEvent(AccountEvent.SignedInOrRegistered)
|
||||
|
||||
this.unlockSyncing()
|
||||
|
||||
const syncPromise = this.syncService
|
||||
.downloadFirstSync(1_000, {
|
||||
checkIntegrity: true,
|
||||
awaitAll: awaitSync,
|
||||
})
|
||||
.then(() => {
|
||||
if (!awaitSync) {
|
||||
void this.protocolService.decryptErroredPayloads()
|
||||
}
|
||||
})
|
||||
|
||||
if (awaitSync) {
|
||||
await syncPromise
|
||||
|
||||
await this.protocolService.decryptErroredPayloads()
|
||||
}
|
||||
} else {
|
||||
this.unlockSyncing()
|
||||
}
|
||||
|
||||
return result.response
|
||||
} finally {
|
||||
this.signingIn = false
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteAccount(): Promise<{
|
||||
error: boolean
|
||||
message?: string
|
||||
}> {
|
||||
if (
|
||||
!(await this.protectionService.authorizeAction(ChallengeReason.DeleteAccount, {
|
||||
requireAccountPassword: true,
|
||||
}))
|
||||
) {
|
||||
return {
|
||||
error: true,
|
||||
message: Messages.INVALID_PASSWORD,
|
||||
}
|
||||
}
|
||||
|
||||
const uuid = this.sessionManager.getSureUser().uuid
|
||||
const response = await this.apiService.deleteAccount(uuid)
|
||||
if (response.error) {
|
||||
return {
|
||||
error: true,
|
||||
message: response.error.message,
|
||||
}
|
||||
}
|
||||
|
||||
await this.signOut(true)
|
||||
|
||||
void this.alertService.alert(Strings.Info.AccountDeleted)
|
||||
|
||||
return {
|
||||
error: false,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A sign in request that occurs while the user was previously signed in, to correct
|
||||
* for missing keys or storage values. Unlike regular sign in, this doesn't worry about
|
||||
* performing one of marking all items as needing sync or deleting all local data.
|
||||
*/
|
||||
public async correctiveSignIn(rootKey: SNRootKey): Promise<HttpResponse | SignInResponse> {
|
||||
this.lockSyncing()
|
||||
|
||||
const response = await this.sessionManager.bypassChecksAndSignInWithRootKey(rootKey.keyParams.identifier, rootKey)
|
||||
|
||||
if (!response.error) {
|
||||
await this.notifyEvent(AccountEvent.SignedInOrRegistered)
|
||||
|
||||
this.unlockSyncing()
|
||||
|
||||
void this.syncService.downloadFirstSync(1_000, {
|
||||
checkIntegrity: true,
|
||||
})
|
||||
|
||||
void this.protocolService.decryptErroredPayloads()
|
||||
}
|
||||
|
||||
this.unlockSyncing()
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* @param passcode - Changing the account password or email requires the local
|
||||
* passcode if configured (to rewrap the account key with passcode). If the passcode
|
||||
* is not passed in, the user will be prompted for the passcode. However if the consumer
|
||||
* already has reference to the passcode, they can pass it in here so that the user
|
||||
* is not prompted again.
|
||||
*/
|
||||
public async changeCredentials(parameters: {
|
||||
currentPassword: string
|
||||
origination: KeyParamsOrigination
|
||||
validateNewPasswordStrength: boolean
|
||||
newEmail?: string
|
||||
newPassword?: string
|
||||
passcode?: string
|
||||
}): Promise<CredentialsChangeFunctionResponse> {
|
||||
const result = await this.performCredentialsChange(parameters)
|
||||
if (result.error) {
|
||||
void this.alertService.alert(result.error.message)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public async signOut(force = false, source = DeinitSource.SignOut): Promise<void> {
|
||||
const performSignOut = async () => {
|
||||
await this.sessionManager.signOut()
|
||||
await this.protocolService.deleteWorkspaceSpecificKeyStateFromDevice()
|
||||
await this.storageService.clearAllData()
|
||||
await this.notifyEvent(AccountEvent.SignedOut, { source })
|
||||
}
|
||||
|
||||
if (force) {
|
||||
await performSignOut()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const dirtyItems = this.itemManager.getDirtyItems()
|
||||
if (dirtyItems.length > 0) {
|
||||
const singular = dirtyItems.length === 1
|
||||
const didConfirm = await this.alertService.confirm(
|
||||
`There ${singular ? 'is' : 'are'} ${dirtyItems.length} ${
|
||||
singular ? 'item' : 'items'
|
||||
} with unsynced changes. If you sign out, these changes will be lost forever. Are you sure you want to sign out?`,
|
||||
)
|
||||
if (didConfirm) {
|
||||
await performSignOut()
|
||||
}
|
||||
} else {
|
||||
await performSignOut()
|
||||
}
|
||||
}
|
||||
|
||||
public async performProtocolUpgrade(): Promise<{
|
||||
success?: true
|
||||
canceled?: true
|
||||
error?: { message: string }
|
||||
}> {
|
||||
const hasPasscode = this.protocolService.hasPasscode()
|
||||
const hasAccount = this.protocolService.hasAccount()
|
||||
const prompts = []
|
||||
if (hasPasscode) {
|
||||
prompts.push(
|
||||
new ChallengePrompt(
|
||||
ChallengeValidation.LocalPasscode,
|
||||
undefined,
|
||||
Messages.ChallengeStrings.LocalPasscodePlaceholder,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (hasAccount) {
|
||||
prompts.push(
|
||||
new ChallengePrompt(
|
||||
ChallengeValidation.AccountPassword,
|
||||
undefined,
|
||||
Messages.ChallengeStrings.AccountPasswordPlaceholder,
|
||||
),
|
||||
)
|
||||
}
|
||||
const challenge = new Challenge(prompts, ChallengeReason.ProtocolUpgrade, true)
|
||||
const response = await this.challengeService.promptForChallengeResponse(challenge)
|
||||
if (!response) {
|
||||
return { canceled: true }
|
||||
}
|
||||
const dismissBlockingDialog = await this.alertService.blockingDialog(
|
||||
Messages.DO_NOT_CLOSE_APPLICATION,
|
||||
Messages.UPGRADING_ENCRYPTION,
|
||||
)
|
||||
try {
|
||||
let passcode: string | undefined
|
||||
if (hasPasscode) {
|
||||
/* Upgrade passcode version */
|
||||
const value = response.getValueForType(ChallengeValidation.LocalPasscode)
|
||||
passcode = value.value as string
|
||||
}
|
||||
if (hasAccount) {
|
||||
/* Upgrade account version */
|
||||
const value = response.getValueForType(ChallengeValidation.AccountPassword)
|
||||
const password = value.value as string
|
||||
const changeResponse = await this.changeCredentials({
|
||||
currentPassword: password,
|
||||
newPassword: password,
|
||||
passcode,
|
||||
origination: KeyParamsOrigination.ProtocolUpgrade,
|
||||
validateNewPasswordStrength: false,
|
||||
})
|
||||
if (changeResponse?.error) {
|
||||
return { error: changeResponse.error }
|
||||
}
|
||||
}
|
||||
if (hasPasscode) {
|
||||
/* Upgrade passcode version */
|
||||
await this.removePasscodeWithoutWarning()
|
||||
await this.setPasscodeWithoutWarning(passcode as string, KeyParamsOrigination.ProtocolUpgrade)
|
||||
}
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
return { error: error as Error }
|
||||
} finally {
|
||||
dismissBlockingDialog()
|
||||
}
|
||||
}
|
||||
|
||||
public async addPasscode(passcode: string): Promise<boolean> {
|
||||
if (passcode.length < MINIMUM_PASSCODE_LENGTH) {
|
||||
return false
|
||||
}
|
||||
if (!(await this.protectionService.authorizeAddingPasscode())) {
|
||||
return false
|
||||
}
|
||||
|
||||
const dismissBlockingDialog = await this.alertService.blockingDialog(
|
||||
Messages.DO_NOT_CLOSE_APPLICATION,
|
||||
Messages.SETTING_PASSCODE,
|
||||
)
|
||||
try {
|
||||
await this.setPasscodeWithoutWarning(passcode, KeyParamsOrigination.PasscodeCreate)
|
||||
return true
|
||||
} finally {
|
||||
dismissBlockingDialog()
|
||||
}
|
||||
}
|
||||
|
||||
public async removePasscode(): Promise<boolean> {
|
||||
if (!(await this.protectionService.authorizeRemovingPasscode())) {
|
||||
return false
|
||||
}
|
||||
|
||||
const dismissBlockingDialog = await this.alertService.blockingDialog(
|
||||
Messages.DO_NOT_CLOSE_APPLICATION,
|
||||
Messages.REMOVING_PASSCODE,
|
||||
)
|
||||
try {
|
||||
await this.removePasscodeWithoutWarning()
|
||||
return true
|
||||
} finally {
|
||||
dismissBlockingDialog()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns whether the passcode was successfuly changed or not
|
||||
*/
|
||||
public async changePasscode(
|
||||
newPasscode: string,
|
||||
origination = KeyParamsOrigination.PasscodeChange,
|
||||
): Promise<boolean> {
|
||||
if (newPasscode.length < MINIMUM_PASSCODE_LENGTH) {
|
||||
return false
|
||||
}
|
||||
if (!(await this.protectionService.authorizeChangingPasscode())) {
|
||||
return false
|
||||
}
|
||||
|
||||
const dismissBlockingDialog = await this.alertService.blockingDialog(
|
||||
Messages.DO_NOT_CLOSE_APPLICATION,
|
||||
origination === KeyParamsOrigination.ProtocolUpgrade
|
||||
? Messages.ProtocolUpgradeStrings.UpgradingPasscode
|
||||
: Messages.CHANGING_PASSCODE,
|
||||
)
|
||||
try {
|
||||
await this.removePasscodeWithoutWarning()
|
||||
await this.setPasscodeWithoutWarning(newPasscode, origination)
|
||||
return true
|
||||
} finally {
|
||||
dismissBlockingDialog()
|
||||
}
|
||||
}
|
||||
|
||||
private async setPasscodeWithoutWarning(passcode: string, origination: KeyParamsOrigination) {
|
||||
const identifier = UuidGenerator.GenerateUuid()
|
||||
const key = await this.protocolService.createRootKey(identifier, passcode, origination)
|
||||
await this.protocolService.setNewRootKeyWrapper(key)
|
||||
await this.rewriteItemsKeys()
|
||||
await this.syncService.sync()
|
||||
}
|
||||
|
||||
private async removePasscodeWithoutWarning() {
|
||||
await this.protocolService.removePasscode()
|
||||
await this.rewriteItemsKeys()
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows items keys to be rewritten to local db on local credential status change,
|
||||
* such as if passcode is added, changed, or removed.
|
||||
* This allows IndexedDB unencrypted logs to be deleted
|
||||
* `deletePayloads` will remove data from backing store,
|
||||
* but not from working memory See:
|
||||
* https://github.com/standardnotes/desktop/issues/131
|
||||
*/
|
||||
private async rewriteItemsKeys(): Promise<void> {
|
||||
const itemsKeys = this.itemManager.getDisplayableItemsKeys()
|
||||
const payloads = itemsKeys.map((key) => key.payloadRepresentation())
|
||||
await this.storageService.forceDeletePayloads(payloads)
|
||||
await this.syncService.persistPayloads(payloads)
|
||||
}
|
||||
|
||||
private lockSyncing(): void {
|
||||
this.syncService.lockSyncing()
|
||||
}
|
||||
|
||||
private unlockSyncing(): void {
|
||||
this.syncService.unlockSyncing()
|
||||
}
|
||||
|
||||
private clearDatabase(): Promise<void> {
|
||||
return this.storageService.clearAllPayloads()
|
||||
}
|
||||
|
||||
private async performCredentialsChange(parameters: {
|
||||
currentPassword: string
|
||||
origination: KeyParamsOrigination
|
||||
validateNewPasswordStrength: boolean
|
||||
newEmail?: string
|
||||
newPassword?: string
|
||||
passcode?: string
|
||||
}): Promise<CredentialsChangeFunctionResponse> {
|
||||
const { wrappingKey, canceled } = await this.challengeService.getWrappingKeyIfApplicable(parameters.passcode)
|
||||
|
||||
if (canceled) {
|
||||
return { error: Error(Messages.CredentialsChangeStrings.PasscodeRequired) }
|
||||
}
|
||||
|
||||
if (parameters.newPassword !== undefined && parameters.validateNewPasswordStrength) {
|
||||
if (parameters.newPassword.length < MINIMUM_PASSWORD_LENGTH) {
|
||||
return {
|
||||
error: Error(Messages.InsufficientPasswordMessage(MINIMUM_PASSWORD_LENGTH)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const accountPasswordValidation = await this.protocolService.validateAccountPassword(parameters.currentPassword)
|
||||
if (!accountPasswordValidation.valid) {
|
||||
return {
|
||||
error: Error(Messages.INVALID_PASSWORD),
|
||||
}
|
||||
}
|
||||
|
||||
const user = this.sessionManager.getUser() as User
|
||||
const currentEmail = user.email
|
||||
const rootKeys = await this.recomputeRootKeysForCredentialChange({
|
||||
currentPassword: parameters.currentPassword,
|
||||
currentEmail,
|
||||
origination: parameters.origination,
|
||||
newEmail: parameters.newEmail,
|
||||
newPassword: parameters.newPassword,
|
||||
})
|
||||
|
||||
this.lockSyncing()
|
||||
|
||||
/** Now, change the credentials on the server. Roll back on failure */
|
||||
const result = await this.sessionManager.changeCredentials({
|
||||
currentServerPassword: rootKeys.currentRootKey.serverPassword as string,
|
||||
newRootKey: rootKeys.newRootKey,
|
||||
wrappingKey,
|
||||
newEmail: parameters.newEmail,
|
||||
})
|
||||
|
||||
this.unlockSyncing()
|
||||
|
||||
if (!result.response.error) {
|
||||
const rollback = await this.protocolService.createNewItemsKeyWithRollback()
|
||||
await this.protocolService.reencryptItemsKeys()
|
||||
await this.syncService.sync({ awaitAll: true })
|
||||
|
||||
const defaultItemsKey = this.protocolService.getSureDefaultItemsKey()
|
||||
const itemsKeyWasSynced = !defaultItemsKey.neverSynced
|
||||
|
||||
if (!itemsKeyWasSynced) {
|
||||
await this.sessionManager.changeCredentials({
|
||||
currentServerPassword: rootKeys.newRootKey.serverPassword as string,
|
||||
newRootKey: rootKeys.currentRootKey,
|
||||
wrappingKey,
|
||||
})
|
||||
await this.protocolService.reencryptItemsKeys()
|
||||
await rollback()
|
||||
await this.syncService.sync({ awaitAll: true })
|
||||
|
||||
return { error: Error(Messages.CredentialsChangeStrings.Failed) }
|
||||
}
|
||||
}
|
||||
|
||||
return result.response
|
||||
}
|
||||
|
||||
private async recomputeRootKeysForCredentialChange(parameters: {
|
||||
currentPassword: string
|
||||
currentEmail: string
|
||||
origination: KeyParamsOrigination
|
||||
newEmail?: string
|
||||
newPassword?: string
|
||||
}): Promise<{ currentRootKey: SNRootKey; newRootKey: SNRootKey }> {
|
||||
const currentRootKey = await this.protocolService.computeRootKey(
|
||||
parameters.currentPassword,
|
||||
(await this.protocolService.getRootKeyParams()) as SNRootKeyParams,
|
||||
)
|
||||
const newRootKey = await this.protocolService.createRootKey(
|
||||
parameters.newEmail ?? parameters.currentEmail,
|
||||
parameters.newPassword ?? parameters.currentPassword,
|
||||
parameters.origination,
|
||||
)
|
||||
|
||||
return {
|
||||
currentRootKey,
|
||||
newRootKey,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './UserServerInterface'
|
||||
export * from './UserService'
|
||||
@@ -19,4 +19,3 @@ export * from './Settings'
|
||||
export * from './Singleton/SingletonManager'
|
||||
export * from './Storage/DiskStorageService'
|
||||
export * from './Sync'
|
||||
export * from './User'
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
export const InfoStrings = {
|
||||
AccountDeleted: 'Your account has been successfully deleted.',
|
||||
UnsupportedBackupFileVersion:
|
||||
'This backup file was created using a newer version of the application and cannot be imported here. Please update your application and try again.',
|
||||
BackupFileMoreRecentThanAccount:
|
||||
"This backup file was created using a newer encryption version than your account's. Please run the available encryption upgrade and try again.",
|
||||
SavingWhileDocumentHidden:
|
||||
'Attempting to save an item while the application is hidden. To protect data integrity, please refresh the application window and try again.',
|
||||
InvalidNote:
|
||||
"The note you are attempting to save can not be found or has been deleted. Changes you make will not be synced. Please copy this note's text and start a new note.",
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
import { ConfirmStrings } from './Confirm'
|
||||
import { InfoStrings } from './Info'
|
||||
import { InputStrings } from './Input'
|
||||
import { NetworkStrings } from './Network'
|
||||
|
||||
export const Strings = {
|
||||
Info: InfoStrings,
|
||||
Network: NetworkStrings,
|
||||
Confirm: ConfirmStrings,
|
||||
Input: InputStrings,
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@standardnotes/api": "workspace:*",
|
||||
"@standardnotes/common": "^1.39.0",
|
||||
"@standardnotes/common": "^1.43.0",
|
||||
"@standardnotes/domain-events": "^2.39.0",
|
||||
"@standardnotes/encryption": "workspace:*",
|
||||
"@standardnotes/features": "workspace:*",
|
||||
|
||||
Reference in New Issue
Block a user