refactor: repo (#1070)
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import { STRING_E2E_ENABLED, STRING_ENC_NOT_ENABLED, STRING_LOCAL_ENC_ENABLED } from '@/Constants/Strings'
|
||||
import { ViewControllerManager } from '@/Services/ViewControllerManager'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'react'
|
||||
import { Title, Text } from '../../PreferencesComponents/Content'
|
||||
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
|
||||
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
|
||||
import EncryptionEnabled from './EncryptionEnabled'
|
||||
|
||||
type Props = { viewControllerManager: ViewControllerManager }
|
||||
|
||||
const Encryption: FunctionComponent<Props> = ({ viewControllerManager }) => {
|
||||
const app = viewControllerManager.application
|
||||
const hasUser = app.hasAccount()
|
||||
const hasPasscode = app.hasPasscode()
|
||||
const isEncryptionEnabled = app.isEncryptionAvailable()
|
||||
|
||||
const encryptionStatusString = hasUser
|
||||
? STRING_E2E_ENABLED
|
||||
: hasPasscode
|
||||
? STRING_LOCAL_ENC_ENABLED
|
||||
: STRING_ENC_NOT_ENABLED
|
||||
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>Encryption</Title>
|
||||
<Text>{encryptionStatusString}</Text>
|
||||
|
||||
{isEncryptionEnabled && <EncryptionEnabled viewControllerManager={viewControllerManager} />}
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(Encryption)
|
||||
@@ -0,0 +1,39 @@
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { ViewControllerManager } from '@/Services/ViewControllerManager'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'react'
|
||||
import EncryptionStatusItem from './EncryptionStatusItem'
|
||||
import { formatCount } from './formatCount'
|
||||
|
||||
type Props = {
|
||||
viewControllerManager: ViewControllerManager
|
||||
}
|
||||
|
||||
const EncryptionEnabled: FunctionComponent<Props> = ({ viewControllerManager }) => {
|
||||
const count = viewControllerManager.accountMenuController.structuredNotesAndTagsCount
|
||||
const notes = formatCount(count.notes, 'notes')
|
||||
const tags = formatCount(count.tags, 'tags')
|
||||
const archived = formatCount(count.archived, 'archived notes')
|
||||
const deleted = formatCount(count.deleted, 'trashed notes')
|
||||
|
||||
const noteIcon = <Icon type="rich-text" className="min-w-5 min-h-5" />
|
||||
const tagIcon = <Icon type="hashtag" className="min-w-5 min-h-5" />
|
||||
const archiveIcon = <Icon type="archive" className="min-w-5 min-h-5" />
|
||||
const trashIcon = <Icon type="trash" className="min-w-5 min-h-5" />
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row items-start pb-1 pt-1.5">
|
||||
<EncryptionStatusItem status={notes} icon={noteIcon} />
|
||||
<div className="min-w-3" />
|
||||
<EncryptionStatusItem status={tags} icon={tagIcon} />
|
||||
</div>
|
||||
<div className="flex flex-row items-start">
|
||||
<EncryptionStatusItem status={archived} icon={archiveIcon} />
|
||||
<div className="min-w-3" />
|
||||
<EncryptionStatusItem status={deleted} icon={trashIcon} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(EncryptionEnabled)
|
||||
@@ -0,0 +1,20 @@
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { FunctionComponent, ReactNode } from 'react'
|
||||
|
||||
type Props = {
|
||||
icon: ReactNode
|
||||
status: string
|
||||
checkmark?: boolean
|
||||
}
|
||||
|
||||
const EncryptionStatusItem: FunctionComponent<Props> = ({ icon, status, checkmark = true }) => (
|
||||
<div className="w-full rounded py-1.5 px-3 text-input my-1 min-h-8 flex flex-row items-center bg-contrast no-border focus-within:ring-info">
|
||||
{icon}
|
||||
<div className="min-w-3 min-h-1" />
|
||||
<div className="flex-grow color-text text-sm">{status}</div>
|
||||
<div className="min-w-3 min-h-1" />
|
||||
{checkmark && <Icon className="success min-w-4 min-h-4" type="check-bold" />}
|
||||
</div>
|
||||
)
|
||||
|
||||
export default EncryptionStatusItem
|
||||
@@ -0,0 +1,140 @@
|
||||
import { ViewControllerManager } from '@/Services/ViewControllerManager'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Fragment, FunctionComponent, useState } from 'react'
|
||||
import { Text, Title, Subtitle } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
import {
|
||||
ButtonType,
|
||||
ClientDisplayableError,
|
||||
DisplayStringForContentType,
|
||||
EncryptedItemInterface,
|
||||
} from '@standardnotes/snjs'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
|
||||
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
|
||||
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
|
||||
|
||||
type Props = { viewControllerManager: ViewControllerManager }
|
||||
|
||||
const ErroredItems: FunctionComponent<Props> = ({ viewControllerManager }: Props) => {
|
||||
const app = viewControllerManager.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)
|
||||
}
|
||||
|
||||
const attemptDecryption = (item: EncryptedItemInterface): void => {
|
||||
const errorOrTrue = app.canAttemptDecryptionOfItem(item)
|
||||
|
||||
if (errorOrTrue instanceof ClientDisplayableError) {
|
||||
void app.alertService.showErrorAlert(errorOrTrue)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
app.presentKeyRecoveryWizard()
|
||||
}
|
||||
|
||||
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-2.5 mb-3" />
|
||||
|
||||
{erroredItems.map((item, index) => {
|
||||
return (
|
||||
<Fragment key={item.uuid}>
|
||||
<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={() => {
|
||||
attemptDecryption(item)
|
||||
}}
|
||||
/>
|
||||
<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-2.5 mb-3" />}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(ErroredItems)
|
||||
@@ -0,0 +1,263 @@
|
||||
import {
|
||||
STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE,
|
||||
STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL,
|
||||
STRING_E2E_ENABLED,
|
||||
STRING_ENC_NOT_ENABLED,
|
||||
STRING_LOCAL_ENC_ENABLED,
|
||||
STRING_NON_MATCHING_PASSCODES,
|
||||
StringUtils,
|
||||
Strings,
|
||||
} from '@/Constants/Strings'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { preventRefreshing } from '@/Utils'
|
||||
import { alertDialog } from '@/Services/AlertService'
|
||||
import { ChangeEventHandler, FormEvent, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { ApplicationEvent } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { ViewControllerManager } from '@/Services/ViewControllerManager'
|
||||
import { Title, Text } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
|
||||
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
viewControllerManager: ViewControllerManager
|
||||
}
|
||||
|
||||
const PasscodeLock = ({ application, viewControllerManager }: Props) => {
|
||||
const keyStorageInfo = StringUtils.keyStorageInfo(application)
|
||||
const passcodeAutoLockOptions = application.getAutolockService().getAutoLockIntervalOptions()
|
||||
|
||||
const { setIsEncryptionEnabled, setIsBackupEncrypted, setEncryptionStatusString } =
|
||||
viewControllerManager.accountMenuController
|
||||
|
||||
const passcodeInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const [passcode, setPasscode] = useState<string>()
|
||||
const [passcodeConfirmation, setPasscodeConfirmation] = useState<string>()
|
||||
const [selectedAutoLockInterval, setSelectedAutoLockInterval] = useState<unknown>(null)
|
||||
const [isPasscodeFocused, setIsPasscodeFocused] = useState(false)
|
||||
const [showPasscodeForm, setShowPasscodeForm] = useState(false)
|
||||
const [canAddPasscode, setCanAddPasscode] = useState(!application.isEphemeralSession())
|
||||
const [hasPasscode, setHasPasscode] = useState(application.hasPasscode())
|
||||
|
||||
const handleAddPassCode = () => {
|
||||
setShowPasscodeForm(true)
|
||||
setIsPasscodeFocused(true)
|
||||
}
|
||||
|
||||
const changePasscodePressed = () => {
|
||||
handleAddPassCode()
|
||||
}
|
||||
|
||||
const reloadAutoLockInterval = useCallback(async () => {
|
||||
const interval = await application.getAutolockService().getAutoLockInterval()
|
||||
setSelectedAutoLockInterval(interval)
|
||||
}, [application])
|
||||
|
||||
const refreshEncryptionStatus = useCallback(() => {
|
||||
const hasUser = application.hasAccount()
|
||||
const hasPasscode = application.hasPasscode()
|
||||
|
||||
setHasPasscode(hasPasscode)
|
||||
|
||||
const encryptionEnabled = hasUser || hasPasscode
|
||||
|
||||
const encryptionStatusString = hasUser
|
||||
? STRING_E2E_ENABLED
|
||||
: hasPasscode
|
||||
? STRING_LOCAL_ENC_ENABLED
|
||||
: STRING_ENC_NOT_ENABLED
|
||||
|
||||
setEncryptionStatusString(encryptionStatusString)
|
||||
setIsEncryptionEnabled(encryptionEnabled)
|
||||
setIsBackupEncrypted(encryptionEnabled)
|
||||
}, [application, setEncryptionStatusString, setIsBackupEncrypted, setIsEncryptionEnabled])
|
||||
|
||||
const selectAutoLockInterval = async (interval: number) => {
|
||||
if (!(await application.authorizeAutolockIntervalChange())) {
|
||||
return
|
||||
}
|
||||
await application.getAutolockService().setAutoLockInterval(interval)
|
||||
reloadAutoLockInterval().catch(console.error)
|
||||
}
|
||||
|
||||
const removePasscodePressed = async () => {
|
||||
await preventRefreshing(STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL, async () => {
|
||||
if (await application.removePasscode()) {
|
||||
await application.getAutolockService().deleteAutolockPreference()
|
||||
await reloadAutoLockInterval()
|
||||
refreshEncryptionStatus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handlePasscodeChange: ChangeEventHandler<HTMLInputElement> = (event) => {
|
||||
const { value } = event.target
|
||||
setPasscode(value)
|
||||
}
|
||||
|
||||
const handleConfirmPasscodeChange: ChangeEventHandler<HTMLInputElement> = (event) => {
|
||||
const { value } = event.target
|
||||
setPasscodeConfirmation(value)
|
||||
}
|
||||
|
||||
const submitPasscodeForm = async (event: MouseEvent | FormEvent) => {
|
||||
event.preventDefault()
|
||||
|
||||
if (!passcode || passcode.length === 0) {
|
||||
await alertDialog({
|
||||
text: Strings.enterPasscode,
|
||||
})
|
||||
}
|
||||
|
||||
if (passcode !== passcodeConfirmation) {
|
||||
await alertDialog({
|
||||
text: STRING_NON_MATCHING_PASSCODES,
|
||||
})
|
||||
setIsPasscodeFocused(true)
|
||||
return
|
||||
}
|
||||
|
||||
await preventRefreshing(STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE, async () => {
|
||||
const successful = application.hasPasscode()
|
||||
? await application.changePasscode(passcode as string)
|
||||
: await application.addPasscode(passcode as string)
|
||||
|
||||
if (!successful) {
|
||||
setIsPasscodeFocused(true)
|
||||
}
|
||||
})
|
||||
|
||||
setPasscode(undefined)
|
||||
setPasscodeConfirmation(undefined)
|
||||
setShowPasscodeForm(false)
|
||||
|
||||
refreshEncryptionStatus()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refreshEncryptionStatus()
|
||||
}, [refreshEncryptionStatus])
|
||||
|
||||
// `reloadAutoLockInterval` gets interval asynchronously, therefore we call `useEffect` to set initial
|
||||
// value of `selectedAutoLockInterval`
|
||||
useEffect(() => {
|
||||
reloadAutoLockInterval().catch(console.error)
|
||||
}, [reloadAutoLockInterval])
|
||||
|
||||
useEffect(() => {
|
||||
if (isPasscodeFocused) {
|
||||
passcodeInputRef.current?.focus()
|
||||
setIsPasscodeFocused(false)
|
||||
}
|
||||
}, [isPasscodeFocused])
|
||||
|
||||
// Add the required event observers
|
||||
useEffect(() => {
|
||||
const removeKeyStatusChangedObserver = application.addEventObserver(async () => {
|
||||
setCanAddPasscode(!application.isEphemeralSession())
|
||||
setHasPasscode(application.hasPasscode())
|
||||
setShowPasscodeForm(false)
|
||||
}, ApplicationEvent.KeyStatusChanged)
|
||||
|
||||
return () => {
|
||||
removeKeyStatusChangedObserver()
|
||||
}
|
||||
}, [application])
|
||||
|
||||
const cancelPasscodeForm = () => {
|
||||
setShowPasscodeForm(false)
|
||||
setPasscode(undefined)
|
||||
setPasscodeConfirmation(undefined)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>Passcode Lock</Title>
|
||||
|
||||
{!hasPasscode && canAddPasscode && (
|
||||
<>
|
||||
<Text className="mb-3">Add a passcode to lock the application and encrypt on-device key storage.</Text>
|
||||
|
||||
{keyStorageInfo && <Text className="mb-3">{keyStorageInfo}</Text>}
|
||||
|
||||
{!showPasscodeForm && <Button label="Add passcode" onClick={handleAddPassCode} variant="primary" />}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!hasPasscode && !canAddPasscode && (
|
||||
<Text>
|
||||
Adding a passcode is not supported in temporary sessions. Please sign out, then sign back in with the
|
||||
"Stay signed in" option checked.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{showPasscodeForm && (
|
||||
<form className="sk-panel-form" onSubmit={submitPasscodeForm}>
|
||||
<div className="sk-panel-row" />
|
||||
<input
|
||||
className="sk-input contrast"
|
||||
type="password"
|
||||
ref={passcodeInputRef}
|
||||
value={passcode ? passcode : ''}
|
||||
onChange={handlePasscodeChange}
|
||||
placeholder="Passcode"
|
||||
/>
|
||||
<input
|
||||
className="sk-input contrast"
|
||||
type="password"
|
||||
value={passcodeConfirmation ? passcodeConfirmation : ''}
|
||||
onChange={handleConfirmPasscodeChange}
|
||||
placeholder="Confirm Passcode"
|
||||
/>
|
||||
<div className="min-h-2" />
|
||||
<Button variant="primary" onClick={submitPasscodeForm} label="Set Passcode" className="mr-3" />
|
||||
<Button variant="normal" onClick={cancelPasscodeForm} label="Cancel" />
|
||||
</form>
|
||||
)}
|
||||
|
||||
{hasPasscode && !showPasscodeForm && (
|
||||
<>
|
||||
<Text>Passcode lock is enabled.</Text>
|
||||
<div className="flex flex-row mt-3">
|
||||
<Button variant="normal" label="Change Passcode" onClick={changePasscodePressed} className="mr-3" />
|
||||
<Button dangerStyle={true} label="Remove Passcode" onClick={removePasscodePressed} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
|
||||
{hasPasscode && (
|
||||
<>
|
||||
<div className="min-h-3" />
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>Autolock</Title>
|
||||
<Text className="mb-3">The autolock timer begins when the window or tab loses focus.</Text>
|
||||
<div className="flex flex-row items-center">
|
||||
{passcodeAutoLockOptions.map((option) => {
|
||||
return (
|
||||
<a
|
||||
key={option.value}
|
||||
className={`sk-a info mr-3 ${option.value === selectedAutoLockInterval ? 'boxed' : ''}`}
|
||||
onClick={() => selectAutoLockInterval(option.value)}
|
||||
>
|
||||
{option.label}
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(PasscodeLock)
|
||||
@@ -0,0 +1,141 @@
|
||||
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
|
||||
import Switch from '@/Components/Switch/Switch'
|
||||
import { Subtitle, Text, Title } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { MuteSignInEmailsOption, LogSessionUserAgentOption, SettingName } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
|
||||
import { STRING_FAILED_TO_UPDATE_USER_SETTING } from '@/Constants/Strings'
|
||||
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
|
||||
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
}
|
||||
|
||||
const Privacy: FunctionComponent<Props> = ({ application }: Props) => {
|
||||
const [signInEmailsMutedValue, setSignInEmailsMutedValue] = useState<MuteSignInEmailsOption>(
|
||||
MuteSignInEmailsOption.NotMuted,
|
||||
)
|
||||
const [sessionUaLoggingValue, setSessionUaLoggingValue] = useState<LogSessionUserAgentOption>(
|
||||
LogSessionUserAgentOption.Enabled,
|
||||
)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
const updateSetting = async (settingName: SettingName, payload: string): Promise<boolean> => {
|
||||
try {
|
||||
await application.settings.updateSetting(settingName, payload, false)
|
||||
return true
|
||||
} catch (e) {
|
||||
application.alertService.alert(STRING_FAILED_TO_UPDATE_USER_SETTING).catch(console.error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const loadSettings = useCallback(async () => {
|
||||
if (!application.getUser()) {
|
||||
return
|
||||
}
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
const userSettings = await application.settings.listSettings()
|
||||
setSignInEmailsMutedValue(
|
||||
userSettings.getSettingValue<MuteSignInEmailsOption>(
|
||||
SettingName.MuteSignInEmails,
|
||||
MuteSignInEmailsOption.NotMuted,
|
||||
),
|
||||
)
|
||||
setSessionUaLoggingValue(
|
||||
userSettings.getSettingValue<LogSessionUserAgentOption>(
|
||||
SettingName.LogSessionUserAgent,
|
||||
LogSessionUserAgentOption.Enabled,
|
||||
),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [application])
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings().catch(console.error)
|
||||
}, [loadSettings])
|
||||
|
||||
const toggleMuteSignInEmails = async () => {
|
||||
const previousValue = signInEmailsMutedValue
|
||||
const newValue =
|
||||
previousValue === MuteSignInEmailsOption.Muted ? MuteSignInEmailsOption.NotMuted : MuteSignInEmailsOption.Muted
|
||||
setSignInEmailsMutedValue(newValue)
|
||||
|
||||
const updateResult = await updateSetting(SettingName.MuteSignInEmails, newValue)
|
||||
|
||||
if (!updateResult) {
|
||||
setSignInEmailsMutedValue(previousValue)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleSessionLogging = async () => {
|
||||
const previousValue = sessionUaLoggingValue
|
||||
const newValue =
|
||||
previousValue === LogSessionUserAgentOption.Enabled
|
||||
? LogSessionUserAgentOption.Disabled
|
||||
: LogSessionUserAgentOption.Enabled
|
||||
setSessionUaLoggingValue(newValue)
|
||||
|
||||
const updateResult = await updateSetting(SettingName.LogSessionUserAgent, newValue)
|
||||
|
||||
if (!updateResult) {
|
||||
setSessionUaLoggingValue(previousValue)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>Privacy</Title>
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<Subtitle>Disable sign-in notification emails</Subtitle>
|
||||
<Text>
|
||||
Disables email notifications when a new sign-in occurs on your account. (Email notifications are
|
||||
available to paid subscribers).
|
||||
</Text>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className={'sk-spinner info small flex-shrink-0 ml-2'} />
|
||||
) : (
|
||||
<Switch
|
||||
onChange={toggleMuteSignInEmails}
|
||||
checked={signInEmailsMutedValue === MuteSignInEmailsOption.Muted}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<HorizontalSeparator classes="my-4" />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<Subtitle>Session user agent logging</Subtitle>
|
||||
<Text>
|
||||
User agent logging allows you to identify the devices or browsers signed into your account. For
|
||||
increased privacy, you can disable this feature, which will remove all saved user agent values from our
|
||||
server, and disable future logging of this value.
|
||||
</Text>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className={'sk-spinner info small flex-shrink-0 ml-2'} />
|
||||
) : (
|
||||
<Switch
|
||||
onChange={toggleSessionLogging}
|
||||
checked={sessionUaLoggingValue === LogSessionUserAgentOption.Enabled}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(Privacy)
|
||||
@@ -0,0 +1,93 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { FunctionComponent, useCallback, useState, useEffect } from 'react'
|
||||
import { ApplicationEvent } from '@standardnotes/snjs'
|
||||
import { isSameDay } from '@/Utils'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
|
||||
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
|
||||
import { Title, Text } from '../../PreferencesComponents/Content'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
}
|
||||
|
||||
const Protections: FunctionComponent<Props> = ({ application }) => {
|
||||
const enableProtections = () => {
|
||||
application.clearProtectionSession().catch(console.error)
|
||||
}
|
||||
|
||||
const [hasProtections, setHasProtections] = useState(() => application.hasProtectionSources())
|
||||
|
||||
const getProtectionsDisabledUntil = useCallback((): string | null => {
|
||||
const protectionExpiry = application.getProtectionSessionExpiryDate()
|
||||
const now = new Date()
|
||||
if (protectionExpiry > now) {
|
||||
let f: Intl.DateTimeFormat
|
||||
if (isSameDay(protectionExpiry, now)) {
|
||||
f = new Intl.DateTimeFormat(undefined, {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
})
|
||||
} else {
|
||||
f = new Intl.DateTimeFormat(undefined, {
|
||||
weekday: 'long',
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
return f.format(protectionExpiry)
|
||||
}
|
||||
return null
|
||||
}, [application])
|
||||
|
||||
const [protectionsDisabledUntil, setProtectionsDisabledUntil] = useState(getProtectionsDisabledUntil())
|
||||
|
||||
useEffect(() => {
|
||||
const removeUnprotectedSessionBeginObserver = application.addEventObserver(async () => {
|
||||
setProtectionsDisabledUntil(getProtectionsDisabledUntil())
|
||||
}, ApplicationEvent.UnprotectedSessionBegan)
|
||||
|
||||
const removeUnprotectedSessionEndObserver = application.addEventObserver(async () => {
|
||||
setProtectionsDisabledUntil(getProtectionsDisabledUntil())
|
||||
}, ApplicationEvent.UnprotectedSessionExpired)
|
||||
|
||||
const removeKeyStatusChangedObserver = application.addEventObserver(async () => {
|
||||
setHasProtections(application.hasProtectionSources())
|
||||
}, ApplicationEvent.KeyStatusChanged)
|
||||
|
||||
return () => {
|
||||
removeUnprotectedSessionBeginObserver()
|
||||
removeUnprotectedSessionEndObserver()
|
||||
removeKeyStatusChangedObserver()
|
||||
}
|
||||
}, [application, getProtectionsDisabledUntil])
|
||||
|
||||
if (!hasProtections) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>Protections</Title>
|
||||
{protectionsDisabledUntil ? (
|
||||
<Text className="info">Unprotected access expires at {protectionsDisabledUntil}.</Text>
|
||||
) : (
|
||||
<Text className="info">Protections are enabled.</Text>
|
||||
)}
|
||||
<Text className="mt-2">
|
||||
Actions like viewing or searching protected notes, exporting decrypted backups, or revoking an active session
|
||||
require additional authentication such as entering your account password or application passcode.
|
||||
</Text>
|
||||
{protectionsDisabledUntil && (
|
||||
<Button className="mt-3" variant="primary" label="End Unprotected Access" onClick={enableProtections} />
|
||||
)}
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
)
|
||||
}
|
||||
|
||||
export default Protections
|
||||
@@ -0,0 +1,31 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { ViewControllerManager } from '@/Services/ViewControllerManager'
|
||||
import { FunctionComponent } from 'react'
|
||||
import TwoFactorAuthWrapper from '../TwoFactorAuth/TwoFactorAuthWrapper'
|
||||
import { MfaProps } from '../TwoFactorAuth/MfaProps'
|
||||
import Encryption from './Encryption'
|
||||
import PasscodeLock from './PasscodeLock'
|
||||
import Privacy from './Privacy'
|
||||
import Protections from './Protections'
|
||||
import ErroredItems from './ErroredItems'
|
||||
import PreferencesPane from '@/Components/Preferences/PreferencesComponents/PreferencesPane'
|
||||
|
||||
interface SecurityProps extends MfaProps {
|
||||
viewControllerManager: ViewControllerManager
|
||||
application: WebApplication
|
||||
}
|
||||
|
||||
const Security: FunctionComponent<SecurityProps> = (props) => (
|
||||
<PreferencesPane>
|
||||
<Encryption viewControllerManager={props.viewControllerManager} />
|
||||
{props.application.items.invalidItems.length > 0 && (
|
||||
<ErroredItems viewControllerManager={props.viewControllerManager} />
|
||||
)}
|
||||
<Protections application={props.application} />
|
||||
<TwoFactorAuthWrapper mfaProvider={props.mfaProvider} userProvider={props.userProvider} />
|
||||
<PasscodeLock viewControllerManager={props.viewControllerManager} application={props.application} />
|
||||
{props.application.getUser() && <Privacy application={props.application} />}
|
||||
</PreferencesPane>
|
||||
)
|
||||
|
||||
export default Security
|
||||
@@ -0,0 +1 @@
|
||||
export const formatCount = (count: number, itemType: string) => `${count} / ${count} ${itemType}`
|
||||
@@ -0,0 +1,5 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
|
||||
export const securityPrefsHasBubble = (application: WebApplication): boolean => {
|
||||
return application.items.invalidItems.length > 0
|
||||
}
|
||||
Reference in New Issue
Block a user