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:
@@ -19,6 +19,7 @@
|
||||
"@standardnotes/api": "workspace:^",
|
||||
"@standardnotes/auth": "^3.19.4",
|
||||
"@standardnotes/common": "^1.45.0",
|
||||
"@standardnotes/domain-core": "^1.11.0",
|
||||
"@standardnotes/encryption": "workspace:^",
|
||||
"@standardnotes/files": "workspace:^",
|
||||
"@standardnotes/models": "workspace:^",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ApplicationIdentifier, ContentType } from '@standardnotes/common'
|
||||
import { BackupFile, DecryptedItemInterface, ItemStream, Platform, PrefKey, PrefValue } from '@standardnotes/models'
|
||||
import { FilesClientInterface } from '@standardnotes/files'
|
||||
|
||||
import { AlertService } from '../Alert/AlertService'
|
||||
import { ComponentManagerInterface } from '../Component/ComponentManagerInterface'
|
||||
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 { ProtocolVersion } from '@standardnotes/common'
|
||||
import { SNRootKey } from '@standardnotes/encryption'
|
||||
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 { SessionManagerResponse } from './SessionManagerResponse'
|
||||
@@ -31,4 +32,13 @@ export interface SessionsClientInterface {
|
||||
wrappingKey?: RootKeyInterface
|
||||
newEmail?: string
|
||||
}): 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 { EncryptionProviderInterface, SNRootKey, SNRootKeyParams } from '@standardnotes/encryption'
|
||||
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 { UserApiServiceInterface, UserRegistrationResponseBody } from '@standardnotes/api'
|
||||
|
||||
@@ -25,6 +25,8 @@ import { DeinitSource } from '../Application/DeinitSource'
|
||||
import { StoragePersistencePolicies } from '../Storage/StorageTypes'
|
||||
import { SessionsClientInterface } from '../Session/SessionsClientInterface'
|
||||
import { ProtectionsClientInterface } from '../Protection/ProtectionClientInterface'
|
||||
import { InternalEventHandlerInterface } from '../Internal/InternalEventHandlerInterface'
|
||||
import { InternalEventInterface } from '../Internal/InternalEventInterface'
|
||||
|
||||
export type CredentialsChangeFunctionResponse = { error?: { message: string } }
|
||||
export type AccountServiceResponse = HttpResponse
|
||||
@@ -34,11 +36,25 @@ export enum AccountEvent {
|
||||
SignedOut = 'SignedOut',
|
||||
}
|
||||
|
||||
type AccountEventData = {
|
||||
export interface SignedInOrRegisteredEventPayload {
|
||||
ephemeral: boolean
|
||||
mergeLocal: boolean
|
||||
awaitSync: boolean
|
||||
checkIntegrity: boolean
|
||||
}
|
||||
|
||||
export interface SignedOutEventPayload {
|
||||
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 registering = false
|
||||
|
||||
@@ -60,6 +76,43 @@ export class UserService extends AbstractService<AccountEvent, AccountEventData>
|
||||
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 {
|
||||
super.deinit()
|
||||
;(this.sessionManager as unknown) = undefined
|
||||
@@ -97,27 +150,17 @@ export class UserService extends AbstractService<AccountEvent, AccountEventData>
|
||||
this.lockSyncing()
|
||||
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
|
||||
|
||||
await this.syncService.downloadFirstSync(300)
|
||||
void this.protocolService.decryptErroredPayloads()
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
this.unlockSyncing()
|
||||
@@ -156,39 +199,15 @@ export class UserService extends AbstractService<AccountEvent, AccountEventData>
|
||||
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, {
|
||||
const notifyingFunction = awaitSync ? this.notifyEventSync.bind(this) : this.notifyEvent.bind(this)
|
||||
await notifyingFunction(AccountEvent.SignedInOrRegistered, {
|
||||
payload: {
|
||||
mergeLocal,
|
||||
awaitSync,
|
||||
ephemeral,
|
||||
checkIntegrity: true,
|
||||
awaitAll: awaitSync,
|
||||
})
|
||||
.then(() => {
|
||||
if (!awaitSync) {
|
||||
void this.protocolService.decryptErroredPayloads()
|
||||
}
|
||||
})
|
||||
|
||||
if (awaitSync) {
|
||||
await syncPromise
|
||||
|
||||
await this.protocolService.decryptErroredPayloads()
|
||||
}
|
||||
},
|
||||
})
|
||||
} else {
|
||||
this.unlockSyncing()
|
||||
}
|
||||
@@ -267,15 +286,14 @@ export class UserService extends AbstractService<AccountEvent, AccountEventData>
|
||||
)
|
||||
|
||||
if (!response.error) {
|
||||
await this.notifyEvent(AccountEvent.SignedInOrRegistered)
|
||||
|
||||
this.unlockSyncing()
|
||||
|
||||
void this.syncService.downloadFirstSync(1_000, {
|
||||
checkIntegrity: true,
|
||||
await this.notifyEvent(AccountEvent.SignedInOrRegistered, {
|
||||
payload: {
|
||||
mergeLocal: true,
|
||||
awaitSync: true,
|
||||
ephemeral: false,
|
||||
checkIntegrity: true,
|
||||
},
|
||||
})
|
||||
|
||||
void this.protocolService.decryptErroredPayloads()
|
||||
}
|
||||
|
||||
this.unlockSyncing()
|
||||
@@ -310,7 +328,7 @@ export class UserService extends AbstractService<AccountEvent, AccountEventData>
|
||||
await this.sessionManager.signOut()
|
||||
await this.protocolService.deleteWorkspaceSpecificKeyStateFromDevice()
|
||||
await this.storageService.clearAllData()
|
||||
await this.notifyEvent(AccountEvent.SignedOut, { source })
|
||||
await this.notifyEvent(AccountEvent.SignedOut, { payload: { source } })
|
||||
}
|
||||
|
||||
if (force) {
|
||||
@@ -473,7 +491,14 @@ export class UserService extends AbstractService<AccountEvent, AccountEventData>
|
||||
|
||||
public async populateSessionFromDemoShareToken(token: Base64String): Promise<void> {
|
||||
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) {
|
||||
|
||||
@@ -6,6 +6,8 @@ export * from './Application/ApplicationStage'
|
||||
export * from './Application/DeinitCallback'
|
||||
export * from './Application/DeinitSource'
|
||||
export * from './Application/DeinitMode'
|
||||
export * from './Auth/AuthClientInterface'
|
||||
export * from './Auth/AuthManager'
|
||||
export * from './Authenticator/AuthenticatorClientInterface'
|
||||
export * from './Authenticator/AuthenticatorManager'
|
||||
export * from './User/UserClientInterface'
|
||||
|
||||
Reference in New Issue
Block a user