feat(snjs): add sign in with recovery codes use case (#2130)
* feat(snjs): add sign in with recovery codes use case * fix(snjs): code review adjustments * fix(snjs): remove unnecessary exposed getter * fix(services): waiting for event handling * fix: preferences test Co-authored-by: Mo <mo@standardnotes.com>
This commit is contained in:
5
packages/api/src/Domain/Client/Auth/AuthApiOperations.ts
Normal file
5
packages/api/src/Domain/Client/Auth/AuthApiOperations.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export enum AuthApiOperations {
|
||||||
|
GenerateRecoveryCodes,
|
||||||
|
GetRecoveryKeyParams,
|
||||||
|
SignInWithRecoveryCodes,
|
||||||
|
}
|
||||||
89
packages/api/src/Domain/Client/Auth/AuthApiService.ts
Normal file
89
packages/api/src/Domain/Client/Auth/AuthApiService.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { ApiVersion } from '../../Api'
|
||||||
|
import { ApiCallError } from '../../Error/ApiCallError'
|
||||||
|
import { ErrorMessage } from '../../Error/ErrorMessage'
|
||||||
|
import {
|
||||||
|
GenerateRecoveryCodesResponse,
|
||||||
|
RecoveryKeyParamsResponse,
|
||||||
|
SignInWithRecoveryCodesResponse,
|
||||||
|
} from '../../Response'
|
||||||
|
import { AuthServerInterface } from '../../Server'
|
||||||
|
|
||||||
|
import { AuthApiOperations } from './AuthApiOperations'
|
||||||
|
import { AuthApiServiceInterface } from './AuthApiServiceInterface'
|
||||||
|
|
||||||
|
export class AuthApiService implements AuthApiServiceInterface {
|
||||||
|
private operationsInProgress: Map<AuthApiOperations, boolean>
|
||||||
|
|
||||||
|
constructor(private authServer: AuthServerInterface) {
|
||||||
|
this.operationsInProgress = new Map()
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateRecoveryCodes(): Promise<GenerateRecoveryCodesResponse> {
|
||||||
|
if (this.operationsInProgress.get(AuthApiOperations.GenerateRecoveryCodes)) {
|
||||||
|
throw new ApiCallError(ErrorMessage.GenericInProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.operationsInProgress.set(AuthApiOperations.GenerateRecoveryCodes, true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.authServer.generateRecoveryCodes()
|
||||||
|
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
throw new ApiCallError(ErrorMessage.GenericFail)
|
||||||
|
} finally {
|
||||||
|
this.operationsInProgress.set(AuthApiOperations.GenerateRecoveryCodes, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async recoveryKeyParams(dto: {
|
||||||
|
username: string
|
||||||
|
codeChallenge: string
|
||||||
|
recoveryCodes: string
|
||||||
|
}): Promise<RecoveryKeyParamsResponse> {
|
||||||
|
if (this.operationsInProgress.get(AuthApiOperations.GetRecoveryKeyParams)) {
|
||||||
|
throw new ApiCallError(ErrorMessage.GenericInProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.operationsInProgress.set(AuthApiOperations.GetRecoveryKeyParams, true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.authServer.recoveryKeyParams({
|
||||||
|
apiVersion: ApiVersion.v0,
|
||||||
|
...dto,
|
||||||
|
})
|
||||||
|
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
throw new ApiCallError(ErrorMessage.GenericFail)
|
||||||
|
} finally {
|
||||||
|
this.operationsInProgress.set(AuthApiOperations.GetRecoveryKeyParams, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async signInWithRecoveryCodes(dto: {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
codeVerifier: string
|
||||||
|
recoveryCodes: string
|
||||||
|
}): Promise<SignInWithRecoveryCodesResponse> {
|
||||||
|
if (this.operationsInProgress.get(AuthApiOperations.SignInWithRecoveryCodes)) {
|
||||||
|
throw new ApiCallError(ErrorMessage.GenericInProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.operationsInProgress.set(AuthApiOperations.SignInWithRecoveryCodes, true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.authServer.signInWithRecoveryCodes({
|
||||||
|
apiVersion: ApiVersion.v0,
|
||||||
|
...dto,
|
||||||
|
})
|
||||||
|
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
throw new ApiCallError(ErrorMessage.GenericFail)
|
||||||
|
} finally {
|
||||||
|
this.operationsInProgress.set(AuthApiOperations.SignInWithRecoveryCodes, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import {
|
||||||
|
GenerateRecoveryCodesResponse,
|
||||||
|
RecoveryKeyParamsResponse,
|
||||||
|
SignInWithRecoveryCodesResponse,
|
||||||
|
} from '../../Response'
|
||||||
|
|
||||||
|
export interface AuthApiServiceInterface {
|
||||||
|
generateRecoveryCodes(): Promise<GenerateRecoveryCodesResponse>
|
||||||
|
recoveryKeyParams(dto: {
|
||||||
|
username: string
|
||||||
|
codeChallenge: string
|
||||||
|
recoveryCodes: string
|
||||||
|
}): Promise<RecoveryKeyParamsResponse>
|
||||||
|
signInWithRecoveryCodes(dto: {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
codeVerifier: string
|
||||||
|
recoveryCodes: string
|
||||||
|
}): Promise<SignInWithRecoveryCodesResponse>
|
||||||
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
export * from './Auth/AuthApiOperations'
|
||||||
|
export * from './Auth/AuthApiService'
|
||||||
|
export * from './Auth/AuthApiServiceInterface'
|
||||||
export * from './Authenticator/AuthenticatorApiOperations'
|
export * from './Authenticator/AuthenticatorApiOperations'
|
||||||
export * from './Authenticator/AuthenticatorApiService'
|
export * from './Authenticator/AuthenticatorApiService'
|
||||||
export * from './Authenticator/AuthenticatorApiServiceInterface'
|
export * from './Authenticator/AuthenticatorApiServiceInterface'
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export type HttpRequestParams = Record<string, unknown>
|
export type HttpRequestParams = unknown
|
||||||
|
|||||||
@@ -276,9 +276,9 @@ export class HttpService implements HttpServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private urlForUrlAndParams(url: string, params: HttpRequestParams) {
|
private urlForUrlAndParams(url: string, params: HttpRequestParams) {
|
||||||
const keyValueString = Object.keys(params)
|
const keyValueString = Object.keys(params as Record<string, unknown>)
|
||||||
.map((key) => {
|
.map((key) => {
|
||||||
return key + '=' + encodeURIComponent(params[key] as string)
|
return key + '=' + encodeURIComponent((params as Record<string, unknown>)[key] as string)
|
||||||
})
|
})
|
||||||
.join('&')
|
.join('&')
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export interface RecoveryKeyParamsRequestParams {
|
||||||
|
apiVersion: string
|
||||||
|
username: string
|
||||||
|
codeChallenge: string
|
||||||
|
recoveryCodes: string
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export interface SignInWithRecoveryCodesRequestParams {
|
||||||
|
apiVersion: string
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
codeVerifier: string
|
||||||
|
recoveryCodes: string
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ export * from './Authenticator/GenerateAuthenticatorRegistrationOptionsRequestPa
|
|||||||
export * from './Authenticator/ListAuthenticatorsRequestParams'
|
export * from './Authenticator/ListAuthenticatorsRequestParams'
|
||||||
export * from './Authenticator/VerifyAuthenticatorAuthenticationResponseRequestParams'
|
export * from './Authenticator/VerifyAuthenticatorAuthenticationResponseRequestParams'
|
||||||
export * from './Authenticator/VerifyAuthenticatorRegistrationResponseRequestParams'
|
export * from './Authenticator/VerifyAuthenticatorRegistrationResponseRequestParams'
|
||||||
|
export * from './Recovery/RecoveryKeyParamsRequestParams'
|
||||||
|
export * from './Recovery/SignInWithRecoveryCodesRequestParams'
|
||||||
export * from './Subscription/AppleIAPConfirmRequestParams'
|
export * from './Subscription/AppleIAPConfirmRequestParams'
|
||||||
export * from './Subscription/SubscriptionInviteAcceptRequestParams'
|
export * from './Subscription/SubscriptionInviteAcceptRequestParams'
|
||||||
export * from './Subscription/SubscriptionInviteCancelRequestParams'
|
export * from './Subscription/SubscriptionInviteCancelRequestParams'
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Either } from '@standardnotes/common'
|
||||||
|
|
||||||
|
import { HttpErrorResponseBody } from '../../Http/HttpErrorResponseBody'
|
||||||
|
import { HttpResponse } from '../../Http/HttpResponse'
|
||||||
|
|
||||||
|
import { GenerateRecoveryCodesResponseBody } from './GenerateRecoveryCodesResponseBody'
|
||||||
|
|
||||||
|
export interface GenerateRecoveryCodesResponse extends HttpResponse {
|
||||||
|
data: Either<GenerateRecoveryCodesResponseBody, HttpErrorResponseBody>
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export interface GenerateRecoveryCodesResponseBody {
|
||||||
|
recoveryCodes: string
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Either } from '@standardnotes/common'
|
||||||
|
|
||||||
|
import { HttpErrorResponseBody } from '../../Http/HttpErrorResponseBody'
|
||||||
|
import { HttpResponse } from '../../Http/HttpResponse'
|
||||||
|
|
||||||
|
import { RecoveryKeyParamsResponseBody } from './RecoveryKeyParamsResponseBody'
|
||||||
|
|
||||||
|
export interface RecoveryKeyParamsResponse extends HttpResponse {
|
||||||
|
data: Either<RecoveryKeyParamsResponseBody, HttpErrorResponseBody>
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { KeyParamsData } from '@standardnotes/responses'
|
||||||
|
|
||||||
|
export interface RecoveryKeyParamsResponseBody {
|
||||||
|
keyParams: KeyParamsData
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Either } from '@standardnotes/common'
|
||||||
|
|
||||||
|
import { HttpErrorResponseBody } from '../../Http/HttpErrorResponseBody'
|
||||||
|
import { HttpResponse } from '../../Http/HttpResponse'
|
||||||
|
|
||||||
|
import { SignInWithRecoveryCodesResponseBody } from './SignInWithRecoveryCodesResponseBody'
|
||||||
|
|
||||||
|
export interface SignInWithRecoveryCodesResponse extends HttpResponse {
|
||||||
|
data: Either<SignInWithRecoveryCodesResponseBody, HttpErrorResponseBody>
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { KeyParamsData, SessionBody } from '@standardnotes/responses'
|
||||||
|
|
||||||
|
export interface SignInWithRecoveryCodesResponseBody {
|
||||||
|
session: SessionBody
|
||||||
|
key_params: KeyParamsData
|
||||||
|
user: {
|
||||||
|
uuid: string
|
||||||
|
email: string
|
||||||
|
protocolVersion: string
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,12 @@ export * from './Authenticator/VerifyAuthenticatorAuthenticationResponseResponse
|
|||||||
export * from './Authenticator/VerifyAuthenticatorAuthenticationResponseResponseBody'
|
export * from './Authenticator/VerifyAuthenticatorAuthenticationResponseResponseBody'
|
||||||
export * from './Authenticator/VerifyAuthenticatorRegistrationResponseResponse'
|
export * from './Authenticator/VerifyAuthenticatorRegistrationResponseResponse'
|
||||||
export * from './Authenticator/VerifyAuthenticatorRegistrationResponseResponseBody'
|
export * from './Authenticator/VerifyAuthenticatorRegistrationResponseResponseBody'
|
||||||
|
export * from './Recovery/GenerateRecoveryCodesResponse'
|
||||||
|
export * from './Recovery/GenerateRecoveryCodesResponseBody'
|
||||||
|
export * from './Recovery/RecoveryKeyParamsResponse'
|
||||||
|
export * from './Recovery/RecoveryKeyParamsResponseBody'
|
||||||
|
export * from './Recovery/SignInWithRecoveryCodesResponse'
|
||||||
|
export * from './Recovery/SignInWithRecoveryCodesResponseBody'
|
||||||
export * from './Subscription/AppleIAPConfirmResponse'
|
export * from './Subscription/AppleIAPConfirmResponse'
|
||||||
export * from './Subscription/AppleIAPConfirmResponseBody'
|
export * from './Subscription/AppleIAPConfirmResponseBody'
|
||||||
export * from './Subscription/SubscriptionInviteAcceptResponse'
|
export * from './Subscription/SubscriptionInviteAcceptResponse'
|
||||||
|
|||||||
33
packages/api/src/Domain/Server/Auth/AuthServer.ts
Normal file
33
packages/api/src/Domain/Server/Auth/AuthServer.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { HttpServiceInterface } from '../../Http/HttpServiceInterface'
|
||||||
|
import { RecoveryKeyParamsRequestParams, SignInWithRecoveryCodesRequestParams } from '../../Request'
|
||||||
|
import {
|
||||||
|
GenerateRecoveryCodesResponse,
|
||||||
|
RecoveryKeyParamsResponse,
|
||||||
|
SignInWithRecoveryCodesResponse,
|
||||||
|
} from '../../Response'
|
||||||
|
import { AuthServerInterface } from './AuthServerInterface'
|
||||||
|
import { Paths } from './Paths'
|
||||||
|
|
||||||
|
export class AuthServer implements AuthServerInterface {
|
||||||
|
constructor(private httpService: HttpServiceInterface) {}
|
||||||
|
|
||||||
|
async generateRecoveryCodes(): Promise<GenerateRecoveryCodesResponse> {
|
||||||
|
const response = await this.httpService.post(Paths.v1.generateRecoveryCodes)
|
||||||
|
|
||||||
|
return response as GenerateRecoveryCodesResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
async recoveryKeyParams(params: RecoveryKeyParamsRequestParams): Promise<RecoveryKeyParamsResponse> {
|
||||||
|
const response = await this.httpService.post(Paths.v1.recoveryKeyParams, params)
|
||||||
|
|
||||||
|
return response as RecoveryKeyParamsResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
async signInWithRecoveryCodes(
|
||||||
|
params: SignInWithRecoveryCodesRequestParams,
|
||||||
|
): Promise<SignInWithRecoveryCodesResponse> {
|
||||||
|
const response = await this.httpService.post(Paths.v1.signInWithRecoveryCodes, params)
|
||||||
|
|
||||||
|
return response as SignInWithRecoveryCodesResponse
|
||||||
|
}
|
||||||
|
}
|
||||||
12
packages/api/src/Domain/Server/Auth/AuthServerInterface.ts
Normal file
12
packages/api/src/Domain/Server/Auth/AuthServerInterface.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { RecoveryKeyParamsRequestParams, SignInWithRecoveryCodesRequestParams } from '../../Request'
|
||||||
|
import {
|
||||||
|
GenerateRecoveryCodesResponse,
|
||||||
|
RecoveryKeyParamsResponse,
|
||||||
|
SignInWithRecoveryCodesResponse,
|
||||||
|
} from '../../Response'
|
||||||
|
|
||||||
|
export interface AuthServerInterface {
|
||||||
|
generateRecoveryCodes(): Promise<GenerateRecoveryCodesResponse>
|
||||||
|
recoveryKeyParams(params: RecoveryKeyParamsRequestParams): Promise<RecoveryKeyParamsResponse>
|
||||||
|
signInWithRecoveryCodes(params: SignInWithRecoveryCodesRequestParams): Promise<SignInWithRecoveryCodesResponse>
|
||||||
|
}
|
||||||
@@ -2,8 +2,15 @@ const SessionPaths = {
|
|||||||
refreshSession: '/v1/sessions/refresh',
|
refreshSession: '/v1/sessions/refresh',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const RecoveryPaths = {
|
||||||
|
generateRecoveryCodes: '/v1/auth/recovery/codes',
|
||||||
|
recoveryKeyParams: '/v1/auth/recovery/login-params',
|
||||||
|
signInWithRecoveryCodes: '/v1/auth/recovery/login',
|
||||||
|
}
|
||||||
|
|
||||||
export const Paths = {
|
export const Paths = {
|
||||||
v1: {
|
v1: {
|
||||||
...SessionPaths,
|
...SessionPaths,
|
||||||
|
...RecoveryPaths,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
export * from './Auth/AuthServer'
|
||||||
|
export * from './Auth/AuthServerInterface'
|
||||||
export * from './Authenticator/AuthenticatorServer'
|
export * from './Authenticator/AuthenticatorServer'
|
||||||
export * from './Authenticator/AuthenticatorServerInterface'
|
export * from './Authenticator/AuthenticatorServerInterface'
|
||||||
export * from './Subscription/SubscriptionServer'
|
export * from './Subscription/SubscriptionServer'
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ export interface EncryptionProviderInterface {
|
|||||||
getKeyEmbeddedKeyParams(key: EncryptedPayloadInterface): SNRootKeyParams | undefined
|
getKeyEmbeddedKeyParams(key: EncryptedPayloadInterface): SNRootKeyParams | undefined
|
||||||
computeRootKey(password: string, keyParams: SNRootKeyParams): Promise<RootKeyInterface>
|
computeRootKey(password: string, keyParams: SNRootKeyParams): Promise<RootKeyInterface>
|
||||||
supportedVersions(): ProtocolVersion[]
|
supportedVersions(): ProtocolVersion[]
|
||||||
|
isVersionNewerThanLibraryVersion(version: ProtocolVersion): boolean
|
||||||
|
platformSupportsKeyDerivation(keyParams: SNRootKeyParams): boolean
|
||||||
|
computeWrappingKey(passcode: string): Promise<RootKeyInterface>
|
||||||
getUserVersion(): ProtocolVersion | undefined
|
getUserVersion(): ProtocolVersion | undefined
|
||||||
decryptBackupFile(
|
decryptBackupFile(
|
||||||
file: BackupFile,
|
file: BackupFile,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"@standardnotes/api": "workspace:^",
|
"@standardnotes/api": "workspace:^",
|
||||||
"@standardnotes/auth": "^3.19.4",
|
"@standardnotes/auth": "^3.19.4",
|
||||||
"@standardnotes/common": "^1.45.0",
|
"@standardnotes/common": "^1.45.0",
|
||||||
|
"@standardnotes/domain-core": "^1.11.0",
|
||||||
"@standardnotes/encryption": "workspace:^",
|
"@standardnotes/encryption": "workspace:^",
|
||||||
"@standardnotes/files": "workspace:^",
|
"@standardnotes/files": "workspace:^",
|
||||||
"@standardnotes/models": "workspace:^",
|
"@standardnotes/models": "workspace:^",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ApplicationIdentifier, ContentType } from '@standardnotes/common'
|
import { ApplicationIdentifier, ContentType } from '@standardnotes/common'
|
||||||
import { BackupFile, DecryptedItemInterface, ItemStream, Platform, PrefKey, PrefValue } from '@standardnotes/models'
|
import { BackupFile, DecryptedItemInterface, ItemStream, Platform, PrefKey, PrefValue } from '@standardnotes/models'
|
||||||
import { FilesClientInterface } from '@standardnotes/files'
|
import { FilesClientInterface } from '@standardnotes/files'
|
||||||
|
|
||||||
import { AlertService } from '../Alert/AlertService'
|
import { AlertService } from '../Alert/AlertService'
|
||||||
import { ComponentManagerInterface } from '../Component/ComponentManagerInterface'
|
import { ComponentManagerInterface } from '../Component/ComponentManagerInterface'
|
||||||
import { ApplicationEvent } from '../Event/ApplicationEvent'
|
import { ApplicationEvent } from '../Event/ApplicationEvent'
|
||||||
|
|||||||
28
packages/services/src/Domain/Auth/AuthClientInterface.ts
Normal file
28
packages/services/src/Domain/Auth/AuthClientInterface.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { AnyKeyParamsContent } from '@standardnotes/common'
|
||||||
|
import { SessionBody } from '@standardnotes/responses'
|
||||||
|
|
||||||
|
export interface AuthClientInterface {
|
||||||
|
generateRecoveryCodes(): Promise<string | false>
|
||||||
|
recoveryKeyParams(dto: {
|
||||||
|
username: string
|
||||||
|
codeChallenge: string
|
||||||
|
recoveryCodes: string
|
||||||
|
}): Promise<AnyKeyParamsContent | false>
|
||||||
|
signInWithRecoveryCodes(dto: {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
codeVerifier: string
|
||||||
|
recoveryCodes: string
|
||||||
|
}): Promise<
|
||||||
|
| {
|
||||||
|
keyParams: AnyKeyParamsContent
|
||||||
|
session: SessionBody
|
||||||
|
user: {
|
||||||
|
uuid: string
|
||||||
|
email: string
|
||||||
|
protocolVersion: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
| false
|
||||||
|
>
|
||||||
|
}
|
||||||
82
packages/services/src/Domain/Auth/AuthManager.ts
Normal file
82
packages/services/src/Domain/Auth/AuthManager.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { AuthApiServiceInterface } from '@standardnotes/api'
|
||||||
|
import { AnyKeyParamsContent } from '@standardnotes/common'
|
||||||
|
import { SessionBody } from '@standardnotes/responses'
|
||||||
|
|
||||||
|
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
|
||||||
|
import { AbstractService } from '../Service/AbstractService'
|
||||||
|
import { AuthClientInterface } from './AuthClientInterface'
|
||||||
|
|
||||||
|
export class AuthManager extends AbstractService implements AuthClientInterface {
|
||||||
|
constructor(
|
||||||
|
private authApiService: AuthApiServiceInterface,
|
||||||
|
protected override internalEventBus: InternalEventBusInterface,
|
||||||
|
) {
|
||||||
|
super(internalEventBus)
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateRecoveryCodes(): Promise<string | false> {
|
||||||
|
try {
|
||||||
|
const result = await this.authApiService.generateRecoveryCodes()
|
||||||
|
|
||||||
|
if (result.data.error) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data.recoveryCodes
|
||||||
|
} catch (error) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async recoveryKeyParams(dto: {
|
||||||
|
username: string
|
||||||
|
codeChallenge: string
|
||||||
|
recoveryCodes: string
|
||||||
|
}): Promise<AnyKeyParamsContent | false> {
|
||||||
|
try {
|
||||||
|
const result = await this.authApiService.recoveryKeyParams(dto)
|
||||||
|
|
||||||
|
if (result.data.error) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data.keyParams as AnyKeyParamsContent
|
||||||
|
} catch (error) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async signInWithRecoveryCodes(dto: {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
codeVerifier: string
|
||||||
|
recoveryCodes: string
|
||||||
|
}): Promise<
|
||||||
|
| {
|
||||||
|
keyParams: AnyKeyParamsContent
|
||||||
|
session: SessionBody
|
||||||
|
user: {
|
||||||
|
uuid: string
|
||||||
|
email: string
|
||||||
|
protocolVersion: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
| false
|
||||||
|
> {
|
||||||
|
try {
|
||||||
|
const result = await this.authApiService.signInWithRecoveryCodes(dto)
|
||||||
|
|
||||||
|
if (result.data.error) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
keyParams: result.data.key_params as AnyKeyParamsContent,
|
||||||
|
session: result.data.session,
|
||||||
|
user: result.data.user,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { UserRegistrationResponseBody } from '@standardnotes/api'
|
import { UserRegistrationResponseBody } from '@standardnotes/api'
|
||||||
import { ProtocolVersion } from '@standardnotes/common'
|
import { ProtocolVersion } from '@standardnotes/common'
|
||||||
|
import { SNRootKey } from '@standardnotes/encryption'
|
||||||
import { RootKeyInterface } from '@standardnotes/models'
|
import { RootKeyInterface } from '@standardnotes/models'
|
||||||
import { ClientDisplayableError, HttpResponse, SignInResponse, User } from '@standardnotes/responses'
|
import { ClientDisplayableError, HttpResponse, SessionBody, SignInResponse, User } from '@standardnotes/responses'
|
||||||
import { Base64String } from '@standardnotes/sncrypto-common'
|
import { Base64String } from '@standardnotes/sncrypto-common'
|
||||||
|
|
||||||
import { SessionManagerResponse } from './SessionManagerResponse'
|
import { SessionManagerResponse } from './SessionManagerResponse'
|
||||||
@@ -31,4 +32,13 @@ export interface SessionsClientInterface {
|
|||||||
wrappingKey?: RootKeyInterface
|
wrappingKey?: RootKeyInterface
|
||||||
newEmail?: string
|
newEmail?: string
|
||||||
}): Promise<SessionManagerResponse>
|
}): Promise<SessionManagerResponse>
|
||||||
|
handleAuthentication(dto: {
|
||||||
|
session: SessionBody
|
||||||
|
user: {
|
||||||
|
uuid: string
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
rootKey: SNRootKey
|
||||||
|
wrappingKey?: SNRootKey
|
||||||
|
}): Promise<void>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Base64String } from '@standardnotes/sncrypto-common'
|
import { Base64String } from '@standardnotes/sncrypto-common'
|
||||||
import { EncryptionProviderInterface, SNRootKey, SNRootKeyParams } from '@standardnotes/encryption'
|
import { EncryptionProviderInterface, SNRootKey, SNRootKeyParams } from '@standardnotes/encryption'
|
||||||
import { HttpResponse, SignInResponse, User } from '@standardnotes/responses'
|
import { HttpResponse, SignInResponse, User } from '@standardnotes/responses'
|
||||||
import { KeyParamsOrigination, UserRequestType } from '@standardnotes/common'
|
import { Either, KeyParamsOrigination, UserRequestType } from '@standardnotes/common'
|
||||||
import { UuidGenerator } from '@standardnotes/utils'
|
import { UuidGenerator } from '@standardnotes/utils'
|
||||||
import { UserApiServiceInterface, UserRegistrationResponseBody } from '@standardnotes/api'
|
import { UserApiServiceInterface, UserRegistrationResponseBody } from '@standardnotes/api'
|
||||||
|
|
||||||
@@ -25,6 +25,8 @@ import { DeinitSource } from '../Application/DeinitSource'
|
|||||||
import { StoragePersistencePolicies } from '../Storage/StorageTypes'
|
import { StoragePersistencePolicies } from '../Storage/StorageTypes'
|
||||||
import { SessionsClientInterface } from '../Session/SessionsClientInterface'
|
import { SessionsClientInterface } from '../Session/SessionsClientInterface'
|
||||||
import { ProtectionsClientInterface } from '../Protection/ProtectionClientInterface'
|
import { ProtectionsClientInterface } from '../Protection/ProtectionClientInterface'
|
||||||
|
import { InternalEventHandlerInterface } from '../Internal/InternalEventHandlerInterface'
|
||||||
|
import { InternalEventInterface } from '../Internal/InternalEventInterface'
|
||||||
|
|
||||||
export type CredentialsChangeFunctionResponse = { error?: { message: string } }
|
export type CredentialsChangeFunctionResponse = { error?: { message: string } }
|
||||||
export type AccountServiceResponse = HttpResponse
|
export type AccountServiceResponse = HttpResponse
|
||||||
@@ -34,11 +36,25 @@ export enum AccountEvent {
|
|||||||
SignedOut = 'SignedOut',
|
SignedOut = 'SignedOut',
|
||||||
}
|
}
|
||||||
|
|
||||||
type AccountEventData = {
|
export interface SignedInOrRegisteredEventPayload {
|
||||||
|
ephemeral: boolean
|
||||||
|
mergeLocal: boolean
|
||||||
|
awaitSync: boolean
|
||||||
|
checkIntegrity: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SignedOutEventPayload {
|
||||||
source: DeinitSource
|
source: DeinitSource
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UserService extends AbstractService<AccountEvent, AccountEventData> implements UserClientInterface {
|
export interface AccountEventData {
|
||||||
|
payload: Either<SignedInOrRegisteredEventPayload, SignedOutEventPayload>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserService
|
||||||
|
extends AbstractService<AccountEvent, AccountEventData>
|
||||||
|
implements UserClientInterface, InternalEventHandlerInterface
|
||||||
|
{
|
||||||
private signingIn = false
|
private signingIn = false
|
||||||
private registering = false
|
private registering = false
|
||||||
|
|
||||||
@@ -60,6 +76,43 @@ export class UserService extends AbstractService<AccountEvent, AccountEventData>
|
|||||||
super(internalEventBus)
|
super(internalEventBus)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||||
|
if (event.type === AccountEvent.SignedInOrRegistered) {
|
||||||
|
const payload = (event.payload as AccountEventData).payload as SignedInOrRegisteredEventPayload
|
||||||
|
this.syncService.resetSyncState()
|
||||||
|
|
||||||
|
await this.storageService.setPersistencePolicy(
|
||||||
|
payload.ephemeral ? StoragePersistencePolicies.Ephemeral : StoragePersistencePolicies.Default,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (payload.mergeLocal) {
|
||||||
|
await this.syncService.markAllItemsAsNeedingSyncAndPersist()
|
||||||
|
} else {
|
||||||
|
void this.itemManager.removeAllItemsFromMemory()
|
||||||
|
await this.clearDatabase()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.unlockSyncing()
|
||||||
|
|
||||||
|
const syncPromise = this.syncService
|
||||||
|
.downloadFirstSync(1_000, {
|
||||||
|
checkIntegrity: payload.checkIntegrity,
|
||||||
|
awaitAll: payload.awaitSync,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
if (!payload.awaitSync) {
|
||||||
|
void this.protocolService.decryptErroredPayloads()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (payload.awaitSync) {
|
||||||
|
await syncPromise
|
||||||
|
|
||||||
|
await this.protocolService.decryptErroredPayloads()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public override deinit(): void {
|
public override deinit(): void {
|
||||||
super.deinit()
|
super.deinit()
|
||||||
;(this.sessionManager as unknown) = undefined
|
;(this.sessionManager as unknown) = undefined
|
||||||
@@ -97,27 +150,17 @@ export class UserService extends AbstractService<AccountEvent, AccountEventData>
|
|||||||
this.lockSyncing()
|
this.lockSyncing()
|
||||||
const response = await this.sessionManager.register(email, password, ephemeral)
|
const response = await this.sessionManager.register(email, password, ephemeral)
|
||||||
|
|
||||||
this.syncService.resetSyncState()
|
await this.notifyEventSync(AccountEvent.SignedInOrRegistered, {
|
||||||
|
payload: {
|
||||||
|
ephemeral,
|
||||||
|
mergeLocal,
|
||||||
|
awaitSync: true,
|
||||||
|
checkIntegrity: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
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
|
this.registering = false
|
||||||
|
|
||||||
await this.syncService.downloadFirstSync(300)
|
|
||||||
void this.protocolService.decryptErroredPayloads()
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.unlockSyncing()
|
this.unlockSyncing()
|
||||||
@@ -156,39 +199,15 @@ export class UserService extends AbstractService<AccountEvent, AccountEventData>
|
|||||||
const result = await this.sessionManager.signIn(email, password, strict, ephemeral)
|
const result = await this.sessionManager.signIn(email, password, strict, ephemeral)
|
||||||
|
|
||||||
if (!result.response.error) {
|
if (!result.response.error) {
|
||||||
this.syncService.resetSyncState()
|
const notifyingFunction = awaitSync ? this.notifyEventSync.bind(this) : this.notifyEvent.bind(this)
|
||||||
|
await notifyingFunction(AccountEvent.SignedInOrRegistered, {
|
||||||
await this.storageService.setPersistencePolicy(
|
payload: {
|
||||||
ephemeral ? StoragePersistencePolicies.Ephemeral : StoragePersistencePolicies.Default,
|
mergeLocal,
|
||||||
)
|
awaitSync,
|
||||||
|
ephemeral,
|
||||||
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,
|
checkIntegrity: true,
|
||||||
awaitAll: awaitSync,
|
},
|
||||||
})
|
})
|
||||||
.then(() => {
|
|
||||||
if (!awaitSync) {
|
|
||||||
void this.protocolService.decryptErroredPayloads()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (awaitSync) {
|
|
||||||
await syncPromise
|
|
||||||
|
|
||||||
await this.protocolService.decryptErroredPayloads()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
this.unlockSyncing()
|
this.unlockSyncing()
|
||||||
}
|
}
|
||||||
@@ -267,15 +286,14 @@ export class UserService extends AbstractService<AccountEvent, AccountEventData>
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (!response.error) {
|
if (!response.error) {
|
||||||
await this.notifyEvent(AccountEvent.SignedInOrRegistered)
|
await this.notifyEvent(AccountEvent.SignedInOrRegistered, {
|
||||||
|
payload: {
|
||||||
this.unlockSyncing()
|
mergeLocal: true,
|
||||||
|
awaitSync: true,
|
||||||
void this.syncService.downloadFirstSync(1_000, {
|
ephemeral: false,
|
||||||
checkIntegrity: true,
|
checkIntegrity: true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
void this.protocolService.decryptErroredPayloads()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.unlockSyncing()
|
this.unlockSyncing()
|
||||||
@@ -310,7 +328,7 @@ export class UserService extends AbstractService<AccountEvent, AccountEventData>
|
|||||||
await this.sessionManager.signOut()
|
await this.sessionManager.signOut()
|
||||||
await this.protocolService.deleteWorkspaceSpecificKeyStateFromDevice()
|
await this.protocolService.deleteWorkspaceSpecificKeyStateFromDevice()
|
||||||
await this.storageService.clearAllData()
|
await this.storageService.clearAllData()
|
||||||
await this.notifyEvent(AccountEvent.SignedOut, { source })
|
await this.notifyEvent(AccountEvent.SignedOut, { payload: { source } })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (force) {
|
if (force) {
|
||||||
@@ -473,7 +491,14 @@ export class UserService extends AbstractService<AccountEvent, AccountEventData>
|
|||||||
|
|
||||||
public async populateSessionFromDemoShareToken(token: Base64String): Promise<void> {
|
public async populateSessionFromDemoShareToken(token: Base64String): Promise<void> {
|
||||||
await this.sessionManager.populateSessionFromDemoShareToken(token)
|
await this.sessionManager.populateSessionFromDemoShareToken(token)
|
||||||
await this.notifyEvent(AccountEvent.SignedInOrRegistered)
|
await this.notifyEvent(AccountEvent.SignedInOrRegistered, {
|
||||||
|
payload: {
|
||||||
|
ephemeral: false,
|
||||||
|
mergeLocal: false,
|
||||||
|
checkIntegrity: false,
|
||||||
|
awaitSync: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private async setPasscodeWithoutWarning(passcode: string, origination: KeyParamsOrigination) {
|
private async setPasscodeWithoutWarning(passcode: string, origination: KeyParamsOrigination) {
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ export * from './Application/ApplicationStage'
|
|||||||
export * from './Application/DeinitCallback'
|
export * from './Application/DeinitCallback'
|
||||||
export * from './Application/DeinitSource'
|
export * from './Application/DeinitSource'
|
||||||
export * from './Application/DeinitMode'
|
export * from './Application/DeinitMode'
|
||||||
|
export * from './Auth/AuthClientInterface'
|
||||||
|
export * from './Auth/AuthManager'
|
||||||
export * from './Authenticator/AuthenticatorClientInterface'
|
export * from './Authenticator/AuthenticatorClientInterface'
|
||||||
export * from './Authenticator/AuthenticatorManager'
|
export * from './Authenticator/AuthenticatorManager'
|
||||||
export * from './User/UserClientInterface'
|
export * from './User/UserClientInterface'
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
|
AuthApiService,
|
||||||
AuthenticatorApiService,
|
AuthenticatorApiService,
|
||||||
AuthenticatorApiServiceInterface,
|
AuthenticatorApiServiceInterface,
|
||||||
AuthenticatorServer,
|
AuthenticatorServer,
|
||||||
AuthenticatorServerInterface,
|
AuthenticatorServerInterface,
|
||||||
|
AuthServer,
|
||||||
HttpService,
|
HttpService,
|
||||||
HttpServiceInterface,
|
HttpServiceInterface,
|
||||||
SubscriptionApiService,
|
SubscriptionApiService,
|
||||||
@@ -69,6 +71,8 @@ import {
|
|||||||
AccountEvent,
|
AccountEvent,
|
||||||
AuthenticatorClientInterface,
|
AuthenticatorClientInterface,
|
||||||
AuthenticatorManager,
|
AuthenticatorManager,
|
||||||
|
AuthClientInterface,
|
||||||
|
AuthManager,
|
||||||
} from '@standardnotes/services'
|
} from '@standardnotes/services'
|
||||||
import { BackupServiceInterface, FilesClientInterface } from '@standardnotes/files'
|
import { BackupServiceInterface, FilesClientInterface } from '@standardnotes/files'
|
||||||
import { ComputePrivateUsername } from '@standardnotes/encryption'
|
import { ComputePrivateUsername } from '@standardnotes/encryption'
|
||||||
@@ -88,9 +92,11 @@ import { SNLog } from '../Log'
|
|||||||
import { ChallengeResponse, ListedClientInterface } from '../Services'
|
import { ChallengeResponse, ListedClientInterface } from '../Services'
|
||||||
import { ApplicationConstructorOptions, FullyResolvedApplicationOptions } from './Options/ApplicationOptions'
|
import { ApplicationConstructorOptions, FullyResolvedApplicationOptions } from './Options/ApplicationOptions'
|
||||||
import { ApplicationOptionsDefaults } from './Options/Defaults'
|
import { ApplicationOptionsDefaults } from './Options/Defaults'
|
||||||
import { LegacySession, MapperInterface, Session } from '@standardnotes/domain-core'
|
import { LegacySession, MapperInterface, Session, UseCaseInterface } from '@standardnotes/domain-core'
|
||||||
import { SessionStorageMapper } from '@Lib/Services/Mapping/SessionStorageMapper'
|
import { SessionStorageMapper } from '@Lib/Services/Mapping/SessionStorageMapper'
|
||||||
import { LegacySessionStorageMapper } from '@Lib/Services/Mapping/LegacySessionStorageMapper'
|
import { LegacySessionStorageMapper } from '@Lib/Services/Mapping/LegacySessionStorageMapper'
|
||||||
|
import { SignInWithRecoveryCodes } from '@Lib/Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes'
|
||||||
|
import { UseCaseContainerInterface } from '@Lib/Domain/UseCase/UseCaseContainerInterface'
|
||||||
|
|
||||||
/** How often to automatically sync, in milliseconds */
|
/** How often to automatically sync, in milliseconds */
|
||||||
const DEFAULT_AUTO_SYNC_INTERVAL = 30_000
|
const DEFAULT_AUTO_SYNC_INTERVAL = 30_000
|
||||||
@@ -106,7 +112,7 @@ type ApplicationObserver = {
|
|||||||
|
|
||||||
type ObserverRemover = () => void
|
type ObserverRemover = () => void
|
||||||
|
|
||||||
export class SNApplication implements ApplicationInterface, AppGroupManagedApplication {
|
export class SNApplication implements ApplicationInterface, AppGroupManagedApplication, UseCaseContainerInterface {
|
||||||
onDeinit!: ExternalServices.DeinitCallback
|
onDeinit!: ExternalServices.DeinitCallback
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -168,6 +174,9 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
|||||||
private declare authenticatorApiService: AuthenticatorApiServiceInterface
|
private declare authenticatorApiService: AuthenticatorApiServiceInterface
|
||||||
private declare authenticatorServer: AuthenticatorServerInterface
|
private declare authenticatorServer: AuthenticatorServerInterface
|
||||||
private declare authenticatorManager: AuthenticatorClientInterface
|
private declare authenticatorManager: AuthenticatorClientInterface
|
||||||
|
private declare authManager: AuthClientInterface
|
||||||
|
|
||||||
|
private declare _signInWithRecoveryCodes: SignInWithRecoveryCodes
|
||||||
|
|
||||||
private internalEventBus!: ExternalServices.InternalEventBusInterface
|
private internalEventBus!: ExternalServices.InternalEventBusInterface
|
||||||
|
|
||||||
@@ -250,6 +259,10 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
|||||||
return this.workspaceManager
|
return this.workspaceManager
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get signInWithRecoveryCodes(): UseCaseInterface<void> {
|
||||||
|
return this._signInWithRecoveryCodes
|
||||||
|
}
|
||||||
|
|
||||||
public get files(): FilesClientInterface {
|
public get files(): FilesClientInterface {
|
||||||
return this.fileService
|
return this.fileService
|
||||||
}
|
}
|
||||||
@@ -1150,6 +1163,9 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
|||||||
this.createAuthenticatorServer()
|
this.createAuthenticatorServer()
|
||||||
this.createAuthenticatorApiService()
|
this.createAuthenticatorApiService()
|
||||||
this.createAuthenticatorManager()
|
this.createAuthenticatorManager()
|
||||||
|
this.createAuthManager()
|
||||||
|
|
||||||
|
this.createUseCases()
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearServices() {
|
private clearServices() {
|
||||||
@@ -1200,6 +1216,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
|||||||
;(this.authenticatorApiService as unknown) = undefined
|
;(this.authenticatorApiService as unknown) = undefined
|
||||||
;(this.authenticatorServer as unknown) = undefined
|
;(this.authenticatorServer as unknown) = undefined
|
||||||
;(this.authenticatorManager as unknown) = undefined
|
;(this.authenticatorManager as unknown) = undefined
|
||||||
|
;(this.authManager as unknown) = undefined
|
||||||
|
;(this._signInWithRecoveryCodes as unknown) = undefined
|
||||||
|
|
||||||
this.services = []
|
this.services = []
|
||||||
}
|
}
|
||||||
@@ -1212,6 +1230,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
|||||||
this.internalEventBus.addEventHandler(this.featuresService, ExternalServices.ApiServiceEvent.MetaReceived)
|
this.internalEventBus.addEventHandler(this.featuresService, ExternalServices.ApiServiceEvent.MetaReceived)
|
||||||
this.internalEventBus.addEventHandler(this.integrityService, ExternalServices.SyncEvent.SyncRequestsIntegrityCheck)
|
this.internalEventBus.addEventHandler(this.integrityService, ExternalServices.SyncEvent.SyncRequestsIntegrityCheck)
|
||||||
this.internalEventBus.addEventHandler(this.syncService, ExternalServices.IntegrityEvent.IntegrityCheckCompleted)
|
this.internalEventBus.addEventHandler(this.syncService, ExternalServices.IntegrityEvent.IntegrityCheckCompleted)
|
||||||
|
this.internalEventBus.addEventHandler(this.userService, AccountEvent.SignedInOrRegistered)
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearInternalEventBus(): void {
|
private clearInternalEventBus(): void {
|
||||||
@@ -1348,7 +1367,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
|||||||
case AccountEvent.SignedOut: {
|
case AccountEvent.SignedOut: {
|
||||||
await this.notifyEvent(ApplicationEvent.SignedOut)
|
await this.notifyEvent(ApplicationEvent.SignedOut)
|
||||||
await this.prepareForDeinit()
|
await this.prepareForDeinit()
|
||||||
this.deinit(this.getDeinitMode(), data?.source || DeinitSource.SignOut)
|
this.deinit(this.getDeinitMode(), data?.payload.source || DeinitSource.SignOut)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
@@ -1739,4 +1758,23 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
|||||||
private createAuthenticatorManager() {
|
private createAuthenticatorManager() {
|
||||||
this.authenticatorManager = new AuthenticatorManager(this.authenticatorApiService, this.internalEventBus)
|
this.authenticatorManager = new AuthenticatorManager(this.authenticatorApiService, this.internalEventBus)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createAuthManager() {
|
||||||
|
const authServer = new AuthServer(this.httpService)
|
||||||
|
|
||||||
|
const authApiService = new AuthApiService(authServer)
|
||||||
|
|
||||||
|
this.authManager = new AuthManager(authApiService, this.internalEventBus)
|
||||||
|
}
|
||||||
|
|
||||||
|
private createUseCases() {
|
||||||
|
this._signInWithRecoveryCodes = new SignInWithRecoveryCodes(
|
||||||
|
this.authManager,
|
||||||
|
this.protocolService,
|
||||||
|
this.inMemoryStore,
|
||||||
|
this.options.crypto,
|
||||||
|
this.sessionManager,
|
||||||
|
this.internalEventBus,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
import {
|
||||||
|
AuthClientInterface,
|
||||||
|
InternalEventBusInterface,
|
||||||
|
KeyValueStoreInterface,
|
||||||
|
SessionsClientInterface,
|
||||||
|
} from '@standardnotes/services'
|
||||||
|
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||||
|
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||||
|
import { AnyKeyParamsContent } from '@standardnotes/common'
|
||||||
|
import { DecryptedPayloadInterface, RootKeyContent, RootKeyInterface } from '@standardnotes/models'
|
||||||
|
import { SessionBody } from '@standardnotes/responses'
|
||||||
|
|
||||||
|
import { SignInWithRecoveryCodes } from './SignInWithRecoveryCodes'
|
||||||
|
|
||||||
|
describe('SignInWithRecoveryCodes', () => {
|
||||||
|
let authManager: AuthClientInterface
|
||||||
|
let protocolService: EncryptionProviderInterface
|
||||||
|
let inMemoryStore: KeyValueStoreInterface<string>
|
||||||
|
let crypto: PureCryptoInterface
|
||||||
|
let sessionManager: SessionsClientInterface
|
||||||
|
let internalEventBus: InternalEventBusInterface
|
||||||
|
|
||||||
|
const createUseCase = () => new SignInWithRecoveryCodes(
|
||||||
|
authManager,
|
||||||
|
protocolService,
|
||||||
|
inMemoryStore,
|
||||||
|
crypto,
|
||||||
|
sessionManager,
|
||||||
|
internalEventBus,
|
||||||
|
)
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
authManager = {} as jest.Mocked<AuthClientInterface>
|
||||||
|
authManager.recoveryKeyParams = jest.fn().mockReturnValue({
|
||||||
|
identifier: 'test@test.te',
|
||||||
|
pw_nonce: 'pw_nonce',
|
||||||
|
created: new Date().toISOString(),
|
||||||
|
/** The event that lead to the creation of these params */
|
||||||
|
origination: 'register',
|
||||||
|
version: '004',
|
||||||
|
})
|
||||||
|
authManager.signInWithRecoveryCodes = jest.fn()
|
||||||
|
|
||||||
|
const rootKey = {
|
||||||
|
serverPassword: 'foobar',
|
||||||
|
} as jest.Mocked<RootKeyInterface>
|
||||||
|
const payload = {} as jest.Mocked<DecryptedPayloadInterface<RootKeyContent>>
|
||||||
|
payload.ejected = jest.fn().mockReturnValue({
|
||||||
|
uuid: 'uuid',
|
||||||
|
})
|
||||||
|
rootKey.payload = payload
|
||||||
|
|
||||||
|
protocolService = {} as jest.Mocked<EncryptionProviderInterface>
|
||||||
|
protocolService.hasAccount = jest.fn()
|
||||||
|
protocolService.computeRootKey = jest.fn().mockReturnValue(rootKey)
|
||||||
|
protocolService.platformSupportsKeyDerivation = jest.fn().mockReturnValue(true)
|
||||||
|
protocolService.supportedVersions = jest.fn().mockReturnValue([
|
||||||
|
'001',
|
||||||
|
'002',
|
||||||
|
'003',
|
||||||
|
'004',
|
||||||
|
])
|
||||||
|
protocolService.isVersionNewerThanLibraryVersion = jest.fn()
|
||||||
|
|
||||||
|
inMemoryStore = {} as jest.Mocked<KeyValueStoreInterface<string>>
|
||||||
|
inMemoryStore.setValue = jest.fn()
|
||||||
|
inMemoryStore.removeValue = jest.fn()
|
||||||
|
|
||||||
|
crypto = {} as jest.Mocked<PureCryptoInterface>
|
||||||
|
crypto.generateRandomKey = jest.fn()
|
||||||
|
crypto.base64URLEncode = jest.fn()
|
||||||
|
crypto.sha256 = jest.fn()
|
||||||
|
|
||||||
|
sessionManager = {} as jest.Mocked<SessionsClientInterface>
|
||||||
|
sessionManager.handleAuthentication = jest.fn()
|
||||||
|
|
||||||
|
internalEventBus = {} as jest.Mocked<InternalEventBusInterface>
|
||||||
|
internalEventBus.publishSync = jest.fn()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fail if an account already exists', async () => {
|
||||||
|
protocolService.hasAccount = jest.fn().mockReturnValue(true)
|
||||||
|
|
||||||
|
const useCase = createUseCase()
|
||||||
|
const result = await useCase.execute({ recoveryCodes: 'recovery-codes', password: 'foobar', username: 'test@test.te' })
|
||||||
|
|
||||||
|
expect(result.isFailed()).toBe(true)
|
||||||
|
expect(result.getError()).toEqual('Tried to sign in when an account already exists.')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fail if recovery key params could not be retrieved', async () => {
|
||||||
|
authManager.recoveryKeyParams = jest.fn().mockReturnValue(false)
|
||||||
|
|
||||||
|
const useCase = createUseCase()
|
||||||
|
const result = await useCase.execute({ recoveryCodes: 'recovery-codes', password: 'foobar', username: 'test@test.te' })
|
||||||
|
|
||||||
|
expect(result.isFailed()).toBe(true)
|
||||||
|
expect(result.getError()).toEqual('Could not retrieve recovery key params')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fail if key params has unsupported deriviation', async () => {
|
||||||
|
protocolService.platformSupportsKeyDerivation = jest.fn().mockReturnValue(false)
|
||||||
|
|
||||||
|
const useCase = createUseCase()
|
||||||
|
const result = await useCase.execute({ recoveryCodes: 'recovery-codes', password: 'foobar', username: 'test@test.te' })
|
||||||
|
|
||||||
|
expect(result.isFailed()).toBe(true)
|
||||||
|
expect(result.getError()).toEqual('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.')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fail if key params has unsupported version', async () => {
|
||||||
|
protocolService.isVersionNewerThanLibraryVersion = jest.fn().mockReturnValue(true)
|
||||||
|
|
||||||
|
authManager.recoveryKeyParams = jest.fn().mockReturnValue({
|
||||||
|
identifier: 'test@test.te',
|
||||||
|
pw_nonce: 'pw_nonce',
|
||||||
|
created: new Date().toISOString(),
|
||||||
|
/** The event that lead to the creation of these params */
|
||||||
|
origination: 'register',
|
||||||
|
version: '006',
|
||||||
|
})
|
||||||
|
|
||||||
|
protocolService.platformSupportsKeyDerivation = jest.fn().mockReturnValue(false)
|
||||||
|
|
||||||
|
const useCase = createUseCase()
|
||||||
|
const result = await useCase.execute({ recoveryCodes: 'recovery-codes', password: 'foobar', username: 'test@test.te' })
|
||||||
|
|
||||||
|
expect(result.isFailed()).toBe(true)
|
||||||
|
expect(result.getError()).toEqual('This version of the application does not support your newer account type. Please upgrade to the latest version of Standard Notes to sign in.')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fail if key params has expired version', async () => {
|
||||||
|
protocolService.isVersionNewerThanLibraryVersion = jest.fn().mockReturnValue(false)
|
||||||
|
|
||||||
|
authManager.recoveryKeyParams = jest.fn().mockReturnValue({
|
||||||
|
identifier: 'test@test.te',
|
||||||
|
pw_nonce: 'pw_nonce',
|
||||||
|
created: new Date().toISOString(),
|
||||||
|
/** The event that lead to the creation of these params */
|
||||||
|
origination: 'register',
|
||||||
|
version: '006',
|
||||||
|
})
|
||||||
|
|
||||||
|
protocolService.platformSupportsKeyDerivation = jest.fn().mockReturnValue(false)
|
||||||
|
|
||||||
|
const useCase = createUseCase()
|
||||||
|
const result = await useCase.execute({ recoveryCodes: 'recovery-codes', password: 'foobar', username: 'test@test.te' })
|
||||||
|
|
||||||
|
expect(result.isFailed()).toBe(true)
|
||||||
|
expect(result.getError()).toEqual('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.')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fail if the sign in with recovery codes fails', async () => {
|
||||||
|
authManager.signInWithRecoveryCodes = jest.fn().mockReturnValue(false)
|
||||||
|
|
||||||
|
const useCase = createUseCase()
|
||||||
|
const result = await useCase.execute({ recoveryCodes: 'recovery-codes', password: 'foobar', username: 'test@test.te' })
|
||||||
|
|
||||||
|
expect(result.isFailed()).toBe(true)
|
||||||
|
expect(result.getError()).toEqual('Could not sign in with recovery codes')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should sign in with recovery codes', async () => {
|
||||||
|
authManager.signInWithRecoveryCodes = jest.fn().mockReturnValue({
|
||||||
|
keyParams: {} as AnyKeyParamsContent,
|
||||||
|
session: {} as SessionBody,
|
||||||
|
user: {
|
||||||
|
uuid: '1-2-3',
|
||||||
|
email: 'test@test.te',
|
||||||
|
protocolVersion: '004',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const useCase = createUseCase()
|
||||||
|
const result = await useCase.execute({ recoveryCodes: 'recovery-codes', password: 'foobar', username: 'test@test.te' })
|
||||||
|
|
||||||
|
expect(result.isFailed()).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
|
||||||
|
import { CopyPayloadWithContentOverride } from '@standardnotes/models'
|
||||||
|
import {
|
||||||
|
AccountEvent,
|
||||||
|
AuthClientInterface,
|
||||||
|
EXPIRED_PROTOCOL_VERSION,
|
||||||
|
InternalEventBusInterface,
|
||||||
|
InternalEventPublishStrategy,
|
||||||
|
KeyValueStoreInterface,
|
||||||
|
SessionsClientInterface,
|
||||||
|
StorageKey,
|
||||||
|
UNSUPPORTED_KEY_DERIVATION,
|
||||||
|
UNSUPPORTED_PROTOCOL_VERSION,
|
||||||
|
} from '@standardnotes/services'
|
||||||
|
import { CreateAnyKeyParams, EncryptionProviderInterface, SNRootKey } from '@standardnotes/encryption'
|
||||||
|
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||||
|
|
||||||
|
import { SignInWithRecoveryCodesDTO } from './SignInWithRecoveryCodesDTO'
|
||||||
|
|
||||||
|
export class SignInWithRecoveryCodes implements UseCaseInterface<void> {
|
||||||
|
constructor(
|
||||||
|
private authManager: AuthClientInterface,
|
||||||
|
private protocolService: EncryptionProviderInterface,
|
||||||
|
private inMemoryStore: KeyValueStoreInterface<string>,
|
||||||
|
private crypto: PureCryptoInterface,
|
||||||
|
private sessionManager: SessionsClientInterface,
|
||||||
|
private internalEventBus: InternalEventBusInterface,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async execute(dto: SignInWithRecoveryCodesDTO): Promise<Result<void>> {
|
||||||
|
if (this.protocolService.hasAccount()) {
|
||||||
|
return Result.fail('Tried to sign in when an account already exists.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const codeVerifier = this.crypto.generateRandomKey(256)
|
||||||
|
this.inMemoryStore.setValue(StorageKey.CodeVerifier, codeVerifier)
|
||||||
|
|
||||||
|
const codeChallenge = this.crypto.base64URLEncode(await this.crypto.sha256(codeVerifier))
|
||||||
|
|
||||||
|
const recoveryKeyParams = await this.authManager.recoveryKeyParams({
|
||||||
|
codeChallenge,
|
||||||
|
...dto,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (recoveryKeyParams === false) {
|
||||||
|
return Result.fail('Could not retrieve recovery key params')
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootKeyParams = CreateAnyKeyParams(recoveryKeyParams)
|
||||||
|
|
||||||
|
if (!this.protocolService.supportedVersions().includes(rootKeyParams.version)) {
|
||||||
|
if (this.protocolService.isVersionNewerThanLibraryVersion(rootKeyParams.version)) {
|
||||||
|
return Result.fail(UNSUPPORTED_PROTOCOL_VERSION)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.fail(EXPIRED_PROTOCOL_VERSION)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.protocolService.platformSupportsKeyDerivation(rootKeyParams)) {
|
||||||
|
return Result.fail(UNSUPPORTED_KEY_DERIVATION)
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootKey = await this.protocolService.computeRootKey(dto.password, rootKeyParams)
|
||||||
|
|
||||||
|
const signInResult = await this.authManager.signInWithRecoveryCodes({
|
||||||
|
codeVerifier,
|
||||||
|
recoveryCodes: dto.recoveryCodes,
|
||||||
|
username: dto.username,
|
||||||
|
password: rootKey.serverPassword as string,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (signInResult === false) {
|
||||||
|
return Result.fail('Could not sign in with recovery codes')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.inMemoryStore.removeValue(StorageKey.CodeVerifier)
|
||||||
|
|
||||||
|
const expandedRootKey = new SNRootKey(
|
||||||
|
CopyPayloadWithContentOverride(rootKey.payload, {
|
||||||
|
keyParams: signInResult.keyParams,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
await this.sessionManager.handleAuthentication({
|
||||||
|
session: signInResult.session,
|
||||||
|
user: signInResult.user,
|
||||||
|
rootKey: expandedRootKey,
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.internalEventBus.publishSync(
|
||||||
|
{
|
||||||
|
type: AccountEvent.SignedInOrRegistered,
|
||||||
|
payload: {
|
||||||
|
payload: {
|
||||||
|
ephemeral: false,
|
||||||
|
mergeLocal: false,
|
||||||
|
awaitSync: true,
|
||||||
|
checkIntegrity: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
InternalEventPublishStrategy.SEQUENCE,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Result.ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export interface SignInWithRecoveryCodesDTO {
|
||||||
|
recoveryCodes: string
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { UseCaseInterface } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
|
export interface UseCaseContainerInterface {
|
||||||
|
get signInWithRecoveryCodes(): UseCaseInterface<void>
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
Challenge,
|
Challenge,
|
||||||
} from '@standardnotes/services'
|
} from '@standardnotes/services'
|
||||||
import { Base64String } from '@standardnotes/sncrypto-common'
|
import { Base64String } from '@standardnotes/sncrypto-common'
|
||||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
import { ClientDisplayableError, SessionBody } from '@standardnotes/responses'
|
||||||
import { CopyPayloadWithContentOverride } from '@standardnotes/models'
|
import { CopyPayloadWithContentOverride } from '@standardnotes/models'
|
||||||
import { isNullOrUndefined } from '@standardnotes/utils'
|
import { isNullOrUndefined } from '@standardnotes/utils'
|
||||||
import { LegacySession, MapperInterface, Session, SessionToken } from '@standardnotes/domain-core'
|
import { LegacySession, MapperInterface, Session, SessionToken } from '@standardnotes/domain-core'
|
||||||
@@ -306,7 +306,12 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
|||||||
throw new ApiCallError((registerResponse.data as HttpErrorResponseBody).error.message)
|
throw new ApiCallError((registerResponse.data as HttpErrorResponseBody).error.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.handleAuthResponse(registerResponse.data, rootKey, wrappingKey)
|
await this.handleAuthentication({
|
||||||
|
rootKey,
|
||||||
|
wrappingKey,
|
||||||
|
session: registerResponse.data.session,
|
||||||
|
user: registerResponse.data.user,
|
||||||
|
})
|
||||||
|
|
||||||
return registerResponse.data
|
return registerResponse.data
|
||||||
}
|
}
|
||||||
@@ -640,22 +645,30 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
|||||||
this.setSession(session)
|
this.setSession(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleAuthResponse(body: UserRegistrationResponseBody, rootKey: SNRootKey, wrappingKey?: SNRootKey) {
|
async handleAuthentication(dto: {
|
||||||
|
session: SessionBody
|
||||||
|
user: {
|
||||||
|
uuid: string
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
rootKey: SNRootKey
|
||||||
|
wrappingKey?: SNRootKey
|
||||||
|
}): Promise<void> {
|
||||||
const session = this.createSession(
|
const session = this.createSession(
|
||||||
body.session.access_token,
|
dto.session.access_token,
|
||||||
body.session.access_expiration,
|
dto.session.access_expiration,
|
||||||
body.session.refresh_token,
|
dto.session.refresh_token,
|
||||||
body.session.refresh_expiration,
|
dto.session.refresh_expiration,
|
||||||
body.session.readonly_access,
|
dto.session.readonly_access,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (session !== null) {
|
if (session !== null) {
|
||||||
await this.populateSession(rootKey, body.user, session, this.apiService.getHost(), wrappingKey)
|
await this.populateSession(dto.rootKey, dto.user, session, this.apiService.getHost(), dto.wrappingKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated use handleAuthResponse instead
|
* @deprecated use handleAuthentication instead
|
||||||
*/
|
*/
|
||||||
private async handleSuccessAuthResponse(
|
private async handleSuccessAuthResponse(
|
||||||
response: Responses.SignInResponse | Responses.ChangeCredentialsResponse,
|
response: Responses.SignInResponse | Responses.ChangeCredentialsResponse,
|
||||||
|
|||||||
@@ -220,6 +220,16 @@ export class AppContext {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
awaitUserPrefsSingletonResolution() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.application.preferencesService.addEventObserver((eventName) => {
|
||||||
|
if (eventName === PreferencesServiceEvent.PreferencesChanged) {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async launch({ awaitDatabaseLoad = true, receiveChallenge } = { awaitDatabaseLoad: true }) {
|
async launch({ awaitDatabaseLoad = true, receiveChallenge } = { awaitDatabaseLoad: true }) {
|
||||||
await this.application.prepareForLaunch({
|
await this.application.prepareForLaunch({
|
||||||
receiveChallenge: receiveChallenge || this.handleChallenge,
|
receiveChallenge: receiveChallenge || this.handleChallenge,
|
||||||
|
|||||||
@@ -74,10 +74,16 @@ describe('preferences', function () {
|
|||||||
await register.call(this)
|
await register.call(this)
|
||||||
await this.application.setPreference('editorLeft', 300)
|
await this.application.setPreference('editorLeft', 300)
|
||||||
await this.application.sync.sync()
|
await this.application.sync.sync()
|
||||||
this.application = await Factory.signOutApplicationAndReturnNew(this.application)
|
|
||||||
|
this.application = await this.context.signout()
|
||||||
|
|
||||||
await this.application.setPreference('editorLeft', 200)
|
await this.application.setPreference('editorLeft', 200)
|
||||||
await this.application.signIn(this.email, this.password)
|
await this.application.signIn(this.email, this.password)
|
||||||
|
|
||||||
|
const promise = this.context.awaitUserPrefsSingletonResolution()
|
||||||
await this.application.sync.sync({ awaitAll: true })
|
await this.application.sync.sync({ awaitAll: true })
|
||||||
|
await promise
|
||||||
|
|
||||||
const editorLeft = this.application.getPreference('editorLeft')
|
const editorLeft = this.application.getPreference('editorLeft')
|
||||||
expect(editorLeft).to.equal(300)
|
expect(editorLeft).to.equal(300)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6005,6 +6005,7 @@ __metadata:
|
|||||||
"@standardnotes/api": "workspace:^"
|
"@standardnotes/api": "workspace:^"
|
||||||
"@standardnotes/auth": ^3.19.4
|
"@standardnotes/auth": ^3.19.4
|
||||||
"@standardnotes/common": ^1.45.0
|
"@standardnotes/common": ^1.45.0
|
||||||
|
"@standardnotes/domain-core": ^1.11.0
|
||||||
"@standardnotes/encryption": "workspace:^"
|
"@standardnotes/encryption": "workspace:^"
|
||||||
"@standardnotes/files": "workspace:^"
|
"@standardnotes/files": "workspace:^"
|
||||||
"@standardnotes/models": "workspace:^"
|
"@standardnotes/models": "workspace:^"
|
||||||
|
|||||||
Reference in New Issue
Block a user