Skip to content

Commit

Permalink
feat: error decrypting preferences section (#990)
Browse files Browse the repository at this point in the history
* feat: error decrypting preferences section

* chore: upgrade snjs
  • Loading branch information
moughxyz authored Apr 21, 2022
1 parent 1391f88 commit fdf290e
Show file tree
Hide file tree
Showing 10 changed files with 266 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,9 @@ export const ChallengeModal: FunctionComponent<Props> = ({
[values],
)

const closeModal = () => {
const cancelChallenge = () => {
if (challenge.cancelable) {
application.cancelChallenge(challenge)
onDismiss(challenge).catch(console.error)
}
}
Expand Down Expand Up @@ -187,7 +188,7 @@ export const ChallengeModal: FunctionComponent<Props> = ({
className={`sn-component ${
challenge.reason === ChallengeReason.ApplicationUnlock ? 'challenge-modal-overlay' : ''
}`}
onDismiss={closeModal}
onDismiss={cancelChallenge}
dangerouslyBypassFocusLock={bypassModalFocusLock}
>
<DialogContent
Expand All @@ -199,7 +200,7 @@ export const ChallengeModal: FunctionComponent<Props> = ({
>
{challenge.cancelable && (
<button
onClick={closeModal}
onClick={cancelChallenge}
aria-label="Close modal"
className="flex p-1 bg-transparent border-0 cursor-pointer absolute top-4 right-4"
>
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
)
})
Original file line number Diff line number Diff line change
Expand Up @@ -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} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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>
)
37 changes: 26 additions & 11 deletions app/assets/javascripts/Components/Preferences/PreferencesMenu.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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 {
Expand All @@ -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' },
Expand All @@ -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(),
Expand Down Expand Up @@ -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
}
Expand All @@ -113,15 +120,23 @@ 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
}

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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)}
/>
))}
Expand Down
12 changes: 11 additions & 1 deletion app/assets/javascripts/Services/ArchiveManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
BackupFile,
BackupFileDecryptedContextualPayload,
NoteContent,
EncryptedItemInterface,
} from '@standardnotes/snjs'

function sanitizeFileName(name: string): string {
Expand Down Expand Up @@ -148,12 +149,21 @@ export class ArchiveManager {
return this.textFile
}

downloadData(data: Blob | ObjectURL, fileName: string) {
downloadData(data: Blob | ObjectURL, fileName: string): void {
const link = document.createElement('a')
link.setAttribute('download', fileName)
link.href = typeof data === 'string' ? data : this.hrefForData(data)
document.body.appendChild(link)
link.click()
link.remove()
}

downloadEncryptedItem(item: EncryptedItemInterface) {
this.downloadData(new Blob([JSON.stringify(item.payload.ejected())]), `${item.uuid}.txt`)
}

downloadEncryptedItems(items: EncryptedItemInterface[]) {
const data = JSON.stringify(items.map((i) => i.payload.ejected()))
this.downloadData(new Blob([data]), 'errored-items.txt')
}
}
4 changes: 4 additions & 0 deletions app/assets/stylesheets/_sn.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1051,6 +1051,10 @@
color: var(--sn-stylekit-neutral-contrast-color);
}

.color-warning {
color: var(--sn-stylekit-warning-color);
}

.active\:bg-info:active {
background-color: var(--sn-stylekit-info-color);
}
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@
"@reach/tooltip": "^0.16.2",
"@reach/visually-hidden": "^0.16.0",
"@standardnotes/components": "1.7.15",
"@standardnotes/filepicker": "1.11.0",
"@standardnotes/filepicker": "1.12.0",
"@standardnotes/sncrypto-web": "1.8.3",
"@standardnotes/snjs": "2.97.4",
"@standardnotes/snjs": "2.97.7",
"@standardnotes/stylekit": "5.23.0",
"@zip.js/zip.js": "^2.4.7",
"mobx": "^6.5.0",
Expand Down
Loading

0 comments on commit fdf290e

Please sign in to comment.