chore: improve preferences folder hierarchies (#1186)
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
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 TwoFactorAuthWrapper from './TwoFactorAuth/TwoFactorAuthWrapper'
|
||||
import { MfaProps } from './TwoFactorAuth/MfaProps'
|
||||
import Encryption from './Encryption'
|
||||
import PasscodeLock from './PasscodeLock'
|
||||
import Privacy from './Privacy'
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
|
||||
import { FunctionComponent, useState, useRef, useEffect, MouseEventHandler } from 'react'
|
||||
import { IconType } from '@standardnotes/snjs'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
icon: IconType
|
||||
onMouseEnter?: MouseEventHandler<HTMLButtonElement>
|
||||
onMouseLeave?: MouseEventHandler<HTMLButtonElement>
|
||||
}
|
||||
|
||||
const DisclosureIconButton: FunctionComponent<Props> = ({ className = '', icon, onMouseEnter, onMouseLeave }) => (
|
||||
<DisclosureButton
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
className={`no-border cursor-pointer bg-transparent p-0 hover:brightness-125 ${className ?? ''}`}
|
||||
>
|
||||
<Icon type={icon} />
|
||||
</DisclosureButton>
|
||||
)
|
||||
|
||||
/**
|
||||
* AuthAppInfoPopup is an info icon that shows a tooltip when clicked
|
||||
* Tooltip is dismissible by clicking outside
|
||||
*
|
||||
* Note: it can be generalized but more use cases are required
|
||||
* @returns
|
||||
*/
|
||||
const AuthAppInfoTooltip: FunctionComponent = () => {
|
||||
const [isClicked, setClicked] = useState(false)
|
||||
const [isHover, setHover] = useState(false)
|
||||
const ref = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
const dismiss = () => setClicked(false)
|
||||
document.addEventListener('mousedown', dismiss)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', dismiss)
|
||||
}
|
||||
}, [ref])
|
||||
|
||||
return (
|
||||
<Disclosure open={isClicked || isHover} onChange={() => setClicked(!isClicked)}>
|
||||
<div className="relative">
|
||||
<DisclosureIconButton
|
||||
icon="info"
|
||||
className="mt-1"
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
/>
|
||||
<DisclosurePanel>
|
||||
<div
|
||||
className={`bg-inverted-default text-inverted-default shadow-overlay w-103 -left-51
|
||||
absolute -top-10 rounded py-1.5 px-2 text-center`}
|
||||
>
|
||||
Some apps, like Google Authenticator, do not back up and restore your secret keys if you lose your device or
|
||||
get a new one.
|
||||
</div>
|
||||
</DisclosurePanel>
|
||||
</div>
|
||||
</Disclosure>
|
||||
)
|
||||
}
|
||||
|
||||
export default AuthAppInfoTooltip
|
||||
@@ -0,0 +1,11 @@
|
||||
import { FunctionComponent } from 'react'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const Bullet: FunctionComponent<Props> = ({ className = '' }) => (
|
||||
<div className={`min-h-1 bg-inverted-default min-w-1 rounded-full ${className} mr-2`} />
|
||||
)
|
||||
|
||||
export default Bullet
|
||||
@@ -0,0 +1,24 @@
|
||||
import { FunctionComponent, useState } from 'react'
|
||||
import IconButton from '@/Components/Button/IconButton'
|
||||
|
||||
type Props = {
|
||||
copyValue: string
|
||||
}
|
||||
|
||||
const CopyButton: FunctionComponent<Props> = ({ copyValue: secretKey }) => {
|
||||
const [isCopied, setCopied] = useState(false)
|
||||
return (
|
||||
<IconButton
|
||||
focusable={false}
|
||||
title="Copy to clipboard"
|
||||
icon={isCopied ? 'check' : 'copy'}
|
||||
className={`${isCopied ? 'success' : undefined} p-0`}
|
||||
onClick={() => {
|
||||
navigator?.clipboard?.writeText(secretKey).catch(console.error)
|
||||
setCopied(() => true)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default CopyButton
|
||||
@@ -0,0 +1,6 @@
|
||||
import { MfaProvider, UserProvider } from '@/Components/Preferences/Providers'
|
||||
|
||||
export interface MfaProps {
|
||||
userProvider: UserProvider
|
||||
mfaProvider: MfaProvider
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import Button from '@/Components/Button/Button'
|
||||
import DecoratedInput from '@/Components/Input/DecoratedInput'
|
||||
import IconButton from '@/Components/Button/IconButton'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'react'
|
||||
import CopyButton from './CopyButton'
|
||||
import Bullet from './Bullet'
|
||||
import { downloadSecretKey } from './download-secret-key'
|
||||
import { TwoFactorActivation } from './TwoFactorActivation'
|
||||
import ModalDialog from '@/Components/Shared/ModalDialog'
|
||||
import ModalDialogButtons from '@/Components/Shared/ModalDialogButtons'
|
||||
import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription'
|
||||
import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel'
|
||||
|
||||
type Props = {
|
||||
activation: TwoFactorActivation
|
||||
}
|
||||
|
||||
const SaveSecretKey: FunctionComponent<Props> = ({ activation: act }) => {
|
||||
const download = (
|
||||
<IconButton
|
||||
focusable={false}
|
||||
title="Download"
|
||||
icon="download"
|
||||
className="p-0"
|
||||
onClick={() => {
|
||||
downloadSecretKey(act.secretKey)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<ModalDialog>
|
||||
<ModalDialogLabel
|
||||
closeDialog={() => {
|
||||
act.cancelActivation()
|
||||
}}
|
||||
>
|
||||
Step 2 of 3 - Save secret key
|
||||
</ModalDialogLabel>
|
||||
<ModalDialogDescription className="h-33 flex flex-row items-center">
|
||||
<div className="flex flex-grow flex-col">
|
||||
<div className="flex flex-row items-center">
|
||||
<Bullet />
|
||||
<div className="min-w-1" />
|
||||
<div className="text-sm">
|
||||
<b>Save your secret key</b>{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://standardnotes.com/help/21/where-should-i-store-my-two-factor-authentication-secret-key"
|
||||
>
|
||||
somewhere safe
|
||||
</a>
|
||||
:
|
||||
</div>
|
||||
<div className="min-w-2" />
|
||||
<DecoratedInput
|
||||
disabled={true}
|
||||
right={[<CopyButton copyValue={act.secretKey} />, download]}
|
||||
value={act.secretKey}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-2" />
|
||||
<div className="flex flex-row items-center">
|
||||
<Bullet />
|
||||
<div className="min-w-1" />
|
||||
<div className="text-sm">
|
||||
You can use this key to generate codes if you lose access to your authenticator app.{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://standardnotes.com/help/22/what-happens-if-i-lose-my-2fa-device-and-my-secret-key"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons>
|
||||
<Button className="min-w-20" label="Back" onClick={() => act.openScanQRCode()} />
|
||||
<Button className="min-w-20" primary label="Next" onClick={() => act.openVerification()} />
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(SaveSecretKey)
|
||||
@@ -0,0 +1,63 @@
|
||||
import { FunctionComponent } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import QRCode from 'qrcode.react'
|
||||
import DecoratedInput from '@/Components/Input/DecoratedInput'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import { TwoFactorActivation } from './TwoFactorActivation'
|
||||
import AuthAppInfoTooltip from './AuthAppInfoPopup'
|
||||
import ModalDialog from '@/Components/Shared/ModalDialog'
|
||||
import ModalDialogButtons from '@/Components/Shared/ModalDialogButtons'
|
||||
import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription'
|
||||
import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel'
|
||||
import CopyButton from './CopyButton'
|
||||
import Bullet from './Bullet'
|
||||
|
||||
type Props = {
|
||||
activation: TwoFactorActivation
|
||||
}
|
||||
|
||||
const ScanQRCode: FunctionComponent<Props> = ({ activation: act }) => {
|
||||
return (
|
||||
<ModalDialog>
|
||||
<ModalDialogLabel closeDialog={act.cancelActivation}>Step 1 of 3 - Scan QR code</ModalDialogLabel>
|
||||
<ModalDialogDescription className="h-33 flex flex-row items-center">
|
||||
<div className="w-25 h-25 flex items-center justify-center bg-info">
|
||||
<QRCode className="border-neutral-contrast-bg border-2 border-solid" value={act.qrCode} size={100} />
|
||||
</div>
|
||||
<div className="min-w-5" />
|
||||
<div className="flex flex-grow flex-col">
|
||||
<div className="flex flex-row items-center">
|
||||
<Bullet />
|
||||
<div className="min-w-1" />
|
||||
<div className="text-sm">
|
||||
Open your <b>authenticator app</b>.
|
||||
</div>
|
||||
<div className="min-w-2" />
|
||||
<AuthAppInfoTooltip />
|
||||
</div>
|
||||
<div className="min-h-2" />
|
||||
<div className="flex flex-row items-center">
|
||||
<Bullet className="mt-2 self-start" />
|
||||
<div className="min-w-1" />
|
||||
<div className="flex-grow text-sm">
|
||||
<b>Scan this QR code</b> or <b>add this secret key</b>:
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-h-2" />
|
||||
<DecoratedInput
|
||||
className={{ container: 'w-92 ml-4' }}
|
||||
disabled={true}
|
||||
value={act.secretKey}
|
||||
right={[<CopyButton copyValue={act.secretKey} />]}
|
||||
/>
|
||||
</div>
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons>
|
||||
<Button className="min-w-20" label="Cancel" onClick={() => act.cancelActivation()} />
|
||||
<Button className="min-w-20" primary label="Next" onClick={() => act.openSaveSecretKey()} />
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(ScanQRCode)
|
||||
@@ -0,0 +1,125 @@
|
||||
import { MfaProvider } from '@/Components/Preferences/Providers'
|
||||
import { action, makeAutoObservable, observable } from 'mobx'
|
||||
|
||||
type ActivationStep = 'scan-qr-code' | 'save-secret-key' | 'verification' | 'success'
|
||||
type VerificationStatus = 'none' | 'invalid-auth-code' | 'invalid-secret' | 'valid'
|
||||
|
||||
export class TwoFactorActivation {
|
||||
public readonly type = 'two-factor-activation' as const
|
||||
|
||||
private _activationStep: ActivationStep
|
||||
|
||||
private _2FAVerification: VerificationStatus = 'none'
|
||||
|
||||
private inputSecretKey = ''
|
||||
private inputOtpToken = ''
|
||||
|
||||
constructor(
|
||||
private mfaProvider: MfaProvider,
|
||||
private readonly email: string,
|
||||
private readonly _secretKey: string,
|
||||
private _cancelActivation: () => void,
|
||||
private _enabled2FA: () => void,
|
||||
) {
|
||||
this._activationStep = 'scan-qr-code'
|
||||
|
||||
makeAutoObservable<
|
||||
TwoFactorActivation,
|
||||
'_secretKey' | '_authCode' | '_step' | '_enable2FAVerification' | 'inputOtpToken' | 'inputSecretKey'
|
||||
>(
|
||||
this,
|
||||
{
|
||||
_secretKey: observable,
|
||||
_authCode: observable,
|
||||
_step: observable,
|
||||
_enable2FAVerification: observable,
|
||||
inputOtpToken: observable,
|
||||
inputSecretKey: observable,
|
||||
},
|
||||
{ autoBind: true },
|
||||
)
|
||||
}
|
||||
|
||||
get secretKey(): string {
|
||||
return this._secretKey
|
||||
}
|
||||
|
||||
get activationStep(): ActivationStep {
|
||||
return this._activationStep
|
||||
}
|
||||
|
||||
get verificationStatus(): VerificationStatus {
|
||||
return this._2FAVerification
|
||||
}
|
||||
|
||||
get qrCode(): string {
|
||||
return `otpauth://totp/2FA?secret=${this._secretKey}&issuer=Standard%20Notes&label=${this.email}`
|
||||
}
|
||||
|
||||
cancelActivation(): void {
|
||||
this._cancelActivation()
|
||||
}
|
||||
|
||||
openScanQRCode(): void {
|
||||
if (this._activationStep === 'save-secret-key') {
|
||||
this._activationStep = 'scan-qr-code'
|
||||
}
|
||||
}
|
||||
|
||||
openSaveSecretKey(): void {
|
||||
const preconditions: ActivationStep[] = ['scan-qr-code', 'verification']
|
||||
if (preconditions.includes(this._activationStep)) {
|
||||
this._activationStep = 'save-secret-key'
|
||||
}
|
||||
}
|
||||
|
||||
openVerification(): void {
|
||||
this.inputOtpToken = ''
|
||||
this.inputSecretKey = ''
|
||||
if (this._activationStep === 'save-secret-key') {
|
||||
this._activationStep = 'verification'
|
||||
this._2FAVerification = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
openSuccess(): void {
|
||||
if (this._activationStep === 'verification') {
|
||||
this._activationStep = 'success'
|
||||
}
|
||||
}
|
||||
|
||||
setInputSecretKey(secretKey: string): void {
|
||||
this.inputSecretKey = secretKey
|
||||
}
|
||||
|
||||
setInputOtpToken(otpToken: string): void {
|
||||
this.inputOtpToken = otpToken
|
||||
}
|
||||
|
||||
enable2FA(): void {
|
||||
if (this.inputSecretKey !== this._secretKey) {
|
||||
this._2FAVerification = 'invalid-secret'
|
||||
return
|
||||
}
|
||||
|
||||
this.mfaProvider
|
||||
.enableMfa(this.inputSecretKey, this.inputOtpToken)
|
||||
.then(
|
||||
action(() => {
|
||||
this._2FAVerification = 'valid'
|
||||
this.openSuccess()
|
||||
}),
|
||||
)
|
||||
.catch(
|
||||
action(() => {
|
||||
this._2FAVerification = 'invalid-auth-code'
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
finishActivation(): void {
|
||||
if (this._activationStep === 'success') {
|
||||
this._enabled2FA()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'react'
|
||||
import { TwoFactorActivation } from './TwoFactorActivation'
|
||||
import SaveSecretKey from './SaveSecretKey'
|
||||
import ScanQRCode from './ScanQRCode'
|
||||
import Verification from './Verification'
|
||||
import TwoFactorSuccess from './TwoFactorSuccess'
|
||||
|
||||
type Props = {
|
||||
activation: TwoFactorActivation
|
||||
}
|
||||
|
||||
const TwoFactorActivationView: FunctionComponent<Props> = ({ activation: act }) => {
|
||||
switch (act.activationStep) {
|
||||
case 'scan-qr-code':
|
||||
return <ScanQRCode activation={act} />
|
||||
case 'save-secret-key':
|
||||
return <SaveSecretKey activation={act} />
|
||||
case 'verification':
|
||||
return <Verification activation={act} />
|
||||
case 'success':
|
||||
return <TwoFactorSuccess activation={act} />
|
||||
}
|
||||
}
|
||||
|
||||
export default observer(TwoFactorActivationView)
|
||||
@@ -0,0 +1,137 @@
|
||||
import { MfaProvider, UserProvider } from '@/Components/Preferences/Providers'
|
||||
import { action, makeAutoObservable, observable } from 'mobx'
|
||||
import { TwoFactorActivation } from './TwoFactorActivation'
|
||||
|
||||
type TwoFactorStatus = 'two-factor-enabled' | TwoFactorActivation | 'two-factor-disabled'
|
||||
|
||||
export const is2FADisabled = (status: TwoFactorStatus): status is 'two-factor-disabled' =>
|
||||
status === 'two-factor-disabled'
|
||||
|
||||
export const is2FAActivation = (status: TwoFactorStatus): status is TwoFactorActivation =>
|
||||
(status as TwoFactorActivation)?.type === 'two-factor-activation'
|
||||
|
||||
export const is2FAEnabled = (status: TwoFactorStatus): status is 'two-factor-enabled' => status === 'two-factor-enabled'
|
||||
|
||||
export class TwoFactorAuth {
|
||||
private _status: TwoFactorStatus | 'fetching' = 'fetching'
|
||||
private _errorMessage: string | null
|
||||
|
||||
constructor(private readonly mfaProvider: MfaProvider, private readonly userProvider: UserProvider) {
|
||||
this._errorMessage = null
|
||||
|
||||
makeAutoObservable<TwoFactorAuth, '_status' | '_errorMessage' | 'deactivateMfa' | 'startActivation'>(
|
||||
this,
|
||||
{
|
||||
_status: observable,
|
||||
_errorMessage: observable,
|
||||
deactivateMfa: action,
|
||||
startActivation: action,
|
||||
},
|
||||
{ autoBind: true },
|
||||
)
|
||||
}
|
||||
|
||||
private startActivation(): void {
|
||||
const setDisabled = action(() => (this._status = 'two-factor-disabled'))
|
||||
const setEnabled = action(() => {
|
||||
this._status = 'two-factor-enabled'
|
||||
this.fetchStatus()
|
||||
})
|
||||
this.mfaProvider
|
||||
.generateMfaSecret()
|
||||
.then(
|
||||
action((secret) => {
|
||||
this._status = new TwoFactorActivation(
|
||||
this.mfaProvider,
|
||||
this.userProvider.getUser()?.email as string,
|
||||
secret,
|
||||
setDisabled,
|
||||
setEnabled,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.catch(
|
||||
action((e) => {
|
||||
this.setError(e.message)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
private deactivate2FA(): void {
|
||||
this.mfaProvider
|
||||
.disableMfa()
|
||||
.then(
|
||||
action(() => {
|
||||
this.fetchStatus()
|
||||
}),
|
||||
)
|
||||
.catch(
|
||||
action((e) => {
|
||||
this.setError(e.message)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
isLoggedIn(): boolean {
|
||||
return this.userProvider.getUser() != undefined
|
||||
}
|
||||
|
||||
fetchStatus(): void {
|
||||
if (!this.isLoggedIn()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.isMfaFeatureAvailable()) {
|
||||
return
|
||||
}
|
||||
|
||||
this.mfaProvider
|
||||
.isMfaActivated()
|
||||
.then(
|
||||
action((active) => {
|
||||
this._status = active ? 'two-factor-enabled' : 'two-factor-disabled'
|
||||
this.setError(null)
|
||||
}),
|
||||
)
|
||||
.catch(
|
||||
action((e) => {
|
||||
this._status = 'two-factor-disabled'
|
||||
this.setError(e.message)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
private setError(errorMessage: string | null): void {
|
||||
this._errorMessage = errorMessage
|
||||
}
|
||||
|
||||
toggle2FA(): void {
|
||||
if (!this.isLoggedIn()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.isMfaFeatureAvailable()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this._status === 'two-factor-disabled') {
|
||||
return this.startActivation()
|
||||
}
|
||||
|
||||
if (this._status === 'two-factor-enabled') {
|
||||
return this.deactivate2FA()
|
||||
}
|
||||
}
|
||||
|
||||
get errorMessage(): string | null {
|
||||
return this._errorMessage
|
||||
}
|
||||
|
||||
get status(): TwoFactorStatus | 'fetching' {
|
||||
return this._status
|
||||
}
|
||||
|
||||
isMfaFeatureAvailable(): boolean {
|
||||
return this.mfaProvider.isMfaFeatureAvailable()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { FunctionComponent } from 'react'
|
||||
import { Text } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { is2FAActivation, TwoFactorAuth } from '../TwoFactorAuth'
|
||||
import TwoFactorActivationView from '../TwoFactorActivationView'
|
||||
import TwoFactorTitle from './TwoFactorTitle'
|
||||
import TwoFactorDescription from './TwoFactorDescription'
|
||||
import TwoFactorSwitch from './TwoFactorSwitch'
|
||||
import PreferencesGroup from '@/Components/Preferences/PreferencesComponents/PreferencesGroup'
|
||||
import PreferencesSegment from '@/Components/Preferences/PreferencesComponents/PreferencesSegment'
|
||||
|
||||
type Props = {
|
||||
auth: TwoFactorAuth
|
||||
}
|
||||
|
||||
const TwoFactorAuthView: FunctionComponent<Props> = ({ auth }) => {
|
||||
return (
|
||||
<>
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex flex-grow flex-col">
|
||||
<TwoFactorTitle auth={auth} />
|
||||
<TwoFactorDescription auth={auth} />
|
||||
</div>
|
||||
<div className="flex min-w-15 flex-col items-center justify-center">
|
||||
<TwoFactorSwitch auth={auth} />
|
||||
</div>
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
|
||||
{auth.errorMessage != null && (
|
||||
<PreferencesSegment>
|
||||
<Text className="text-danger">{auth.errorMessage}</Text>
|
||||
</PreferencesSegment>
|
||||
)}
|
||||
</PreferencesGroup>
|
||||
{auth.status !== 'fetching' && is2FAActivation(auth.status) && (
|
||||
<TwoFactorActivationView activation={auth.status} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(TwoFactorAuthView)
|
||||
@@ -0,0 +1,28 @@
|
||||
import { FunctionComponent } from 'react'
|
||||
import { Text } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { TwoFactorAuth } from '../TwoFactorAuth'
|
||||
|
||||
type Props = {
|
||||
auth: TwoFactorAuth
|
||||
}
|
||||
|
||||
const TwoFactorDescription: FunctionComponent<Props> = ({ auth }) => {
|
||||
if (!auth.isLoggedIn()) {
|
||||
return <Text>Sign in or register for an account to configure 2FA.</Text>
|
||||
}
|
||||
if (!auth.isMfaFeatureAvailable()) {
|
||||
return (
|
||||
<Text>
|
||||
A paid subscription plan is required to enable 2FA.{' '}
|
||||
<a target="_blank" href="https://standardnotes.com/features">
|
||||
Learn more
|
||||
</a>
|
||||
.
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
return <Text>An extra layer of security when logging in to your account.</Text>
|
||||
}
|
||||
|
||||
export default observer(TwoFactorDescription)
|
||||
@@ -0,0 +1,23 @@
|
||||
import { FunctionComponent } from 'react'
|
||||
import Switch from '@/Components/Switch/Switch'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { is2FADisabled, TwoFactorAuth } from '../TwoFactorAuth'
|
||||
import Spinner from '@/Components/Spinner/Spinner'
|
||||
|
||||
type Props = {
|
||||
auth: TwoFactorAuth
|
||||
}
|
||||
|
||||
const TwoFactorSwitch: FunctionComponent<Props> = ({ auth }) => {
|
||||
if (!(auth.isLoggedIn() && auth.isMfaFeatureAvailable())) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (auth.status === 'fetching') {
|
||||
return <Spinner className="h-4 w-4" />
|
||||
}
|
||||
|
||||
return <Switch checked={!is2FADisabled(auth.status)} onChange={auth.toggle2FA} />
|
||||
}
|
||||
|
||||
export default observer(TwoFactorSwitch)
|
||||
@@ -0,0 +1,20 @@
|
||||
import { FunctionComponent } from 'react'
|
||||
import { Title } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { TwoFactorAuth } from '../TwoFactorAuth'
|
||||
|
||||
type Props = {
|
||||
auth: TwoFactorAuth
|
||||
}
|
||||
|
||||
const TwoFactorTitle: FunctionComponent<Props> = ({ auth }) => {
|
||||
if (!auth.isLoggedIn()) {
|
||||
return <Title>Two-factor authentication not available</Title>
|
||||
}
|
||||
if (!auth.isMfaFeatureAvailable()) {
|
||||
return <Title>Two-factor authentication not available</Title>
|
||||
}
|
||||
return <Title>Two-factor authentication</Title>
|
||||
}
|
||||
|
||||
export default observer(TwoFactorTitle)
|
||||
@@ -0,0 +1,12 @@
|
||||
import { FunctionComponent, useState } from 'react'
|
||||
import { MfaProps } from './MfaProps'
|
||||
import { TwoFactorAuth } from './TwoFactorAuth'
|
||||
import TwoFactorAuthView from './TwoFactorAuthView/TwoFactorAuthView'
|
||||
|
||||
const TwoFactorAuthWrapper: FunctionComponent<MfaProps> = (props) => {
|
||||
const [auth] = useState(() => new TwoFactorAuth(props.mfaProvider, props.userProvider))
|
||||
auth.fetchStatus()
|
||||
return <TwoFactorAuthView auth={auth} />
|
||||
}
|
||||
|
||||
export default TwoFactorAuthWrapper
|
||||
@@ -0,0 +1,29 @@
|
||||
import Button from '@/Components/Button/Button'
|
||||
import ModalDialog from '@/Components/Shared/ModalDialog'
|
||||
import ModalDialogButtons from '@/Components/Shared/ModalDialogButtons'
|
||||
import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription'
|
||||
import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel'
|
||||
import { Subtitle } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'react'
|
||||
import { TwoFactorActivation } from './TwoFactorActivation'
|
||||
|
||||
type Props = {
|
||||
activation: TwoFactorActivation
|
||||
}
|
||||
|
||||
const TwoFactorSuccess: FunctionComponent<Props> = ({ activation: act }) => (
|
||||
<ModalDialog>
|
||||
<ModalDialogLabel closeDialog={act.finishActivation}>Successfully Enabled</ModalDialogLabel>
|
||||
<ModalDialogDescription className="flex flex-row items-center">
|
||||
<div className="flex flex-row items-center justify-center pt-2">
|
||||
<Subtitle>Two-factor authentication has been successfully enabled for your account.</Subtitle>
|
||||
</div>
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons>
|
||||
<Button className="min-w-20" primary label="Finish" onClick={act.finishActivation} />
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
)
|
||||
|
||||
export default observer(TwoFactorSuccess)
|
||||
@@ -0,0 +1,58 @@
|
||||
import Button from '@/Components/Button/Button'
|
||||
import DecoratedInput from '@/Components/Input/DecoratedInput'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'react'
|
||||
import Bullet from './Bullet'
|
||||
import { TwoFactorActivation } from './TwoFactorActivation'
|
||||
import ModalDialog from '@/Components/Shared/ModalDialog'
|
||||
import ModalDialogButtons from '@/Components/Shared/ModalDialogButtons'
|
||||
import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription'
|
||||
import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel'
|
||||
|
||||
type Props = {
|
||||
activation: TwoFactorActivation
|
||||
}
|
||||
|
||||
const Verification: FunctionComponent<Props> = ({ activation: act }) => {
|
||||
const secretKeyClass = act.verificationStatus === 'invalid-secret' ? 'border-danger' : ''
|
||||
const authTokenClass = act.verificationStatus === 'invalid-auth-code' ? 'border-danger' : ''
|
||||
return (
|
||||
<ModalDialog>
|
||||
<ModalDialogLabel closeDialog={act.cancelActivation}>Step 3 of 3 - Verification</ModalDialogLabel>
|
||||
<ModalDialogDescription className="h-33 flex flex-row items-center">
|
||||
<div className="flex flex-grow flex-col">
|
||||
<div className="mb-4 flex flex-row items-center">
|
||||
<Bullet />
|
||||
<div className="min-w-1" />
|
||||
<div className="text-sm">
|
||||
Enter your <b>secret key</b>:
|
||||
</div>
|
||||
<div className="min-w-2" />
|
||||
<DecoratedInput className={{ container: `w-92 ${secretKeyClass}` }} onChange={act.setInputSecretKey} />
|
||||
</div>
|
||||
<div className="flex flex-row items-center">
|
||||
<Bullet />
|
||||
<div className="min-w-1" />
|
||||
<div className="text-sm">
|
||||
Verify the <b>authentication code</b> generated by your authenticator app:
|
||||
</div>
|
||||
<div className="min-w-2" />
|
||||
<DecoratedInput className={{ container: `w-30 ${authTokenClass}` }} onChange={act.setInputOtpToken} />
|
||||
</div>
|
||||
</div>
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons>
|
||||
{act.verificationStatus === 'invalid-auth-code' && (
|
||||
<div className="flex-grow text-sm text-danger">Incorrect authentication code, please try again.</div>
|
||||
)}
|
||||
{act.verificationStatus === 'invalid-secret' && (
|
||||
<div className="flex-grow text-sm text-danger">Incorrect secret key, please try again.</div>
|
||||
)}
|
||||
<Button className="min-w-20" label="Back" onClick={act.openSaveSecretKey} />
|
||||
<Button className="min-w-20" primary label="Next" onClick={act.enable2FA} />
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(Verification)
|
||||
@@ -0,0 +1,13 @@
|
||||
// Temporary implementation until integration
|
||||
export function downloadSecretKey(text: string) {
|
||||
const link = document.createElement('a')
|
||||
const blob = new Blob([text], {
|
||||
type: 'text/plain;charset=utf-8',
|
||||
})
|
||||
link.href = window.URL.createObjectURL(blob)
|
||||
link.setAttribute('download', 'standardnotes_2fa_key.txt')
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
link.remove()
|
||||
window.URL.revokeObjectURL(link.href)
|
||||
}
|
||||
Reference in New Issue
Block a user