refactor: format and lint codebase (#971)

This commit is contained in:
Aman Harwara
2022-04-13 22:02:34 +05:30
committed by GitHub
parent dc9c1ea0fc
commit 8e467f9e6d
367 changed files with 13778 additions and 16093 deletions

View File

@@ -0,0 +1,66 @@
import { Icon } from '@/Components/Icon'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
import { FunctionComponent } from 'preact'
import { MouseEventHandler } from 'react'
import { useState, useRef, useEffect } from 'preact/hooks'
import { IconType } from '@standardnotes/snjs'
const DisclosureIconButton: FunctionComponent<{
className?: string
icon: IconType
onMouseEnter?: MouseEventHandler
onMouseLeave?: MouseEventHandler
}> = ({ className = '', icon, onMouseEnter, onMouseLeave }) => (
<DisclosureButton
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
className={`no-border cursor-pointer bg-transparent hover:brightness-130 p-0 ${
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
*/
export 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 color-inverted-default text-center rounded shadow-overlay
py-1.5 px-2 absolute w-103 -top-10 -left-51`}
>
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>
)
}

View File

@@ -0,0 +1,5 @@
import { FunctionComponent } from 'preact'
export const Bullet: FunctionComponent<{ className?: string }> = ({ className = '' }) => (
<div className={`min-w-1 min-h-1 rounded-full bg-inverted-default ${className} mr-2`} />
)

View File

@@ -0,0 +1,21 @@
import { FunctionComponent } from 'preact'
import { IconButton } from '@/Components/Button/IconButton'
import { useState } from 'preact/hooks'
export const CopyButton: FunctionComponent<{ copyValue: string }> = ({ copyValue: secretKey }) => {
const [isCopied, setCopied] = useState(false)
return (
<IconButton
focusable={false}
title="Copy to clipboard"
icon={isCopied ? 'check' : 'copy'}
className={isCopied ? 'success' : undefined}
onClick={() => {
navigator?.clipboard?.writeText(secretKey).catch(console.error)
setCopied(() => true)
}}
/>
)
}

View File

@@ -0,0 +1,6 @@
import { MfaProvider, UserProvider } from '@/Components/Preferences/Providers'
export interface MfaProps {
userProvider: UserProvider
mfaProvider: MfaProvider
}

View File

@@ -0,0 +1,93 @@
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 'preact'
import { CopyButton } from './CopyButton'
import { Bullet } from './Bullet'
import { downloadSecretKey } from './download-secret-key'
import { TwoFactorActivation } from './TwoFactorActivation'
import {
ModalDialog,
ModalDialogButtons,
ModalDialogDescription,
ModalDialogLabel,
} from '@/Components/Shared/ModalDialog'
export const SaveSecretKey: FunctionComponent<{
activation: TwoFactorActivation
}> = observer(({ activation: act }) => {
const download = (
<IconButton
focusable={false}
title="Download"
icon="download"
onClick={() => {
downloadSecretKey(act.secretKey)
}}
/>
)
return (
<ModalDialog>
<ModalDialogLabel
closeDialog={() => {
act.cancelActivation()
}}
>
Step 2 of 3 - Save secret key
</ModalDialogLabel>
<ModalDialogDescription className="h-33">
<div className="flex-grow flex 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"
variant="normal"
label="Back"
onClick={() => act.openScanQRCode()}
/>
<Button
className="min-w-20"
variant="primary"
label="Next"
onClick={() => act.openVerification()}
/>
</ModalDialogButtons>
</ModalDialog>
)
})

View File

@@ -0,0 +1,77 @@
import { FunctionComponent } from 'preact'
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,
ModalDialogButtons,
ModalDialogDescription,
ModalDialogLabel,
} from '@/Components/Shared/ModalDialog'
import { CopyButton } from './CopyButton'
import { Bullet } from './Bullet'
export const ScanQRCode: FunctionComponent<{
activation: TwoFactorActivation
}> = observer(({ activation: act }) => {
return (
<ModalDialog>
<ModalDialogLabel closeDialog={act.cancelActivation}>
Step 1 of 3 - Scan QR code
</ModalDialogLabel>
<ModalDialogDescription className="h-33">
<div className="w-25 h-25 flex items-center justify-center bg-info">
<QRCode
className="border-neutral-contrast-bg border-solid border-2"
value={act.qrCode}
size={100}
/>
</div>
<div className="min-w-5" />
<div className="flex-grow flex 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="self-start mt-2" />
<div className="min-w-1" />
<div className="text-sm flex-grow">
<b>Scan this QR code</b> or <b>add this secret key</b>:
</div>
</div>
<div className="min-h-2" />
<DecoratedInput
className="ml-4 w-92"
disabled={true}
value={act.secretKey}
right={[<CopyButton copyValue={act.secretKey} />]}
/>
</div>
</ModalDialogDescription>
<ModalDialogButtons>
<Button
className="min-w-20"
variant="normal"
label="Cancel"
onClick={() => act.cancelActivation()}
/>
<Button
className="min-w-20"
variant="primary"
label="Next"
onClick={() => act.openSaveSecretKey()}
/>
</ModalDialogButtons>
</ModalDialog>
)
})

View File

@@ -0,0 +1,130 @@
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()
}
}
}

View File

@@ -0,0 +1,22 @@
import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'preact'
import { TwoFactorActivation } from './TwoFactorActivation'
import { SaveSecretKey } from './SaveSecretKey'
import { ScanQRCode } from './ScanQRCode'
import { Verification } from './Verification'
import { TwoFactorSuccess } from './TwoFactorSuccess'
export const TwoFactorActivationView: FunctionComponent<{
activation: TwoFactorActivation
}> = observer(({ 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} />
}
})

View File

@@ -0,0 +1,144 @@
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()
}
}

View File

@@ -0,0 +1,77 @@
import { FunctionComponent } from 'preact'
import { Title, Text, PreferencesGroup, PreferencesSegment } from '@/Components/Preferences/PreferencesComponents'
import { Switch } from '@/Components/Switch'
import { observer } from 'mobx-react-lite'
import { is2FAActivation, is2FADisabled, TwoFactorAuth } from './TwoFactorAuth'
import { TwoFactorActivationView } from './TwoFactorActivationView'
const TwoFactorTitle: FunctionComponent<{ auth: TwoFactorAuth }> = observer(({ 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>
})
const TwoFactorDescription: FunctionComponent<{ auth: TwoFactorAuth }> = observer(({ 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>
})
const TwoFactorSwitch: FunctionComponent<{ auth: TwoFactorAuth }> = observer(({ auth }) => {
if (!(auth.isLoggedIn() && auth.isMfaFeatureAvailable())) {
return null
}
if (auth.status === 'fetching') {
return <div class="sk-spinner normal info" />
}
return <Switch checked={!is2FADisabled(auth.status)} onChange={auth.toggle2FA} />
})
export const TwoFactorAuthView: FunctionComponent<{
auth: TwoFactorAuth
}> = observer(({ auth }) => {
return (
<>
<PreferencesGroup>
<PreferencesSegment>
<div className="flex flex-row items-center">
<div className="flex-grow flex flex-col">
<TwoFactorTitle auth={auth} />
<TwoFactorDescription auth={auth} />
</div>
<div className="flex flex-col justify-center items-center min-w-15">
<TwoFactorSwitch auth={auth} />
</div>
</div>
</PreferencesSegment>
{auth.errorMessage != null && (
<PreferencesSegment>
<Text className="color-danger">{auth.errorMessage}</Text>
</PreferencesSegment>
)}
</PreferencesGroup>
{auth.status !== 'fetching' && is2FAActivation(auth.status) && (
<TwoFactorActivationView activation={auth.status} />
)}
</>
)
})

View File

@@ -0,0 +1,33 @@
import { Button } from '@/Components/Button/Button'
import ModalDialog, {
ModalDialogButtons,
ModalDialogDescription,
ModalDialogLabel,
} from '@/Components/Shared/ModalDialog'
import { Subtitle } from '@/Components/Preferences/PreferencesComponents'
import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'preact'
import { TwoFactorActivation } from './TwoFactorActivation'
export const TwoFactorSuccess: FunctionComponent<{
activation: TwoFactorActivation
}> = observer(({ activation: act }) => (
<ModalDialog>
<ModalDialogLabel closeDialog={act.finishActivation}>Successfully Enabled</ModalDialogLabel>
<ModalDialogDescription>
<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"
variant="primary"
label="Finish"
onClick={act.finishActivation}
/>
</ModalDialogButtons>
</ModalDialog>
))

View File

@@ -0,0 +1,67 @@
import { Button } from '@/Components/Button/Button'
import { DecoratedInput } from '@/Components/Input/DecoratedInput'
import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'preact'
import { Bullet } from './Bullet'
import { TwoFactorActivation } from './TwoFactorActivation'
import {
ModalDialog,
ModalDialogButtons,
ModalDialogDescription,
ModalDialogLabel,
} from '@/Components/Shared/ModalDialog'
export const Verification: FunctionComponent<{
activation: TwoFactorActivation
}> = observer(({ 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">
<div className="flex-grow flex flex-col">
<div className="flex flex-row items-center mb-4">
<Bullet />
<div className="min-w-1" />
<div className="text-sm">
Enter your <b>secret key</b>:
</div>
<div className="min-w-2" />
<DecoratedInput className={`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={`w-30 ${authTokenClass}`} onChange={act.setInputOtpToken} />
</div>
</div>
</ModalDialogDescription>
<ModalDialogButtons>
{act.verificationStatus === 'invalid-auth-code' && (
<div className="text-sm color-danger flex-grow">
Incorrect authentication code, please try again.
</div>
)}
{act.verificationStatus === 'invalid-secret' && (
<div className="text-sm color-danger flex-grow">
Incorrect secret key, please try again.
</div>
)}
<Button
className="min-w-20"
variant="normal"
label="Back"
onClick={act.openSaveSecretKey}
/>
<Button className="min-w-20" variant="primary" label="Next" onClick={act.enable2FA} />
</ModalDialogButtons>
</ModalDialog>
)
})

View File

@@ -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)
}

View File

@@ -0,0 +1,11 @@
import { FunctionComponent } from 'preact'
import { useState } from 'preact/hooks'
import { MfaProps } from './MfaProps'
import { TwoFactorAuth } from './TwoFactorAuth'
import { TwoFactorAuthView } from './TwoFactorAuthView'
export const TwoFactorAuthWrapper: FunctionComponent<MfaProps> = (props) => {
const [auth] = useState(() => new TwoFactorAuth(props.mfaProvider, props.userProvider))
auth.fetchStatus()
return <TwoFactorAuthView auth={auth} />
}