feat: error decrypting preferences section (#990)
* feat: error decrypting preferences section * chore: upgrade snjs
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import {
|
||||
PreferencesGroup,
|
||||
PreferencesSegment,
|
||||
Text,
|
||||
Title,
|
||||
Subtitle,
|
||||
} from '@/Components/Preferences/PreferencesComponents'
|
||||
import {
|
||||
ButtonType,
|
||||
DisplayStringForContentType,
|
||||
EncryptedItemInterface,
|
||||
} from '@standardnotes/snjs'
|
||||
import { Button } from '@/Components/Button/Button'
|
||||
import { HorizontalSeparator } from '@/Components/Shared/HorizontalSeparator'
|
||||
import { useState } from 'preact/hooks'
|
||||
|
||||
export const ErroredItems: FunctionComponent<{ appState: AppState }> = observer(({ appState }) => {
|
||||
const app = appState.application
|
||||
|
||||
const [erroredItems, setErroredItems] = useState(app.items.invalidItems)
|
||||
|
||||
const getContentTypeDisplay = (item: EncryptedItemInterface): string => {
|
||||
const display = DisplayStringForContentType(item.content_type)
|
||||
if (display) {
|
||||
return `${display[0].toUpperCase()}${display.slice(1)}`
|
||||
} else {
|
||||
return `Item of type ${item.content_type}`
|
||||
}
|
||||
}
|
||||
|
||||
const deleteItem = async (item: EncryptedItemInterface): Promise<void> => {
|
||||
return deleteItems([item])
|
||||
}
|
||||
|
||||
const deleteItems = async (items: EncryptedItemInterface[]): Promise<void> => {
|
||||
const confirmed = await app.alertService.confirm(
|
||||
`Are you sure you want to permanently delete ${items.length} item(s)?`,
|
||||
undefined,
|
||||
'Delete',
|
||||
ButtonType.Danger,
|
||||
)
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
|
||||
void app.mutator.deleteItems(items)
|
||||
|
||||
setErroredItems(app.items.invalidItems)
|
||||
}
|
||||
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>
|
||||
Error Decrypting Items <span className="ml-1 color-warning">⚠️</span>
|
||||
</Title>
|
||||
<Text>{`${erroredItems.length} items are errored and could not be decrypted.`}</Text>
|
||||
<div className="flex">
|
||||
<Button
|
||||
className="min-w-20 mt-3 mr-2"
|
||||
variant="normal"
|
||||
label="Export all"
|
||||
onClick={() => {
|
||||
void app.getArchiveService().downloadEncryptedItems(erroredItems)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
className="min-w-20 mt-3 mr-2"
|
||||
variant="normal"
|
||||
dangerStyle={true}
|
||||
label="Delete all"
|
||||
onClick={() => {
|
||||
void deleteItems(erroredItems)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<HorizontalSeparator classes="mt-5 mb-3" />
|
||||
|
||||
{erroredItems.map((item, index) => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<Subtitle>{`${getContentTypeDisplay(item)} created on ${
|
||||
item.createdAtString
|
||||
}`}</Subtitle>
|
||||
<Text>
|
||||
<div>Item ID: {item.uuid}</div>
|
||||
<div>Last Modified: {item.updatedAtString}</div>
|
||||
</Text>
|
||||
<div className="flex">
|
||||
<Button
|
||||
className="min-w-20 mt-3 mr-2"
|
||||
variant="normal"
|
||||
label="Attempt decryption"
|
||||
onClick={() => {
|
||||
void app.presentKeyRecoveryWizard()
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
className="min-w-20 mt-3 mr-2"
|
||||
variant="normal"
|
||||
label="Export"
|
||||
onClick={() => {
|
||||
void app.getArchiveService().downloadEncryptedItem(item)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
className="min-w-20 mt-3 mr-2"
|
||||
variant="normal"
|
||||
dangerStyle={true}
|
||||
label="Delete"
|
||||
onClick={() => {
|
||||
void deleteItem(item)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{index < erroredItems.length - 1 && <HorizontalSeparator classes="mt-5 mb-3" />}
|
||||
</>
|
||||
)
|
||||
})}
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
)
|
||||
})
|
||||
@@ -8,15 +8,21 @@ import { Encryption } from './Encryption'
|
||||
import { PasscodeLock } from './PasscodeLock'
|
||||
import { Privacy } from './Privacy'
|
||||
import { Protections } from './Protections'
|
||||
import { ErroredItems } from './ErroredItems'
|
||||
|
||||
interface SecurityProps extends MfaProps {
|
||||
appState: AppState
|
||||
application: WebApplication
|
||||
}
|
||||
|
||||
export const securityPrefsHasBubble = (application: WebApplication): boolean => {
|
||||
return application.items.invalidItems.length > 0
|
||||
}
|
||||
|
||||
export const Security: FunctionComponent<SecurityProps> = (props) => (
|
||||
<PreferencesPane>
|
||||
<Encryption appState={props.appState} />
|
||||
{props.application.items.invalidItems.length > 0 && <ErroredItems appState={props.appState} />}
|
||||
<Protections application={props.application} />
|
||||
<TwoFactorAuthWrapper mfaProvider={props.mfaProvider} userProvider={props.userProvider} />
|
||||
<PasscodeLock appState={props.appState} application={props.application} />
|
||||
|
||||
@@ -6,10 +6,17 @@ interface Props {
|
||||
iconType: IconType
|
||||
label: string
|
||||
selected: boolean
|
||||
hasBubble?: boolean
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
export const MenuItem: FunctionComponent<Props> = ({ iconType, label, selected, onClick }) => (
|
||||
export const MenuItem: FunctionComponent<Props> = ({
|
||||
iconType,
|
||||
label,
|
||||
selected,
|
||||
onClick,
|
||||
hasBubble,
|
||||
}) => (
|
||||
<div
|
||||
className={`preferences-menu-item select-none ${selected ? 'selected' : ''}`}
|
||||
onClick={(e) => {
|
||||
@@ -20,5 +27,6 @@ export const MenuItem: FunctionComponent<Props> = ({ iconType, label, selected,
|
||||
<Icon className="icon" type={iconType} />
|
||||
<div className="min-w-1" />
|
||||
{label}
|
||||
{hasBubble && <span className="ml-1 color-warning">⚠️</span>}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { action, makeAutoObservable, observable } from 'mobx'
|
||||
import { FeatureIdentifier, IconType } from '@standardnotes/snjs'
|
||||
import { IconType } from '@standardnotes/snjs'
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { ExtensionsLatestVersions } from './Panes/Extensions/ExtensionsLatestVersions'
|
||||
import { securityPrefsHasBubble } from './Panes'
|
||||
|
||||
const PREFERENCE_IDS = [
|
||||
'general',
|
||||
@@ -17,10 +18,12 @@ const PREFERENCE_IDS = [
|
||||
] as const
|
||||
|
||||
export type PreferenceId = typeof PREFERENCE_IDS[number]
|
||||
|
||||
interface PreferencesMenuItem {
|
||||
readonly id: PreferenceId | FeatureIdentifier
|
||||
readonly id: PreferenceId
|
||||
readonly icon: IconType
|
||||
readonly label: string
|
||||
readonly hasBubble?: boolean
|
||||
}
|
||||
|
||||
interface SelectableMenuItem extends PreferencesMenuItem {
|
||||
@@ -34,8 +37,8 @@ const PREFERENCES_MENU_ITEMS: PreferencesMenuItem[] = [
|
||||
{ id: 'account', label: 'Account', icon: 'user' },
|
||||
{ id: 'general', label: 'General', icon: 'settings' },
|
||||
{ id: 'security', label: 'Security', icon: 'security' },
|
||||
{ id: 'appearance', label: 'Appearance', icon: 'themes' },
|
||||
{ id: 'backups', label: 'Backups', icon: 'restore' },
|
||||
{ id: 'appearance', label: 'Appearance', icon: 'themes' },
|
||||
{ id: 'listed', label: 'Listed', icon: 'listed' },
|
||||
{ id: 'shortcuts', label: 'Shortcuts', icon: 'keyboard' },
|
||||
{ id: 'accessibility', label: 'Accessibility', icon: 'accessibility' },
|
||||
@@ -47,14 +50,14 @@ const READY_PREFERENCES_MENU_ITEMS: PreferencesMenuItem[] = [
|
||||
{ id: 'account', label: 'Account', icon: 'user' },
|
||||
{ id: 'general', label: 'General', icon: 'settings' },
|
||||
{ id: 'security', label: 'Security', icon: 'security' },
|
||||
{ id: 'appearance', label: 'Appearance', icon: 'themes' },
|
||||
{ id: 'backups', label: 'Backups', icon: 'restore' },
|
||||
{ id: 'appearance', label: 'Appearance', icon: 'themes' },
|
||||
{ id: 'listed', label: 'Listed', icon: 'listed' },
|
||||
{ id: 'help-feedback', label: 'Help & feedback', icon: 'help' },
|
||||
]
|
||||
|
||||
export class PreferencesMenu {
|
||||
private _selectedPane: PreferenceId | FeatureIdentifier = 'account'
|
||||
private _selectedPane: PreferenceId = 'account'
|
||||
private _menu: PreferencesMenuItem[]
|
||||
private _extensionLatestVersions: ExtensionsLatestVersions = new ExtensionsLatestVersions(
|
||||
new Map(),
|
||||
@@ -101,10 +104,14 @@ export class PreferencesMenu {
|
||||
}
|
||||
|
||||
get menuItems(): SelectableMenuItem[] {
|
||||
const menuItems = this._menu.map((preference) => ({
|
||||
...preference,
|
||||
selected: preference.id === this._selectedPane,
|
||||
}))
|
||||
const menuItems = this._menu.map((preference) => {
|
||||
const item: SelectableMenuItem = {
|
||||
...preference,
|
||||
selected: preference.id === this._selectedPane,
|
||||
hasBubble: this.sectionHasBubble(preference.id),
|
||||
}
|
||||
return item
|
||||
})
|
||||
|
||||
return menuItems
|
||||
}
|
||||
@@ -113,7 +120,7 @@ export class PreferencesMenu {
|
||||
return this._menu.find((item) => item.id === this._selectedPane)
|
||||
}
|
||||
|
||||
get selectedPaneId(): PreferenceId | FeatureIdentifier {
|
||||
get selectedPaneId(): PreferenceId {
|
||||
if (this.selectedMenuItem != undefined) {
|
||||
return this.selectedMenuItem.id
|
||||
}
|
||||
@@ -121,7 +128,15 @@ export class PreferencesMenu {
|
||||
return 'account'
|
||||
}
|
||||
|
||||
selectPane(key: PreferenceId | FeatureIdentifier): void {
|
||||
selectPane(key: PreferenceId): void {
|
||||
this._selectedPane = key
|
||||
}
|
||||
|
||||
sectionHasBubble(id: PreferenceId): boolean {
|
||||
if (id === 'security') {
|
||||
return securityPrefsHasBubble(this.application)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ export const PreferencesMenuView: FunctionComponent<{
|
||||
iconType={pref.icon}
|
||||
label={pref.label}
|
||||
selected={pref.selected}
|
||||
hasBubble={pref.hasBubble}
|
||||
onClick={() => menu.selectPane(pref.id)}
|
||||
/>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user