internal: change password preprocessing step (#2347)
This commit is contained in:
@@ -2,6 +2,11 @@ export enum AsymmetricMessagePayloadType {
|
|||||||
ContactShare = 'contact-share',
|
ContactShare = 'contact-share',
|
||||||
SharedVaultRootKeyChanged = 'shared-vault-root-key-changed',
|
SharedVaultRootKeyChanged = 'shared-vault-root-key-changed',
|
||||||
SenderKeypairChanged = 'sender-keypair-changed',
|
SenderKeypairChanged = 'sender-keypair-changed',
|
||||||
SharedVaultInvite = 'shared-vault-invite',
|
|
||||||
SharedVaultMetadataChanged = 'shared-vault-metadata-changed',
|
SharedVaultMetadataChanged = 'shared-vault-metadata-changed',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared Vault Invites conform to the asymmetric message protocol, but are sent via the dedicated
|
||||||
|
* SharedVaultInvite model and not the AsymmetricMessage model on the server side.
|
||||||
|
*/
|
||||||
|
SharedVaultInvite = 'shared-vault-invite',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AsymmetricMessageServiceInterface } from './../AsymmetricMessage/AsymmetricMessageServiceInterface'
|
||||||
import { SyncOptions } from './../Sync/SyncOptions'
|
import { SyncOptions } from './../Sync/SyncOptions'
|
||||||
import { ImportDataReturnType } from './../Mutator/ImportDataUseCase'
|
import { ImportDataReturnType } from './../Mutator/ImportDataUseCase'
|
||||||
import { ChallengeServiceInterface } from './../Challenge/ChallengeServiceInterface'
|
import { ChallengeServiceInterface } from './../Challenge/ChallengeServiceInterface'
|
||||||
@@ -102,6 +103,8 @@ export interface ApplicationInterface {
|
|||||||
get vaults(): VaultServiceInterface
|
get vaults(): VaultServiceInterface
|
||||||
get challenges(): ChallengeServiceInterface
|
get challenges(): ChallengeServiceInterface
|
||||||
get alerts(): AlertService
|
get alerts(): AlertService
|
||||||
|
get asymmetric(): AsymmetricMessageServiceInterface
|
||||||
|
|
||||||
readonly identifier: ApplicationIdentifier
|
readonly identifier: ApplicationIdentifier
|
||||||
readonly platform: Platform
|
readonly platform: Platform
|
||||||
deviceInterface: DeviceInterface
|
deviceInterface: DeviceInterface
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
|
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
|
||||||
import { ContactServiceInterface } from './../Contacts/ContactServiceInterface'
|
import { ContactServiceInterface } from './../Contacts/ContactServiceInterface'
|
||||||
import { AsymmetricMessageServerHash, ClientDisplayableError } from '@standardnotes/responses'
|
import { AsymmetricMessageServerHash, ClientDisplayableError, isClientDisplayableError } from '@standardnotes/responses'
|
||||||
import { SyncEvent, SyncEventReceivedAsymmetricMessagesData } from '../Event/SyncEvent'
|
import { SyncEvent, SyncEventReceivedAsymmetricMessagesData } from '../Event/SyncEvent'
|
||||||
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
|
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
|
||||||
import { InternalEventHandlerInterface } from '../Internal/InternalEventHandlerInterface'
|
import { InternalEventHandlerInterface } from '../Internal/InternalEventHandlerInterface'
|
||||||
@@ -27,8 +27,12 @@ import { SendOwnContactChangeMessage } from './UseCase/SendOwnContactChangeMessa
|
|||||||
import { GetOutboundAsymmetricMessages } from './UseCase/GetOutboundAsymmetricMessages'
|
import { GetOutboundAsymmetricMessages } from './UseCase/GetOutboundAsymmetricMessages'
|
||||||
import { GetInboundAsymmetricMessages } from './UseCase/GetInboundAsymmetricMessages'
|
import { GetInboundAsymmetricMessages } from './UseCase/GetInboundAsymmetricMessages'
|
||||||
import { GetVaultUseCase } from '../Vaults/UseCase/GetVault'
|
import { GetVaultUseCase } from '../Vaults/UseCase/GetVault'
|
||||||
|
import { AsymmetricMessageServiceInterface } from './AsymmetricMessageServiceInterface'
|
||||||
|
|
||||||
export class AsymmetricMessageService extends AbstractService implements InternalEventHandlerInterface {
|
export class AsymmetricMessageService
|
||||||
|
extends AbstractService
|
||||||
|
implements AsymmetricMessageServiceInterface, InternalEventHandlerInterface
|
||||||
|
{
|
||||||
private messageServer: AsymmetricMessageServer
|
private messageServer: AsymmetricMessageServer
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -69,7 +73,16 @@ export class AsymmetricMessageService extends AbstractService implements Interna
|
|||||||
return usecase.execute()
|
return usecase.execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendOwnContactChangeEventToAllContacts(data: UserKeyPairChangedEventData): Promise<void> {
|
public async downloadAndProcessInboundMessages(): Promise<void> {
|
||||||
|
const messages = await this.getInboundMessages()
|
||||||
|
if (isClientDisplayableError(messages)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.handleRemoteReceivedAsymmetricMessages(messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sendOwnContactChangeEventToAllContacts(data: UserKeyPairChangedEventData): Promise<void> {
|
||||||
if (!data.oldKeyPair || !data.oldSigningKeyPair) {
|
if (!data.oldKeyPair || !data.oldSigningKeyPair) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { AsymmetricMessageServerHash, ClientDisplayableError } from '@standardnotes/responses'
|
||||||
|
|
||||||
|
export interface AsymmetricMessageServiceInterface {
|
||||||
|
getOutboundMessages(): Promise<AsymmetricMessageServerHash[] | ClientDisplayableError>
|
||||||
|
getInboundMessages(): Promise<AsymmetricMessageServerHash[] | ClientDisplayableError>
|
||||||
|
downloadAndProcessInboundMessages(): Promise<void>
|
||||||
|
}
|
||||||
@@ -237,7 +237,7 @@ export class ContactService
|
|||||||
}
|
}
|
||||||
|
|
||||||
findTrustedContactForInvite(invite: SharedVaultInviteServerHash): TrustedContactInterface | undefined {
|
findTrustedContactForInvite(invite: SharedVaultInviteServerHash): TrustedContactInterface | undefined {
|
||||||
return this.findTrustedContact(invite.user_uuid)
|
return this.findTrustedContact(invite.sender_uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
getCollaborationIDForTrustedContact(contact: TrustedContactInterface): string {
|
getCollaborationIDForTrustedContact(contact: TrustedContactInterface): string {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { PublicKeySet } from '@standardnotes/encryption'
|
|||||||
|
|
||||||
export class SelfContactManager {
|
export class SelfContactManager {
|
||||||
public selfContact?: TrustedContactInterface
|
public selfContact?: TrustedContactInterface
|
||||||
private shouldReloadSelfContact = true
|
|
||||||
private isReloadingSelfContact = false
|
private isReloadingSelfContact = false
|
||||||
private eventDisposers: (() => void)[] = []
|
private eventDisposers: (() => void)[] = []
|
||||||
|
|
||||||
@@ -32,16 +32,14 @@ export class SelfContactManager {
|
|||||||
private session: SessionsClientInterface,
|
private session: SessionsClientInterface,
|
||||||
private singletons: SingletonManagerInterface,
|
private singletons: SingletonManagerInterface,
|
||||||
) {
|
) {
|
||||||
this.eventDisposers.push(
|
|
||||||
items.addObserver(ContentType.TrustedContact, () => {
|
|
||||||
this.shouldReloadSelfContact = true
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
this.eventDisposers.push(
|
this.eventDisposers.push(
|
||||||
sync.addEventObserver((event) => {
|
sync.addEventObserver((event) => {
|
||||||
if (event === SyncEvent.SyncCompletedWithAllItemsUploaded || event === SyncEvent.LocalDataIncrementalLoad) {
|
if (event === SyncEvent.LocalDataIncrementalLoad) {
|
||||||
void this.reloadSelfContact()
|
this.loadSelfContactFromDatabase()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event === SyncEvent.SyncCompletedWithAllItemsUploaded) {
|
||||||
|
void this.reloadSelfContactAndCreateIfNecessary()
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -49,13 +47,21 @@ export class SelfContactManager {
|
|||||||
|
|
||||||
public async handleApplicationStage(stage: ApplicationStage): Promise<void> {
|
public async handleApplicationStage(stage: ApplicationStage): Promise<void> {
|
||||||
if (stage === ApplicationStage.LoadedDatabase_12) {
|
if (stage === ApplicationStage.LoadedDatabase_12) {
|
||||||
this.selfContact = this.singletons.findSingleton<TrustedContactInterface>(
|
this.loadSelfContactFromDatabase()
|
||||||
ContentType.UserPrefs,
|
|
||||||
TrustedContact.singletonPredicate,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private loadSelfContactFromDatabase(): void {
|
||||||
|
if (this.selfContact) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selfContact = this.singletons.findSingleton<TrustedContactInterface>(
|
||||||
|
ContentType.TrustedContact,
|
||||||
|
TrustedContact.singletonPredicate,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
public async updateWithNewPublicKeySet(publicKeySet: PublicKeySet) {
|
public async updateWithNewPublicKeySet(publicKeySet: PublicKeySet) {
|
||||||
if (!InternalFeatureService.get().isFeatureEnabled(InternalFeature.Vaults)) {
|
if (!InternalFeatureService.get().isFeatureEnabled(InternalFeature.Vaults)) {
|
||||||
return
|
return
|
||||||
@@ -74,12 +80,16 @@ export class SelfContactManager {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private async reloadSelfContact() {
|
private async reloadSelfContactAndCreateIfNecessary() {
|
||||||
if (!InternalFeatureService.get().isFeatureEnabled(InternalFeature.Vaults)) {
|
if (!InternalFeatureService.get().isFeatureEnabled(InternalFeature.Vaults)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.shouldReloadSelfContact || this.isReloadingSelfContact) {
|
if (this.selfContact) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isReloadingSelfContact) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,17 +115,13 @@ export class SelfContactManager {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
this.selfContact = await this.singletons.findOrCreateSingleton<TrustedContactContent, TrustedContact>(
|
||||||
this.selfContact = await this.singletons.findOrCreateSingleton<TrustedContactContent, TrustedContact>(
|
TrustedContact.singletonPredicate,
|
||||||
TrustedContact.singletonPredicate,
|
ContentType.TrustedContact,
|
||||||
ContentType.TrustedContact,
|
FillItemContent<TrustedContactContent>(content),
|
||||||
FillItemContent<TrustedContactContent>(content),
|
)
|
||||||
)
|
|
||||||
|
|
||||||
this.shouldReloadSelfContact = false
|
this.isReloadingSelfContact = false
|
||||||
} finally {
|
|
||||||
this.isReloadingSelfContact = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit() {
|
deinit() {
|
||||||
|
|||||||
@@ -283,6 +283,7 @@ export class EncryptionService
|
|||||||
const usecase = new CreateNewItemsKeyWithRollbackUseCase(
|
const usecase = new CreateNewItemsKeyWithRollbackUseCase(
|
||||||
this.mutator,
|
this.mutator,
|
||||||
this.items,
|
this.items,
|
||||||
|
this.storage,
|
||||||
this.operators,
|
this.operators,
|
||||||
this.rootKeyManager,
|
this.rootKeyManager,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import { StorageServiceInterface } from './../../../Storage/StorageServiceInterface'
|
||||||
import { ItemsKeyMutator, OperatorManager, findDefaultItemsKey } from '@standardnotes/encryption'
|
import { ItemsKeyMutator, OperatorManager, findDefaultItemsKey } from '@standardnotes/encryption'
|
||||||
import { MutatorClientInterface } from '../../../Mutator/MutatorClientInterface'
|
import { MutatorClientInterface } from '../../../Mutator/MutatorClientInterface'
|
||||||
import { ItemManagerInterface } from '../../../Item/ItemManagerInterface'
|
import { ItemManagerInterface } from '../../../Item/ItemManagerInterface'
|
||||||
import { RootKeyManager } from '../../RootKey/RootKeyManager'
|
import { RootKeyManager } from '../../RootKey/RootKeyManager'
|
||||||
import { CreateNewDefaultItemsKeyUseCase } from './CreateNewDefaultItemsKey'
|
import { CreateNewDefaultItemsKeyUseCase } from './CreateNewDefaultItemsKey'
|
||||||
|
import { RemoveItemsLocallyUseCase } from '../../../UseCase/RemoveItemsLocally'
|
||||||
|
|
||||||
export class CreateNewItemsKeyWithRollbackUseCase {
|
export class CreateNewItemsKeyWithRollbackUseCase {
|
||||||
private createDefaultItemsKeyUseCase = new CreateNewDefaultItemsKeyUseCase(
|
private createDefaultItemsKeyUseCase = new CreateNewDefaultItemsKeyUseCase(
|
||||||
@@ -12,9 +14,12 @@ export class CreateNewItemsKeyWithRollbackUseCase {
|
|||||||
this.rootKeyManager,
|
this.rootKeyManager,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private removeItemsLocallyUsecase = new RemoveItemsLocallyUseCase(this.items, this.storage)
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private mutator: MutatorClientInterface,
|
private mutator: MutatorClientInterface,
|
||||||
private items: ItemManagerInterface,
|
private items: ItemManagerInterface,
|
||||||
|
private storage: StorageServiceInterface,
|
||||||
private operatorManager: OperatorManager,
|
private operatorManager: OperatorManager,
|
||||||
private rootKeyManager: RootKeyManager,
|
private rootKeyManager: RootKeyManager,
|
||||||
) {}
|
) {}
|
||||||
@@ -24,7 +29,7 @@ export class CreateNewItemsKeyWithRollbackUseCase {
|
|||||||
const newDefaultItemsKey = await this.createDefaultItemsKeyUseCase.execute()
|
const newDefaultItemsKey = await this.createDefaultItemsKeyUseCase.execute()
|
||||||
|
|
||||||
const rollback = async () => {
|
const rollback = async () => {
|
||||||
await this.mutator.setItemToBeDeleted(newDefaultItemsKey)
|
await this.removeItemsLocallyUsecase.execute([newDefaultItemsKey])
|
||||||
|
|
||||||
if (currentDefaultItemsKey) {
|
if (currentDefaultItemsKey) {
|
||||||
await this.mutator.changeItem<ItemsKeyMutator>(currentDefaultItemsKey, (mutator) => {
|
await this.mutator.changeItem<ItemsKeyMutator>(currentDefaultItemsKey, (mutator) => {
|
||||||
|
|||||||
@@ -111,7 +111,9 @@ export class SharedVaultService
|
|||||||
)
|
)
|
||||||
|
|
||||||
this.eventDisposers.push(
|
this.eventDisposers.push(
|
||||||
items.addObserver<TrustedContactInterface>(ContentType.TrustedContact, ({ changed, inserted, source }) => {
|
items.addObserver<TrustedContactInterface>(ContentType.TrustedContact, async ({ changed, inserted, source }) => {
|
||||||
|
await this.reprocessCachedInvitesTrustStatusAfterTrustedContactsChange()
|
||||||
|
|
||||||
if (source === PayloadEmitSource.LocalChanged && inserted.length > 0) {
|
if (source === PayloadEmitSource.LocalChanged && inserted.length > 0) {
|
||||||
void this.handleCreationOfNewTrustedContacts(inserted)
|
void this.handleCreationOfNewTrustedContacts(inserted)
|
||||||
}
|
}
|
||||||
@@ -250,8 +252,6 @@ export class SharedVaultService
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async handleTrustedContactsChange(contacts: TrustedContactInterface[]): Promise<void> {
|
private async handleTrustedContactsChange(contacts: TrustedContactInterface[]): Promise<void> {
|
||||||
await this.reprocessCachedInvitesTrustStatusAfterTrustedContactsChange()
|
|
||||||
|
|
||||||
for (const contact of contacts) {
|
for (const contact of contacts) {
|
||||||
await this.shareContactWithUserAdministeredSharedVaults(contact)
|
await this.shareContactWithUserAdministeredSharedVaults(contact)
|
||||||
}
|
}
|
||||||
@@ -328,28 +328,9 @@ export class SharedVaultService
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async reprocessCachedInvitesTrustStatusAfterTrustedContactsChange(): Promise<void> {
|
private async reprocessCachedInvitesTrustStatusAfterTrustedContactsChange(): Promise<void> {
|
||||||
const cachedInvites = this.getCachedPendingInviteRecords()
|
const cachedInvites = this.getCachedPendingInviteRecords().map((record) => record.invite)
|
||||||
|
|
||||||
for (const record of cachedInvites) {
|
await this.processInboundInvites(cachedInvites)
|
||||||
if (record.trusted) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const trustedMessageUseCase = new GetAsymmetricMessageTrustedPayload<AsymmetricMessageSharedVaultInvite>(
|
|
||||||
this.encryption,
|
|
||||||
this.contacts,
|
|
||||||
)
|
|
||||||
|
|
||||||
const trustedMessage = trustedMessageUseCase.execute({
|
|
||||||
message: record.invite,
|
|
||||||
privateKey: this.encryption.getKeyPair().privateKey,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (trustedMessage) {
|
|
||||||
record.message = trustedMessage
|
|
||||||
record.trusted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async processInboundInvites(invites: SharedVaultInviteServerHash[]): Promise<void> {
|
private async processInboundInvites(invites: SharedVaultInviteServerHash[]): Promise<void> {
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ import { StorageServiceInterface } from '../../Storage/StorageServiceInterface'
|
|||||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||||
import { ItemManagerInterface } from '../../Item/ItemManagerInterface'
|
import { ItemManagerInterface } from '../../Item/ItemManagerInterface'
|
||||||
import { AnyItemInterface, VaultListingInterface } from '@standardnotes/models'
|
import { AnyItemInterface, VaultListingInterface } from '@standardnotes/models'
|
||||||
import { Uuids } from '@standardnotes/utils'
|
|
||||||
import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface'
|
import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface'
|
||||||
|
import { RemoveItemsLocallyUseCase } from '../../UseCase/RemoveItemsLocally'
|
||||||
|
|
||||||
export class DeleteExternalSharedVaultUseCase {
|
export class DeleteExternalSharedVaultUseCase {
|
||||||
|
private removeItemsLocallyUsecase = new RemoveItemsLocallyUseCase(this.items, this.storage)
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private items: ItemManagerInterface,
|
private items: ItemManagerInterface,
|
||||||
private mutator: MutatorClientInterface,
|
private mutator: MutatorClientInterface,
|
||||||
@@ -28,15 +30,13 @@ export class DeleteExternalSharedVaultUseCase {
|
|||||||
* The data will be removed locally without syncing the items
|
* The data will be removed locally without syncing the items
|
||||||
*/
|
*/
|
||||||
private async deleteDataSharedByVaultUsers(vault: VaultListingInterface): Promise<void> {
|
private async deleteDataSharedByVaultUsers(vault: VaultListingInterface): Promise<void> {
|
||||||
const vaultItems = this.items
|
const vaultItems = <AnyItemInterface[]>(
|
||||||
.allTrackedItems()
|
this.items.allTrackedItems().filter((item) => item.key_system_identifier === vault.systemIdentifier)
|
||||||
.filter((item) => item.key_system_identifier === vault.systemIdentifier)
|
)
|
||||||
this.items.removeItemsLocally(vaultItems as AnyItemInterface[])
|
|
||||||
|
|
||||||
const itemsKeys = this.encryption.keys.getKeySystemItemsKeys(vault.systemIdentifier)
|
const itemsKeys = this.encryption.keys.getKeySystemItemsKeys(vault.systemIdentifier)
|
||||||
this.items.removeItemsLocally(itemsKeys)
|
|
||||||
|
|
||||||
await this.storage.deletePayloadsWithUuids([...Uuids(vaultItems), ...Uuids(itemsKeys)])
|
await this.removeItemsLocallyUsecase.execute([...vaultItems, ...itemsKeys])
|
||||||
}
|
}
|
||||||
|
|
||||||
private async deleteDataOwnedByThisUser(vault: VaultListingInterface): Promise<void> {
|
private async deleteDataOwnedByThisUser(vault: VaultListingInterface): Promise<void> {
|
||||||
|
|||||||
14
packages/services/src/Domain/UseCase/RemoveItemsLocally.ts
Normal file
14
packages/services/src/Domain/UseCase/RemoveItemsLocally.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { StorageServiceInterface } from '../Storage/StorageServiceInterface'
|
||||||
|
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
|
||||||
|
import { AnyItemInterface } from '@standardnotes/models'
|
||||||
|
import { Uuids } from '@standardnotes/utils'
|
||||||
|
|
||||||
|
export class RemoveItemsLocallyUseCase {
|
||||||
|
constructor(private readonly items: ItemManagerInterface, private readonly storage: StorageServiceInterface) {}
|
||||||
|
|
||||||
|
async execute(items: AnyItemInterface[]): Promise<void> {
|
||||||
|
this.items.removeItemsLocally(items)
|
||||||
|
|
||||||
|
await this.storage.deletePayloadsWithUuids(Uuids(items))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -589,7 +589,6 @@ export class UserService
|
|||||||
|
|
||||||
this.lockSyncing()
|
this.lockSyncing()
|
||||||
|
|
||||||
/** Now, change the credentials on the server. Roll back on failure */
|
|
||||||
const { response } = await this.sessionManager.changeCredentials({
|
const { response } = await this.sessionManager.changeCredentials({
|
||||||
currentServerPassword: currentRootKey.serverPassword as string,
|
currentServerPassword: currentRootKey.serverPassword as string,
|
||||||
newRootKey: newRootKey,
|
newRootKey: newRootKey,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export * from './Application/DeinitMode'
|
|||||||
export * from './Application/DeinitSource'
|
export * from './Application/DeinitSource'
|
||||||
|
|
||||||
export * from './AsymmetricMessage/AsymmetricMessageService'
|
export * from './AsymmetricMessage/AsymmetricMessageService'
|
||||||
|
export * from './AsymmetricMessage/AsymmetricMessageServiceInterface'
|
||||||
|
|
||||||
export * from './Auth/AuthClientInterface'
|
export * from './Auth/AuthClientInterface'
|
||||||
export * from './Auth/AuthManager'
|
export * from './Auth/AuthManager'
|
||||||
|
|||||||
@@ -387,6 +387,10 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
|||||||
return this.challengeService
|
return this.challengeService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get asymmetric(): ExternalServices.AsymmetricMessageServiceInterface {
|
||||||
|
return this.asymmetricMessageService
|
||||||
|
}
|
||||||
|
|
||||||
get homeServer(): ExternalServices.HomeServerServiceInterface | undefined {
|
get homeServer(): ExternalServices.HomeServerServiceInterface | undefined {
|
||||||
return this.homeServerService
|
return this.homeServerService
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
PayloadEmitSource,
|
PayloadEmitSource,
|
||||||
EncryptedItemInterface,
|
EncryptedItemInterface,
|
||||||
getIncrementedDirtyIndex,
|
getIncrementedDirtyIndex,
|
||||||
|
ContentTypeUsesRootKeyEncryption,
|
||||||
} from '@standardnotes/models'
|
} from '@standardnotes/models'
|
||||||
import { SNSyncService } from '../Sync/SyncService'
|
import { SNSyncService } from '../Sync/SyncService'
|
||||||
import { DiskStorageService } from '../Storage/DiskStorageService'
|
import { DiskStorageService } from '../Storage/DiskStorageService'
|
||||||
@@ -187,6 +188,10 @@ export class SNKeyRecoveryService extends AbstractService<KeyRecoveryEvent, Decr
|
|||||||
}
|
}
|
||||||
|
|
||||||
public canAttemptDecryptionOfItem(item: EncryptedItemInterface): ClientDisplayableError | true {
|
public canAttemptDecryptionOfItem(item: EncryptedItemInterface): ClientDisplayableError | true {
|
||||||
|
if (ContentTypeUsesRootKeyEncryption(item.content_type)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
const keyId = item.payload.items_key_id
|
const keyId = item.payload.items_key_id
|
||||||
|
|
||||||
if (!keyId) {
|
if (!keyId) {
|
||||||
|
|||||||
@@ -437,6 +437,30 @@ describe('basic auth', function () {
|
|||||||
expect(performSignIn.callCount).to.equal(1)
|
expect(performSignIn.callCount).to.equal(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should rollback password change if fails to sync new items key', async function () {
|
||||||
|
/** Should delete the new items key locally without marking it as deleted so that it doesn't sync */
|
||||||
|
await this.context.register()
|
||||||
|
|
||||||
|
const originalImpl = this.application.encryptionService.getSureDefaultItemsKey
|
||||||
|
this.application.encryptionService.getSureDefaultItemsKey = () => {
|
||||||
|
return {
|
||||||
|
neverSynced: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mutatorSpy = sinon.spy(this.application.mutator, 'setItemToBeDeleted')
|
||||||
|
const removeItemsSpy = sinon.spy(this.application.items, 'removeItemsLocally')
|
||||||
|
const deletePayloadsSpy = sinon.spy(this.application.storage, 'deletePayloadsWithUuids')
|
||||||
|
|
||||||
|
await this.context.changePassword('new-password')
|
||||||
|
|
||||||
|
this.application.encryptionService.getSureDefaultItemsKey = originalImpl
|
||||||
|
|
||||||
|
expect(mutatorSpy.callCount).to.equal(0)
|
||||||
|
expect(removeItemsSpy.callCount).to.equal(1)
|
||||||
|
expect(deletePayloadsSpy.callCount).to.equal(1)
|
||||||
|
})
|
||||||
|
|
||||||
describe('add passcode', function () {
|
describe('add passcode', function () {
|
||||||
it('should set passcode successfully', async function () {
|
it('should set passcode successfully', async function () {
|
||||||
const passcode = 'passcode'
|
const passcode = 'passcode'
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ import {
|
|||||||
Environment,
|
Environment,
|
||||||
ApplicationOptionsDefaults,
|
ApplicationOptionsDefaults,
|
||||||
BackupServiceInterface,
|
BackupServiceInterface,
|
||||||
|
InternalFeatureService,
|
||||||
|
InternalFeatureServiceInterface,
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
import { makeObservable, observable } from 'mobx'
|
import { makeObservable, observable } from 'mobx'
|
||||||
import { startAuthentication, startRegistration } from '@simplewebauthn/browser'
|
import { startAuthentication, startRegistration } from '@simplewebauthn/browser'
|
||||||
@@ -263,6 +265,10 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getInternalFeatureService(): InternalFeatureServiceInterface {
|
||||||
|
return InternalFeatureService.get()
|
||||||
|
}
|
||||||
|
|
||||||
isNativeIOS() {
|
isNativeIOS() {
|
||||||
return this.isNativeMobileWeb() && this.platform === Platform.Ios
|
return this.isNativeMobileWeb() && this.platform === Platform.Ios
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { CheckmarkCircle } from '../UIElements/CheckmarkCircle'
|
||||||
|
|
||||||
|
export const FinishStep = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex flex-row items-start gap-3">
|
||||||
|
<div className="pt-1">
|
||||||
|
<CheckmarkCircle />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="text-base font-bold">Your password has been successfully changed.</div>
|
||||||
|
<p>Ensure you are running the latest version of Standard Notes on all platforms for maximum compatibility.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import DecoratedPasswordInput from '../Input/DecoratedPasswordInput'
|
||||||
|
|
||||||
|
export const PasswordStep = ({
|
||||||
|
onCurrentPasswordChange,
|
||||||
|
onNewPasswordChange,
|
||||||
|
onNewPasswordConfirmationChange,
|
||||||
|
}: {
|
||||||
|
onCurrentPasswordChange: (value: string) => void
|
||||||
|
onNewPasswordChange: (value: string) => void
|
||||||
|
onNewPasswordConfirmationChange: (value: string) => void
|
||||||
|
}) => {
|
||||||
|
const [currentPassword, setCurrentPassword] = useState<string>('')
|
||||||
|
const [newPassword, setNewPassword] = useState<string>('')
|
||||||
|
const [newPasswordConfirmation, setNewPasswordConfirmation] = useState<string>('')
|
||||||
|
|
||||||
|
const handleCurrentPasswordChange = (value: string) => {
|
||||||
|
setCurrentPassword(value)
|
||||||
|
onCurrentPasswordChange(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNewPasswordChange = (value: string) => {
|
||||||
|
setNewPassword(value)
|
||||||
|
onNewPasswordChange(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNewPasswordConfirmationChange = (value: string) => {
|
||||||
|
setNewPasswordConfirmation(value)
|
||||||
|
onNewPasswordConfirmationChange(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col pb-1.5">
|
||||||
|
<form>
|
||||||
|
<label htmlFor="password-wiz-current-password" className="mb-1 block">
|
||||||
|
Current Password
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<DecoratedPasswordInput
|
||||||
|
autofocus={true}
|
||||||
|
id="password-wiz-current-password"
|
||||||
|
value={currentPassword}
|
||||||
|
onChange={handleCurrentPasswordChange}
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="min-h-2" />
|
||||||
|
|
||||||
|
<label htmlFor="password-wiz-new-password" className="mb-1 block">
|
||||||
|
New Password
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<DecoratedPasswordInput
|
||||||
|
id="password-wiz-new-password"
|
||||||
|
value={newPassword}
|
||||||
|
onChange={handleNewPasswordChange}
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="min-h-2" />
|
||||||
|
|
||||||
|
<label htmlFor="password-wiz-confirm-new-password" className="mb-1 block">
|
||||||
|
Confirm New Password
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<DecoratedPasswordInput
|
||||||
|
id="password-wiz-confirm-new-password"
|
||||||
|
value={newPasswordConfirmation}
|
||||||
|
onChange={handleNewPasswordConfirmationChange}
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import { WebApplication } from '@/Application/WebApplication'
|
import { WebApplication } from '@/Application/WebApplication'
|
||||||
import { createRef } from 'react'
|
|
||||||
import { AbstractComponent } from '@/Components/Abstract/PureComponent'
|
import { AbstractComponent } from '@/Components/Abstract/PureComponent'
|
||||||
import DecoratedPasswordInput from '../Input/DecoratedPasswordInput'
|
|
||||||
import Modal from '../Modal/Modal'
|
import Modal from '../Modal/Modal'
|
||||||
import { isMobileScreen } from '@/Utils'
|
import { isMobileScreen } from '@/Utils'
|
||||||
import Spinner from '../Spinner/Spinner'
|
import Spinner from '../Spinner/Spinner'
|
||||||
|
import { PasswordStep } from './PasswordStep'
|
||||||
|
import { FinishStep } from './FinishStep'
|
||||||
|
import { PreprocessingStep } from './PreprocessingStep'
|
||||||
|
import { featureTrunkVaultsEnabled } from '@/FeatureTrunk'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
application: WebApplication
|
application: WebApplication
|
||||||
@@ -19,7 +21,6 @@ type State = {
|
|||||||
processing?: boolean
|
processing?: boolean
|
||||||
showSpinner?: boolean
|
showSpinner?: boolean
|
||||||
step: Steps
|
step: Steps
|
||||||
title: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_CONTINUE_TITLE = 'Continue'
|
const DEFAULT_CONTINUE_TITLE = 'Continue'
|
||||||
@@ -27,8 +28,9 @@ const GENERATING_CONTINUE_TITLE = 'Generating Keys...'
|
|||||||
const FINISH_CONTINUE_TITLE = 'Finish'
|
const FINISH_CONTINUE_TITLE = 'Finish'
|
||||||
|
|
||||||
enum Steps {
|
enum Steps {
|
||||||
PasswordStep = 1,
|
PreprocessingStep = 'preprocessing-step',
|
||||||
FinishStep = 2,
|
PasswordStep = 'password-step',
|
||||||
|
FinishStep = 'finish-step',
|
||||||
}
|
}
|
||||||
|
|
||||||
type FormData = {
|
type FormData = {
|
||||||
@@ -39,22 +41,32 @@ type FormData = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class PasswordWizard extends AbstractComponent<Props, State> {
|
class PasswordWizard extends AbstractComponent<Props, State> {
|
||||||
private currentPasswordInput = createRef<HTMLInputElement>()
|
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props, props.application)
|
super(props, props.application)
|
||||||
this.registerWindowUnloadStopper()
|
this.registerWindowUnloadStopper()
|
||||||
this.state = {
|
|
||||||
|
const baseState = {
|
||||||
formData: {},
|
formData: {},
|
||||||
continueTitle: DEFAULT_CONTINUE_TITLE,
|
continueTitle: DEFAULT_CONTINUE_TITLE,
|
||||||
step: Steps.PasswordStep,
|
}
|
||||||
title: 'Change Password',
|
|
||||||
|
if (featureTrunkVaultsEnabled()) {
|
||||||
|
this.state = {
|
||||||
|
...baseState,
|
||||||
|
lockContinue: true,
|
||||||
|
step: Steps.PreprocessingStep,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.state = {
|
||||||
|
...baseState,
|
||||||
|
lockContinue: false,
|
||||||
|
step: Steps.PasswordStep,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override componentDidMount(): void {
|
override componentDidMount(): void {
|
||||||
super.componentDidMount()
|
super.componentDidMount()
|
||||||
this.currentPasswordInput.current?.focus()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override componentWillUnmount(): void {
|
override componentWillUnmount(): void {
|
||||||
@@ -83,6 +95,15 @@ class PasswordWizard extends AbstractComponent<Props, State> {
|
|||||||
|
|
||||||
if (this.state.step === Steps.FinishStep) {
|
if (this.state.step === Steps.FinishStep) {
|
||||||
this.dismiss()
|
this.dismiss()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.step === Steps.PreprocessingStep) {
|
||||||
|
this.setState({
|
||||||
|
step: Steps.PasswordStep,
|
||||||
|
})
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +163,6 @@ class PasswordWizard extends AbstractComponent<Props, State> {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Validate current password */
|
|
||||||
const success = await this.application.validateAccountPassword(this.state.formData.currentPassword as string)
|
const success = await this.application.validateAccountPassword(this.state.formData.currentPassword as string)
|
||||||
if (!success) {
|
if (!success) {
|
||||||
this.application.alertService
|
this.application.alertService
|
||||||
@@ -192,7 +212,7 @@ class PasswordWizard extends AbstractComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dismiss = () => {
|
dismiss = () => {
|
||||||
if (this.state.lockContinue) {
|
if (this.state.processing) {
|
||||||
this.application.alertService.alert('Cannot close window until pending tasks are complete.').catch(console.error)
|
this.application.alertService.alert('Cannot close window until pending tasks are complete.').catch(console.error)
|
||||||
} else {
|
} else {
|
||||||
this.props.dismissModal()
|
this.props.dismissModal()
|
||||||
@@ -226,11 +246,32 @@ class PasswordWizard extends AbstractComponent<Props, State> {
|
|||||||
}).catch(console.error)
|
}).catch(console.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setContinueEnabled = (enabled: boolean) => {
|
||||||
|
this.setState({
|
||||||
|
lockContinue: !enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
nextStepFromPreprocessing = () => {
|
||||||
|
if (this.state.lockContinue) {
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
|
lockContinue: false,
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
void this.nextStep()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
void this.nextStep()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
return (
|
return (
|
||||||
<div className="sn-component h-full w-full md:h-auto md:w-auto" id="password-wizard">
|
<div className="sn-component h-full w-full md:h-auto md:w-auto" id="password-wizard">
|
||||||
<Modal
|
<Modal
|
||||||
title={this.state.title}
|
title={'Change Password'}
|
||||||
close={this.dismiss}
|
close={this.dismiss}
|
||||||
actions={[
|
actions={[
|
||||||
{
|
{
|
||||||
@@ -253,59 +294,23 @@ class PasswordWizard extends AbstractComponent<Props, State> {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<div className="px-4 py-4">
|
<div className="px-4.5 py-4">
|
||||||
|
{this.state.step === Steps.PreprocessingStep && (
|
||||||
|
<PreprocessingStep
|
||||||
|
onContinue={this.nextStepFromPreprocessing}
|
||||||
|
setContinueEnabled={this.setContinueEnabled}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{this.state.step === Steps.PasswordStep && (
|
{this.state.step === Steps.PasswordStep && (
|
||||||
<div className="flex flex-col pb-1.5">
|
<PasswordStep
|
||||||
<form>
|
onCurrentPasswordChange={this.handleCurrentPasswordInputChange}
|
||||||
<label htmlFor="password-wiz-current-password" className="mb-1 block">
|
onNewPasswordChange={this.handleNewPasswordInputChange}
|
||||||
Current Password
|
onNewPasswordConfirmationChange={this.handleNewPasswordConfirmationInputChange}
|
||||||
</label>
|
/>
|
||||||
|
|
||||||
<DecoratedPasswordInput
|
|
||||||
ref={this.currentPasswordInput}
|
|
||||||
id="password-wiz-current-password"
|
|
||||||
value={this.state.formData.currentPassword}
|
|
||||||
onChange={this.handleCurrentPasswordInputChange}
|
|
||||||
type="password"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="min-h-2" />
|
|
||||||
|
|
||||||
<label htmlFor="password-wiz-new-password" className="mb-1 block">
|
|
||||||
New Password
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<DecoratedPasswordInput
|
|
||||||
id="password-wiz-new-password"
|
|
||||||
value={this.state.formData.newPassword}
|
|
||||||
onChange={this.handleNewPasswordInputChange}
|
|
||||||
type="password"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="min-h-2" />
|
|
||||||
|
|
||||||
<label htmlFor="password-wiz-confirm-new-password" className="mb-1 block">
|
|
||||||
Confirm New Password
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<DecoratedPasswordInput
|
|
||||||
id="password-wiz-confirm-new-password"
|
|
||||||
value={this.state.formData.newPasswordConfirmation}
|
|
||||||
onChange={this.handleNewPasswordConfirmationInputChange}
|
|
||||||
type="password"
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{this.state.step === Steps.FinishStep && (
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<div className="mb-1 font-bold text-info">Your password has been successfully changed.</div>
|
|
||||||
<p className="sk-p">
|
|
||||||
Please ensure you are running the latest version of Standard Notes on all platforms to ensure maximum
|
|
||||||
compatibility.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{this.state.step === Steps.FinishStep && <FinishStep />}
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import Spinner from '../Spinner/Spinner'
|
||||||
|
import { useApplication } from '../ApplicationProvider'
|
||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
export const PreprocessingStep = ({
|
||||||
|
onContinue,
|
||||||
|
setContinueEnabled,
|
||||||
|
}: {
|
||||||
|
onContinue: () => void
|
||||||
|
setContinueEnabled: (disabled: boolean) => void
|
||||||
|
}) => {
|
||||||
|
const application = useApplication()
|
||||||
|
|
||||||
|
const [isProcessingSync, setIsProcessingSync] = useState<boolean>(true)
|
||||||
|
const [isProcessingMessages, setIsProcessingMessages] = useState<boolean>(true)
|
||||||
|
const [isProcessingInvites, setIsProcessingInvites] = useState<boolean>(true)
|
||||||
|
const [needsUserConfirmation, setNeedsUserConfirmation] = useState<'yes' | 'no'>()
|
||||||
|
|
||||||
|
const continueIfPossible = useCallback(() => {
|
||||||
|
if (isProcessingMessages || isProcessingInvites || isProcessingSync) {
|
||||||
|
setContinueEnabled(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsUserConfirmation === 'yes') {
|
||||||
|
setContinueEnabled(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
onContinue()
|
||||||
|
}, [
|
||||||
|
isProcessingInvites,
|
||||||
|
isProcessingMessages,
|
||||||
|
isProcessingSync,
|
||||||
|
needsUserConfirmation,
|
||||||
|
onContinue,
|
||||||
|
setContinueEnabled,
|
||||||
|
])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
continueIfPossible()
|
||||||
|
}, [isProcessingInvites, isProcessingMessages, isProcessingSync, continueIfPossible])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const processPendingSync = async () => {
|
||||||
|
await application.sync.sync()
|
||||||
|
setIsProcessingSync(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
void processPendingSync()
|
||||||
|
}, [application.sync])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const processPendingMessages = async () => {
|
||||||
|
await application.asymmetric.downloadAndProcessInboundMessages()
|
||||||
|
setIsProcessingMessages(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
void processPendingMessages()
|
||||||
|
}, [application.asymmetric])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const processPendingInvites = async () => {
|
||||||
|
await application.sharedVaults.downloadInboundInvites()
|
||||||
|
const hasPendingInvites = application.sharedVaults.getCachedPendingInviteRecords().length > 0
|
||||||
|
setNeedsUserConfirmation(hasPendingInvites ? 'yes' : 'no')
|
||||||
|
setIsProcessingInvites(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
void processPendingInvites()
|
||||||
|
}, [application.sharedVaults])
|
||||||
|
|
||||||
|
const isProcessing = isProcessingSync || isProcessingMessages || isProcessingInvites
|
||||||
|
|
||||||
|
if (isProcessing) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-row items-center gap-3">
|
||||||
|
<Spinner className="h-3 w-3" />
|
||||||
|
<p className="">Checking for data conflicts...</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsUserConfirmation === 'no') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<p>
|
||||||
|
You have pending vault invites. Changing your password will delete these invites. It is recommended you accept
|
||||||
|
or decline these invites before changing your password. If you choose to continue, these invites will be
|
||||||
|
deleted.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { PreferencesMenuItem } from './PreferencesMenuItem'
|
||||||
|
|
||||||
|
export const PREFERENCES_MENU_ITEMS: PreferencesMenuItem[] = [
|
||||||
|
{ id: 'whats-new', label: "What's New", icon: 'asterisk', order: 0 },
|
||||||
|
{ id: 'account', label: 'Account', icon: 'user', order: 1 },
|
||||||
|
{ id: 'general', label: 'General', icon: 'settings', order: 3 },
|
||||||
|
{ id: 'security', label: 'Security', icon: 'security', order: 4 },
|
||||||
|
{ id: 'backups', label: 'Backups', icon: 'restore', order: 5 },
|
||||||
|
{ id: 'appearance', label: 'Appearance', icon: 'themes', order: 6 },
|
||||||
|
{ id: 'listed', label: 'Listed', icon: 'listed', order: 7 },
|
||||||
|
{ id: 'shortcuts', label: 'Shortcuts', icon: 'keyboard', order: 8 },
|
||||||
|
{ id: 'accessibility', label: 'Accessibility', icon: 'accessibility', order: 9 },
|
||||||
|
{ id: 'get-free-month', label: 'Get a free month', icon: 'star', order: 10 },
|
||||||
|
{ id: 'help-feedback', label: 'Help & feedback', icon: 'help', order: 11 },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const READY_PREFERENCES_MENU_ITEMS: PreferencesMenuItem[] = [
|
||||||
|
{ id: 'whats-new', label: "What's New", icon: 'asterisk', order: 0 },
|
||||||
|
{ id: 'account', label: 'Account', icon: 'user', order: 1 },
|
||||||
|
{ id: 'general', label: 'General', icon: 'settings', order: 3 },
|
||||||
|
{ id: 'security', label: 'Security', icon: 'security', order: 4 },
|
||||||
|
{ id: 'backups', label: 'Backups', icon: 'restore', order: 5 },
|
||||||
|
{ id: 'appearance', label: 'Appearance', icon: 'themes', order: 6 },
|
||||||
|
{ id: 'listed', label: 'Listed', icon: 'listed', order: 7 },
|
||||||
|
{ id: 'help-feedback', label: 'Help & feedback', icon: 'help', order: 11 },
|
||||||
|
]
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { IconType } from '@standardnotes/snjs'
|
||||||
|
import { PreferenceId } from '@standardnotes/ui-services'
|
||||||
|
|
||||||
|
export interface PreferencesMenuItem {
|
||||||
|
readonly id: PreferenceId
|
||||||
|
readonly icon: IconType
|
||||||
|
readonly label: string
|
||||||
|
readonly order: number
|
||||||
|
readonly hasBubble?: boolean
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { action, makeAutoObservable, observable } from 'mobx'
|
||||||
|
import { WebApplication } from '@/Application/WebApplication'
|
||||||
|
import { PackageProvider } from '../Panes/General/Advanced/Packages/Provider/PackageProvider'
|
||||||
|
import { securityPrefsHasBubble } from '../Panes/Security/securityPrefsHasBubble'
|
||||||
|
import { PreferenceId } from '@standardnotes/ui-services'
|
||||||
|
import { isDesktopApplication } from '@/Utils'
|
||||||
|
import { featureTrunkHomeServerEnabled, featureTrunkVaultsEnabled } from '@/FeatureTrunk'
|
||||||
|
import { PreferencesMenuItem } from './PreferencesMenuItem'
|
||||||
|
import { SelectableMenuItem } from './SelectableMenuItem'
|
||||||
|
import { PREFERENCES_MENU_ITEMS, READY_PREFERENCES_MENU_ITEMS } from './MenuItems'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unlike PreferencesController, the PreferencesSessionController is ephemeral and bound to a single opening of the
|
||||||
|
* Preferences menu. It is created and destroyed each time the menu is opened and closed.
|
||||||
|
*/
|
||||||
|
export class PreferencesSessionController {
|
||||||
|
private _selectedPane: PreferenceId = 'account'
|
||||||
|
private _menu: PreferencesMenuItem[]
|
||||||
|
private _extensionLatestVersions: PackageProvider = new PackageProvider(new Map())
|
||||||
|
|
||||||
|
constructor(private application: WebApplication, private readonly _enableUnfinishedFeatures: boolean) {
|
||||||
|
const menuItems = this._enableUnfinishedFeatures
|
||||||
|
? PREFERENCES_MENU_ITEMS.slice()
|
||||||
|
: READY_PREFERENCES_MENU_ITEMS.slice()
|
||||||
|
|
||||||
|
if (featureTrunkVaultsEnabled()) {
|
||||||
|
menuItems.push({ id: 'vaults', label: 'Vaults', icon: 'safe-square', order: 5 })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (featureTrunkHomeServerEnabled() && isDesktopApplication()) {
|
||||||
|
menuItems.push({ id: 'home-server', label: 'Home Server', icon: 'server', order: 5 })
|
||||||
|
}
|
||||||
|
|
||||||
|
this._menu = menuItems.sort((a, b) => a.order - b.order)
|
||||||
|
|
||||||
|
this.loadLatestVersions()
|
||||||
|
|
||||||
|
makeAutoObservable<
|
||||||
|
PreferencesSessionController,
|
||||||
|
'_selectedPane' | '_twoFactorAuth' | '_extensionPanes' | '_extensionLatestVersions' | 'loadLatestVersions'
|
||||||
|
>(this, {
|
||||||
|
_twoFactorAuth: observable,
|
||||||
|
_selectedPane: observable,
|
||||||
|
_extensionPanes: observable.ref,
|
||||||
|
_extensionLatestVersions: observable.ref,
|
||||||
|
loadLatestVersions: action,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadLatestVersions(): void {
|
||||||
|
PackageProvider.load()
|
||||||
|
.then((versions) => {
|
||||||
|
if (versions) {
|
||||||
|
this._extensionLatestVersions = versions
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(console.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
get extensionsLatestVersions(): PackageProvider {
|
||||||
|
return this._extensionLatestVersions
|
||||||
|
}
|
||||||
|
|
||||||
|
get menuItems(): SelectableMenuItem[] {
|
||||||
|
const menuItems = this._menu.map((preference) => {
|
||||||
|
const item: SelectableMenuItem = {
|
||||||
|
...preference,
|
||||||
|
selected: preference.id === this._selectedPane,
|
||||||
|
hasBubble: this.sectionHasBubble(preference.id),
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
|
||||||
|
return menuItems
|
||||||
|
}
|
||||||
|
|
||||||
|
get selectedMenuItem(): PreferencesMenuItem | undefined {
|
||||||
|
return this._menu.find((item) => item.id === this._selectedPane)
|
||||||
|
}
|
||||||
|
|
||||||
|
get selectedPaneId(): PreferenceId {
|
||||||
|
if (this.selectedMenuItem != undefined) {
|
||||||
|
return this.selectedMenuItem.id
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'account'
|
||||||
|
}
|
||||||
|
|
||||||
|
selectPane = (key: PreferenceId) => {
|
||||||
|
this._selectedPane = key
|
||||||
|
}
|
||||||
|
|
||||||
|
sectionHasBubble(id: PreferenceId): boolean {
|
||||||
|
if (id === 'security') {
|
||||||
|
return securityPrefsHasBubble(this.application)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { PreferencesMenuItem } from './PreferencesMenuItem'
|
||||||
|
|
||||||
|
export interface SelectableMenuItem extends PreferencesMenuItem {
|
||||||
|
selected: boolean
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { FunctionComponent } from 'react'
|
import { FunctionComponent } from 'react'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { PreferencesMenu } from './PreferencesMenu'
|
import { PreferencesSessionController } from './Controller/PreferencesSessionController'
|
||||||
import Backups from '@/Components/Preferences/Panes/Backups/Backups'
|
import Backups from '@/Components/Preferences/Panes/Backups/Backups'
|
||||||
import Appearance from './Panes/Appearance'
|
import Appearance from './Panes/Appearance'
|
||||||
import General from './Panes/General/General'
|
import General from './Panes/General/General'
|
||||||
@@ -13,7 +13,7 @@ import WhatsNew from './Panes/WhatsNew/WhatsNew'
|
|||||||
import HomeServer from './Panes/HomeServer/HomeServer'
|
import HomeServer from './Panes/HomeServer/HomeServer'
|
||||||
import Vaults from './Panes/Vaults/Vaults'
|
import Vaults from './Panes/Vaults/Vaults'
|
||||||
|
|
||||||
const PaneSelector: FunctionComponent<PreferencesProps & { menu: PreferencesMenu }> = ({
|
const PaneSelector: FunctionComponent<PreferencesProps & { menu: PreferencesSessionController }> = ({
|
||||||
menu,
|
menu,
|
||||||
viewControllerManager,
|
viewControllerManager,
|
||||||
application,
|
application,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
|
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { Fragment, FunctionComponent, useState } from 'react'
|
import { Fragment, FunctionComponent, useEffect, useState } from 'react'
|
||||||
import { Text, Title, Subtitle } from '@/Components/Preferences/PreferencesComponents/Content'
|
import { Text, Title, Subtitle } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||||
import {
|
import {
|
||||||
ButtonType,
|
ButtonType,
|
||||||
ClientDisplayableError,
|
ClientDisplayableError,
|
||||||
|
ContentType,
|
||||||
DisplayStringForContentType,
|
DisplayStringForContentType,
|
||||||
EncryptedItemInterface,
|
EncryptedItemInterface,
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
@@ -12,13 +12,18 @@ import Button from '@/Components/Button/Button'
|
|||||||
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
|
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
|
||||||
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
|
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
|
||||||
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
|
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
|
||||||
|
import { ErrorCircle } from '@/Components/UIElements/ErrorCircle'
|
||||||
|
import { useApplication } from '@/Components/ApplicationProvider'
|
||||||
|
|
||||||
type Props = { viewControllerManager: ViewControllerManager }
|
const ErroredItems: FunctionComponent = () => {
|
||||||
|
const application = useApplication()
|
||||||
|
const [erroredItems, setErroredItems] = useState(application.items.invalidNonVaultedItems)
|
||||||
|
|
||||||
const ErroredItems: FunctionComponent<Props> = ({ viewControllerManager }: Props) => {
|
useEffect(() => {
|
||||||
const app = viewControllerManager.application
|
return application.streamItems(ContentType.Any, () => {
|
||||||
|
setErroredItems(application.items.invalidNonVaultedItems)
|
||||||
const [erroredItems, setErroredItems] = useState(app.items.invalidNonVaultedItems)
|
})
|
||||||
|
}, [application])
|
||||||
|
|
||||||
const getContentTypeDisplay = (item: EncryptedItemInterface): string => {
|
const getContentTypeDisplay = (item: EncryptedItemInterface): string => {
|
||||||
const display = DisplayStringForContentType(item.content_type)
|
const display = DisplayStringForContentType(item.content_type)
|
||||||
@@ -34,7 +39,7 @@ const ErroredItems: FunctionComponent<Props> = ({ viewControllerManager }: Props
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deleteItems = async (items: EncryptedItemInterface[]): Promise<void> => {
|
const deleteItems = async (items: EncryptedItemInterface[]): Promise<void> => {
|
||||||
const confirmed = await app.alertService.confirm(
|
const confirmed = await application.alertService.confirm(
|
||||||
`Are you sure you want to permanently delete ${items.length} item(s)?`,
|
`Are you sure you want to permanently delete ${items.length} item(s)?`,
|
||||||
undefined,
|
undefined,
|
||||||
'Delete',
|
'Delete',
|
||||||
@@ -44,30 +49,35 @@ const ErroredItems: FunctionComponent<Props> = ({ viewControllerManager }: Props
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
void app.mutator.deleteItems(items).then(() => {
|
void application.mutator.deleteItems(items).then(() => {
|
||||||
void app.sync.sync()
|
void application.sync.sync()
|
||||||
})
|
})
|
||||||
|
|
||||||
setErroredItems(app.items.invalidItems)
|
setErroredItems(application.items.invalidItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
const attemptDecryption = (item: EncryptedItemInterface): void => {
|
const attemptDecryption = (item: EncryptedItemInterface): void => {
|
||||||
const errorOrTrue = app.canAttemptDecryptionOfItem(item)
|
const errorOrTrue = application.canAttemptDecryptionOfItem(item)
|
||||||
|
|
||||||
if (errorOrTrue instanceof ClientDisplayableError) {
|
if (errorOrTrue instanceof ClientDisplayableError) {
|
||||||
void app.alertService.showErrorAlert(errorOrTrue)
|
void application.alertService.showErrorAlert(errorOrTrue)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
app.presentKeyRecoveryWizard()
|
application.presentKeyRecoveryWizard()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (erroredItems.length === 0) {
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PreferencesGroup>
|
<PreferencesGroup>
|
||||||
<PreferencesSegment>
|
<PreferencesSegment>
|
||||||
<Title>
|
<Title className="flex flex-row items-center gap-2">
|
||||||
Error decrypting items <span className="ml-1 text-warning">⚠️</span>
|
<ErrorCircle />
|
||||||
|
Error decrypting items
|
||||||
</Title>
|
</Title>
|
||||||
<Text>{`${erroredItems.length} items are errored and could not be decrypted.`}</Text>
|
<Text>{`${erroredItems.length} items are errored and could not be decrypted.`}</Text>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
@@ -75,7 +85,7 @@ const ErroredItems: FunctionComponent<Props> = ({ viewControllerManager }: Props
|
|||||||
className="mt-3 mr-2 min-w-20"
|
className="mt-3 mr-2 min-w-20"
|
||||||
label="Export all"
|
label="Export all"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void app.getArchiveService().downloadEncryptedItems(erroredItems)
|
void application.getArchiveService().downloadEncryptedItems(erroredItems)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
@@ -95,10 +105,8 @@ const ErroredItems: FunctionComponent<Props> = ({ viewControllerManager }: Props
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<Subtitle>{`${getContentTypeDisplay(item)} created on ${item.createdAtString}`}</Subtitle>
|
<Subtitle>{`${getContentTypeDisplay(item)} created on ${item.createdAtString}`}</Subtitle>
|
||||||
<Text>
|
<Text>Item ID: {item.uuid}</Text>
|
||||||
<div>Item ID: {item.uuid}</div>
|
<Text>Last Modified: {item.updatedAtString}</Text>
|
||||||
<div>Last Modified: {item.updatedAtString}</div>
|
|
||||||
</Text>
|
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<Button
|
<Button
|
||||||
className="mt-3 mr-2 min-w-20"
|
className="mt-3 mr-2 min-w-20"
|
||||||
@@ -111,7 +119,7 @@ const ErroredItems: FunctionComponent<Props> = ({ viewControllerManager }: Props
|
|||||||
className="mt-3 mr-2 min-w-20"
|
className="mt-3 mr-2 min-w-20"
|
||||||
label="Export"
|
label="Export"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void app.getArchiveService().downloadEncryptedItem(item)
|
void application.getArchiveService().downloadEncryptedItem(item)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -30,9 +30,7 @@ const Security: FunctionComponent<SecurityProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<PreferencesPane>
|
<PreferencesPane>
|
||||||
<Encryption viewControllerManager={props.viewControllerManager} />
|
<Encryption viewControllerManager={props.viewControllerManager} />
|
||||||
{props.application.items.invalidNonVaultedItems.length > 0 && (
|
{props.application.items.invalidNonVaultedItems.length > 0 && <ErroredItems />}
|
||||||
<ErroredItems viewControllerManager={props.viewControllerManager} />
|
|
||||||
)}
|
|
||||||
<Protections application={props.application} />
|
<Protections application={props.application} />
|
||||||
<TwoFactorAuthWrapper
|
<TwoFactorAuthWrapper
|
||||||
mfaProvider={props.mfaProvider}
|
mfaProvider={props.mfaProvider}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const ContactItem = ({ contact }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ModalOverlay isOpen={isContactModalOpen} close={closeContactModal}>
|
<ModalOverlay isOpen={isContactModalOpen} close={closeContactModal}>
|
||||||
<EditContactModal editContactUuid={contact.uuid} onCloseDialog={closeContactModal} />
|
<EditContactModal editContactUuid={contact.contactUuid} onCloseDialog={closeContactModal} />
|
||||||
</ModalOverlay>
|
</ModalOverlay>
|
||||||
|
|
||||||
<div className="bg-gray-100 flex flex-row gap-3.5 rounded-lg py-2.5 px-3.5 shadow-md">
|
<div className="bg-gray-100 flex flex-row gap-3.5 rounded-lg py-2.5 px-3.5 shadow-md">
|
||||||
|
|||||||
@@ -5,33 +5,36 @@ import ModalOverlay from '@/Components/Modal/ModalOverlay'
|
|||||||
import { PendingSharedVaultInviteRecord } from '@standardnotes/snjs'
|
import { PendingSharedVaultInviteRecord } from '@standardnotes/snjs'
|
||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
import EditContactModal from '../Contacts/EditContactModal'
|
import EditContactModal from '../Contacts/EditContactModal'
|
||||||
|
import { CheckmarkCircle } from '../../../../UIElements/CheckmarkCircle'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
invite: PendingSharedVaultInviteRecord
|
inviteRecord: PendingSharedVaultInviteRecord
|
||||||
}
|
}
|
||||||
|
|
||||||
const InviteItem = ({ invite }: Props) => {
|
const InviteItem = ({ inviteRecord }: Props) => {
|
||||||
const application = useApplication()
|
const application = useApplication()
|
||||||
const [isAddContactModalOpen, setIsAddContactModalOpen] = useState(false)
|
const [isAddContactModalOpen, setIsAddContactModalOpen] = useState(false)
|
||||||
|
|
||||||
const isTrusted = invite.trusted
|
const isTrusted = inviteRecord.trusted
|
||||||
const inviteData = invite.message.data
|
const inviteData = inviteRecord.message.data
|
||||||
|
|
||||||
const addAsTrustedContact = useCallback(() => {
|
const addAsTrustedContact = useCallback(() => {
|
||||||
setIsAddContactModalOpen(true)
|
setIsAddContactModalOpen(true)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const acceptInvite = useCallback(async () => {
|
const acceptInvite = useCallback(async () => {
|
||||||
await application.sharedVaults.acceptPendingSharedVaultInvite(invite)
|
await application.sharedVaults.acceptPendingSharedVaultInvite(inviteRecord)
|
||||||
}, [application.sharedVaults, invite])
|
}, [application.sharedVaults, inviteRecord])
|
||||||
|
|
||||||
const closeAddContactModal = () => setIsAddContactModalOpen(false)
|
const closeAddContactModal = () => setIsAddContactModalOpen(false)
|
||||||
const collaborationId = application.contacts.getCollaborationIDFromInvite(invite.invite)
|
const collaborationId = application.contacts.getCollaborationIDFromInvite(inviteRecord.invite)
|
||||||
|
|
||||||
|
const trustedContact = application.contacts.findTrustedContactForInvite(inviteRecord.invite)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ModalOverlay isOpen={isAddContactModalOpen} close={closeAddContactModal}>
|
<ModalOverlay isOpen={isAddContactModalOpen} close={closeAddContactModal}>
|
||||||
<EditContactModal fromInvite={invite} onCloseDialog={closeAddContactModal} />
|
<EditContactModal fromInvite={inviteRecord} onCloseDialog={closeAddContactModal} />
|
||||||
</ModalOverlay>
|
</ModalOverlay>
|
||||||
|
|
||||||
<div className="bg-gray-100 flex flex-row gap-3.5 rounded-lg py-2.5 px-3.5 shadow-md">
|
<div className="bg-gray-100 flex flex-row gap-3.5 rounded-lg py-2.5 px-3.5 shadow-md">
|
||||||
@@ -41,9 +44,16 @@ const InviteItem = ({ invite }: Props) => {
|
|||||||
<span className="mr-auto overflow-hidden text-ellipsis text-sm">
|
<span className="mr-auto overflow-hidden text-ellipsis text-sm">
|
||||||
Vault Description: {inviteData.metadata.description}
|
Vault Description: {inviteData.metadata.description}
|
||||||
</span>
|
</span>
|
||||||
<span className="mr-auto overflow-hidden text-ellipsis text-sm">
|
{trustedContact ? (
|
||||||
Sender CollaborationID: {collaborationId}
|
<div className="flex flex-row gap-2">
|
||||||
</span>
|
<span className="overflow-hidden text-ellipsis text-sm">Trusted Sender: {trustedContact.name}</span>
|
||||||
|
<CheckmarkCircle />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="mr-auto overflow-hidden text-ellipsis text-sm">
|
||||||
|
Sender CollaborationID: {collaborationId}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="mt-2.5 flex flex-row">
|
<div className="mt-2.5 flex flex-row">
|
||||||
{isTrusted ? (
|
{isTrusted ? (
|
||||||
|
|||||||
@@ -40,31 +40,36 @@ const Vaults = () => {
|
|||||||
setVaults(vaultService.getVaults())
|
setVaults(vaultService.getVaults())
|
||||||
}, [vaultService])
|
}, [vaultService])
|
||||||
|
|
||||||
const fetchInvites = useCallback(async () => {
|
const updateInvites = useCallback(async () => {
|
||||||
await sharedVaultService.downloadInboundInvites()
|
setInvites(sharedVaultService.getCachedPendingInviteRecords())
|
||||||
const invites = sharedVaultService.getCachedPendingInviteRecords()
|
|
||||||
setInvites(invites)
|
|
||||||
}, [sharedVaultService])
|
}, [sharedVaultService])
|
||||||
|
|
||||||
const updateContacts = useCallback(async () => {
|
const updateContacts = useCallback(async () => {
|
||||||
setContacts(contactService.getAllContacts())
|
setContacts(contactService.getAllContacts())
|
||||||
}, [contactService])
|
}, [contactService])
|
||||||
|
|
||||||
|
const updateAllData = useCallback(async () => {
|
||||||
|
await Promise.all([updateVaults(), updateInvites(), updateContacts()])
|
||||||
|
}, [updateContacts, updateInvites, updateVaults])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return application.sharedVaults.addEventObserver((event) => {
|
return application.sharedVaults.addEventObserver((event) => {
|
||||||
if (event === SharedVaultServiceEvent.SharedVaultStatusChanged) {
|
if (event === SharedVaultServiceEvent.SharedVaultStatusChanged) {
|
||||||
void fetchInvites()
|
void updateAllData()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
}, [application.sharedVaults, updateAllData])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return application.streamItems([ContentType.VaultListing, ContentType.TrustedContact], () => {
|
return application.streamItems([ContentType.VaultListing, ContentType.TrustedContact], () => {
|
||||||
void updateVaults()
|
void updateAllData()
|
||||||
void fetchInvites()
|
|
||||||
void updateContacts()
|
|
||||||
})
|
})
|
||||||
}, [application, updateVaults, fetchInvites, updateContacts])
|
}, [application, updateAllData])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void sharedVaultService.downloadInboundInvites()
|
||||||
|
void updateAllData()
|
||||||
|
}, [updateAllData, sharedVaultService])
|
||||||
|
|
||||||
const createNewVault = useCallback(async () => {
|
const createNewVault = useCallback(async () => {
|
||||||
setIsVaultModalOpen(true)
|
setIsVaultModalOpen(true)
|
||||||
@@ -74,12 +79,6 @@ const Vaults = () => {
|
|||||||
setIsAddContactModalOpen(true)
|
setIsAddContactModalOpen(true)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
void updateVaults()
|
|
||||||
void fetchInvites()
|
|
||||||
void updateContacts()
|
|
||||||
}, [updateContacts, updateVaults, fetchInvites])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ModalOverlay isOpen={isAddContactModalOpen} close={closeAddContactModal}>
|
<ModalOverlay isOpen={isAddContactModalOpen} close={closeAddContactModal}>
|
||||||
@@ -95,7 +94,7 @@ const Vaults = () => {
|
|||||||
<Title>Incoming Invites</Title>
|
<Title>Incoming Invites</Title>
|
||||||
<div className="my-2 flex flex-col">
|
<div className="my-2 flex flex-col">
|
||||||
{invites.map((invite) => {
|
{invites.map((invite) => {
|
||||||
return <InviteItem invite={invite} key={invite.invite.uuid} />
|
return <InviteItem inviteRecord={invite} key={invite.invite.uuid} />
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</PreferencesSegment>
|
</PreferencesSegment>
|
||||||
|
|||||||
@@ -28,9 +28,8 @@ export const VaultModalInvites = ({
|
|||||||
<div className="mb-3 text-lg">Pending Invites</div>
|
<div className="mb-3 text-lg">Pending Invites</div>
|
||||||
{invites.map((invite) => {
|
{invites.map((invite) => {
|
||||||
const contact = application.contacts.findTrustedContactForInvite(invite)
|
const contact = application.contacts.findTrustedContactForInvite(invite)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-gray-100 flex flex-row gap-3.5 rounded-lg py-2.5 px-3.5 shadow-md">
|
<div key={invite.uuid} className="bg-gray-100 flex flex-row gap-3.5 rounded-lg py-2.5 px-3.5 shadow-md">
|
||||||
<Icon type={'user'} size="custom" className="mt-2.5 h-5.5 w-5.5 flex-shrink-0" />
|
<Icon type={'user'} size="custom" className="mt-2.5 h-5.5 w-5.5 flex-shrink-0" />
|
||||||
<div className="flex flex-col gap-2 py-1.5">
|
<div className="flex flex-col gap-2 py-1.5">
|
||||||
<span className="mr-auto overflow-hidden text-ellipsis text-base font-bold">
|
<span className="mr-auto overflow-hidden text-ellipsis text-base font-bold">
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { FunctionComponent } from 'react'
|
import { FunctionComponent } from 'react'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { PreferencesMenu } from './PreferencesMenu'
|
import { PreferencesSessionController } from './Controller/PreferencesSessionController'
|
||||||
import PreferencesMenuView from './PreferencesMenuView'
|
import PreferencesMenuView from './PreferencesMenuView'
|
||||||
import PaneSelector from './PaneSelector'
|
import PaneSelector from './PaneSelector'
|
||||||
import { PreferencesProps } from './PreferencesProps'
|
import { PreferencesProps } from './PreferencesProps'
|
||||||
|
|
||||||
const PreferencesCanvas: FunctionComponent<PreferencesProps & { menu: PreferencesMenu }> = (props) => (
|
const PreferencesCanvas: FunctionComponent<PreferencesProps & { menu: PreferencesSessionController }> = (props) => (
|
||||||
<div className="flex min-h-0 flex-grow flex-col md:flex-row md:justify-between">
|
<div className="flex min-h-0 flex-grow flex-col md:flex-row md:justify-between">
|
||||||
<PreferencesMenuView menu={props.menu} />
|
<PreferencesMenuView menu={props.menu} />
|
||||||
<div className="min-h-0 flex-grow overflow-auto bg-contrast">
|
<div className="min-h-0 flex-grow overflow-auto bg-contrast">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Icon from '@/Components/Icon/Icon'
|
import Icon from '@/Components/Icon/Icon'
|
||||||
import { FunctionComponent } from 'react'
|
import { FunctionComponent } from 'react'
|
||||||
import { IconType } from '@standardnotes/snjs'
|
import { IconType } from '@standardnotes/snjs'
|
||||||
|
import { ErrorCircle } from '@/Components/UIElements/ErrorCircle'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
iconType: IconType
|
iconType: IconType
|
||||||
@@ -23,7 +24,11 @@ const PreferencesMenuItem: FunctionComponent<Props> = ({ iconType, label, select
|
|||||||
<Icon className={`icon text-base ${selected ? 'text-info' : 'text-neutral'}`} type={iconType} />
|
<Icon className={`icon text-base ${selected ? 'text-info' : 'text-neutral'}`} type={iconType} />
|
||||||
<div className="min-w-1" />
|
<div className="min-w-1" />
|
||||||
{label}
|
{label}
|
||||||
{hasBubble && <span className="ml-1 text-warning">⚠️</span>}
|
{hasBubble && (
|
||||||
|
<span className="ml-2">
|
||||||
|
<ErrorCircle />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
import { action, makeAutoObservable, observable } from 'mobx'
|
|
||||||
import { IconType } from '@standardnotes/snjs'
|
|
||||||
import { WebApplication } from '@/Application/WebApplication'
|
|
||||||
import { PackageProvider } from './Panes/General/Advanced/Packages/Provider/PackageProvider'
|
|
||||||
import { securityPrefsHasBubble } from './Panes/Security/securityPrefsHasBubble'
|
|
||||||
import { PreferenceId } from '@standardnotes/ui-services'
|
|
||||||
import { isDesktopApplication } from '@/Utils'
|
|
||||||
import { featureTrunkHomeServerEnabled, featureTrunkVaultsEnabled } from '@/FeatureTrunk'
|
|
||||||
|
|
||||||
interface PreferencesMenuItem {
|
|
||||||
readonly id: PreferenceId
|
|
||||||
readonly icon: IconType
|
|
||||||
readonly label: string
|
|
||||||
readonly order: number
|
|
||||||
readonly hasBubble?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SelectableMenuItem extends PreferencesMenuItem {
|
|
||||||
selected: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Items are in order of appearance
|
|
||||||
*/
|
|
||||||
const PREFERENCES_MENU_ITEMS: PreferencesMenuItem[] = [
|
|
||||||
{ id: 'whats-new', label: "What's New", icon: 'asterisk', order: 0 },
|
|
||||||
{ id: 'account', label: 'Account', icon: 'user', order: 1 },
|
|
||||||
{ id: 'general', label: 'General', icon: 'settings', order: 3 },
|
|
||||||
{ id: 'security', label: 'Security', icon: 'security', order: 4 },
|
|
||||||
{ id: 'backups', label: 'Backups', icon: 'restore', order: 5 },
|
|
||||||
{ id: 'appearance', label: 'Appearance', icon: 'themes', order: 6 },
|
|
||||||
{ id: 'listed', label: 'Listed', icon: 'listed', order: 7 },
|
|
||||||
{ id: 'shortcuts', label: 'Shortcuts', icon: 'keyboard', order: 8 },
|
|
||||||
{ id: 'accessibility', label: 'Accessibility', icon: 'accessibility', order: 9 },
|
|
||||||
{ id: 'get-free-month', label: 'Get a free month', icon: 'star', order: 10 },
|
|
||||||
{ id: 'help-feedback', label: 'Help & feedback', icon: 'help', order: 11 },
|
|
||||||
]
|
|
||||||
|
|
||||||
const READY_PREFERENCES_MENU_ITEMS: PreferencesMenuItem[] = [
|
|
||||||
{ id: 'whats-new', label: "What's New", icon: 'asterisk', order: 0 },
|
|
||||||
{ id: 'account', label: 'Account', icon: 'user', order: 1 },
|
|
||||||
{ id: 'general', label: 'General', icon: 'settings', order: 3 },
|
|
||||||
{ id: 'security', label: 'Security', icon: 'security', order: 4 },
|
|
||||||
{ id: 'backups', label: 'Backups', icon: 'restore', order: 5 },
|
|
||||||
{ id: 'appearance', label: 'Appearance', icon: 'themes', order: 6 },
|
|
||||||
{ id: 'listed', label: 'Listed', icon: 'listed', order: 7 },
|
|
||||||
{ id: 'help-feedback', label: 'Help & feedback', icon: 'help', order: 11 },
|
|
||||||
]
|
|
||||||
|
|
||||||
const DESKTOP_PREFERENCES_MENU_ITEMS: PreferencesMenuItem[] = []
|
|
||||||
|
|
||||||
export class PreferencesMenu {
|
|
||||||
private _selectedPane: PreferenceId = 'account'
|
|
||||||
private _menu: PreferencesMenuItem[]
|
|
||||||
private _extensionLatestVersions: PackageProvider = new PackageProvider(new Map())
|
|
||||||
|
|
||||||
constructor(private application: WebApplication, private readonly _enableUnfinishedFeatures: boolean) {
|
|
||||||
if (featureTrunkVaultsEnabled()) {
|
|
||||||
PREFERENCES_MENU_ITEMS.splice(3, 0, { id: 'vaults', label: 'Vaults', icon: 'safe-square', order: 5 })
|
|
||||||
READY_PREFERENCES_MENU_ITEMS.splice(3, 0, { id: 'vaults', label: 'Vaults', icon: 'safe-square', order: 5 })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (featureTrunkHomeServerEnabled()) {
|
|
||||||
DESKTOP_PREFERENCES_MENU_ITEMS.push({ id: 'home-server', label: 'Home Server', icon: 'server', order: 5 })
|
|
||||||
}
|
|
||||||
|
|
||||||
let menuItems = this._enableUnfinishedFeatures ? PREFERENCES_MENU_ITEMS : READY_PREFERENCES_MENU_ITEMS
|
|
||||||
|
|
||||||
if (isDesktopApplication()) {
|
|
||||||
menuItems = [...menuItems, ...DESKTOP_PREFERENCES_MENU_ITEMS]
|
|
||||||
}
|
|
||||||
|
|
||||||
this._menu = menuItems.sort((a, b) => a.order - b.order)
|
|
||||||
|
|
||||||
this.loadLatestVersions()
|
|
||||||
|
|
||||||
makeAutoObservable<
|
|
||||||
PreferencesMenu,
|
|
||||||
'_selectedPane' | '_twoFactorAuth' | '_extensionPanes' | '_extensionLatestVersions' | 'loadLatestVersions'
|
|
||||||
>(this, {
|
|
||||||
_twoFactorAuth: observable,
|
|
||||||
_selectedPane: observable,
|
|
||||||
_extensionPanes: observable.ref,
|
|
||||||
_extensionLatestVersions: observable.ref,
|
|
||||||
loadLatestVersions: action,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private loadLatestVersions(): void {
|
|
||||||
PackageProvider.load()
|
|
||||||
.then((versions) => {
|
|
||||||
if (versions) {
|
|
||||||
this._extensionLatestVersions = versions
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(console.error)
|
|
||||||
}
|
|
||||||
|
|
||||||
get extensionsLatestVersions(): PackageProvider {
|
|
||||||
return this._extensionLatestVersions
|
|
||||||
}
|
|
||||||
|
|
||||||
get menuItems(): SelectableMenuItem[] {
|
|
||||||
const menuItems = this._menu.map((preference) => {
|
|
||||||
const item: SelectableMenuItem = {
|
|
||||||
...preference,
|
|
||||||
selected: preference.id === this._selectedPane,
|
|
||||||
hasBubble: this.sectionHasBubble(preference.id),
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
})
|
|
||||||
|
|
||||||
return menuItems
|
|
||||||
}
|
|
||||||
|
|
||||||
get selectedMenuItem(): PreferencesMenuItem | undefined {
|
|
||||||
return this._menu.find((item) => item.id === this._selectedPane)
|
|
||||||
}
|
|
||||||
|
|
||||||
get selectedPaneId(): PreferenceId {
|
|
||||||
if (this.selectedMenuItem != undefined) {
|
|
||||||
return this.selectedMenuItem.id
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'account'
|
|
||||||
}
|
|
||||||
|
|
||||||
selectPane = (key: PreferenceId) => {
|
|
||||||
this._selectedPane = key
|
|
||||||
}
|
|
||||||
|
|
||||||
sectionHasBubble(id: PreferenceId): boolean {
|
|
||||||
if (id === 'security') {
|
|
||||||
return securityPrefsHasBubble(this.application)
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,11 +3,11 @@ import { FunctionComponent, useMemo } from 'react'
|
|||||||
import Dropdown from '../Dropdown/Dropdown'
|
import Dropdown from '../Dropdown/Dropdown'
|
||||||
import { DropdownItem } from '../Dropdown/DropdownItem'
|
import { DropdownItem } from '../Dropdown/DropdownItem'
|
||||||
import PreferencesMenuItem from './PreferencesComponents/MenuItem'
|
import PreferencesMenuItem from './PreferencesComponents/MenuItem'
|
||||||
import { PreferencesMenu } from './PreferencesMenu'
|
import { PreferencesSessionController } from './Controller/PreferencesSessionController'
|
||||||
import { PreferenceId } from '@standardnotes/ui-services'
|
import { PreferenceId } from '@standardnotes/ui-services'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
menu: PreferencesMenu
|
menu: PreferencesSessionController
|
||||||
}
|
}
|
||||||
|
|
||||||
const PreferencesMenuView: FunctionComponent<Props> = ({ menu }) => {
|
const PreferencesMenuView: FunctionComponent<Props> = ({ menu }) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import RoundIconButton from '@/Components/Button/RoundIconButton'
|
import RoundIconButton from '@/Components/Button/RoundIconButton'
|
||||||
import { FunctionComponent, useEffect, useMemo } from 'react'
|
import { FunctionComponent, useEffect, useMemo } from 'react'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { PreferencesMenu } from './PreferencesMenu'
|
import { PreferencesSessionController } from './Controller/PreferencesSessionController'
|
||||||
import PreferencesCanvas from './PreferencesCanvas'
|
import PreferencesCanvas from './PreferencesCanvas'
|
||||||
import { PreferencesProps } from './PreferencesProps'
|
import { PreferencesProps } from './PreferencesProps'
|
||||||
import { useAndroidBackHandler } from '@/NativeMobileWeb/useAndroidBackHandler'
|
import { useAndroidBackHandler } from '@/NativeMobileWeb/useAndroidBackHandler'
|
||||||
@@ -19,7 +19,7 @@ const PreferencesView: FunctionComponent<PreferencesProps> = ({
|
|||||||
mfaProvider,
|
mfaProvider,
|
||||||
}) => {
|
}) => {
|
||||||
const menu = useMemo(
|
const menu = useMemo(
|
||||||
() => new PreferencesMenu(application, viewControllerManager.enableUnfinishedFeatures),
|
() => new PreferencesSessionController(application, viewControllerManager.enableUnfinishedFeatures),
|
||||||
[viewControllerManager.enableUnfinishedFeatures, application],
|
[viewControllerManager.enableUnfinishedFeatures, application],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import Icon from '@/Components/Icon/Icon'
|
||||||
|
|
||||||
|
export const CheckmarkCircle = () => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={
|
||||||
|
'peer flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-success text-success-contrast'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon type={'check'} size="small" />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import Icon from '@/Components/Icon/Icon'
|
||||||
|
|
||||||
|
export const ErrorCircle = () => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={
|
||||||
|
'peer flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-danger text-danger-contrast'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon type={'warning'} size="small" />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user