Merge branch 'develop' into feature/subscription-info-in-preferences
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { PreferencesPane } from '../components';
|
||||
import { TwoFactorAuthWrapper } from './two-factor-auth';
|
||||
import { MfaProps } from './two-factor-auth/MfaProps';
|
||||
|
||||
export const Security: FunctionComponent = () => (
|
||||
interface SecurityProps extends MfaProps {}
|
||||
|
||||
export const Security: FunctionComponent<SecurityProps> = (props) => (
|
||||
<PreferencesPane>
|
||||
<TwoFactorAuthWrapper />
|
||||
<TwoFactorAuthWrapper mfaGateway={props.mfaGateway} />
|
||||
</PreferencesPane>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
export interface MfaGateway {
|
||||
getUser(): { uuid: string; email: string } | undefined;
|
||||
|
||||
isMfaActivated(): Promise<boolean>;
|
||||
|
||||
generateMfaSecret(): Promise<string>;
|
||||
|
||||
getOtpToken(secret: string): Promise<string>;
|
||||
|
||||
enableMfa(secret: string, otpToken: string): Promise<void>;
|
||||
|
||||
disableMfa(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface MfaProps {
|
||||
mfaGateway: MfaGateway;
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { IconButton } from '@/components/IconButton';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { downloadSecretKey } from './download-secret-key';
|
||||
import { TwoFactorActivation } from './model';
|
||||
import { TwoFactorActivation } from './TwoFactorActivation';
|
||||
import {
|
||||
TwoFactorDialog,
|
||||
TwoFactorDialogLabel,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import QRCode from 'qrcode.react';
|
||||
|
||||
import { DecoratedInput } from '../../../components/DecoratedInput';
|
||||
import { IconButton } from '../../../components/IconButton';
|
||||
import { Button } from '@/components/Button';
|
||||
import { TwoFactorActivation } from './model';
|
||||
import { TwoFactorActivation } from './TwoFactorActivation';
|
||||
import {
|
||||
TwoFactorDialog,
|
||||
TwoFactorDialogLabel,
|
||||
@@ -35,7 +38,7 @@ export const ScanQRCode: FunctionComponent<{
|
||||
<TwoFactorDialogDescription>
|
||||
<div className="flex flex-row gap-3 items-center">
|
||||
<div className="w-25 h-25 flex items-center justify-center bg-info">
|
||||
QR code
|
||||
<QRCode value={act.qrCode} size={100} />
|
||||
</div>
|
||||
<div className="flex-grow flex flex-col gap-2">
|
||||
<div className="flex flex-row gap-1 items-center">
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { action, makeAutoObservable, observable, untracked } from 'mobx';
|
||||
import { MfaGateway } from './MfaProps';
|
||||
|
||||
type ActivationStep = 'scan-qr-code' | 'save-secret-key' | 'verification';
|
||||
type VerificationStatus = 'none' | 'invalid' | '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 mfaGateway: MfaGateway,
|
||||
private readonly _secretKey: string,
|
||||
private _cancelActivation: () => void,
|
||||
private _enabled2FA: () => void
|
||||
) {
|
||||
this._activationStep = 'scan-qr-code';
|
||||
|
||||
makeAutoObservable<
|
||||
TwoFactorActivation,
|
||||
| '_secretKey'
|
||||
| '_authCode'
|
||||
| '_step'
|
||||
| '_enable2FAVerification'
|
||||
| 'refreshOtp'
|
||||
| 'inputOtpToken'
|
||||
| 'inputSecretKey'
|
||||
>(
|
||||
this,
|
||||
{
|
||||
_secretKey: observable,
|
||||
_authCode: observable,
|
||||
_step: observable,
|
||||
_enable2FAVerification: observable,
|
||||
refreshOtp: action,
|
||||
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 {
|
||||
const email = this.mfaGateway.getUser()!.email;
|
||||
return `otpauth://totp/2FA?secret=${this._secretKey}&issuer=Standard%20Notes&label=${email}`;
|
||||
}
|
||||
|
||||
cancelActivation(): void {
|
||||
this._cancelActivation();
|
||||
}
|
||||
|
||||
openScanQRCode(): void {
|
||||
const preconditions: ActivationStep[] = ['save-secret-key'];
|
||||
if (preconditions.includes(this._activationStep)) {
|
||||
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 = '';
|
||||
const preconditions: ActivationStep[] = ['save-secret-key'];
|
||||
if (preconditions.includes(this._activationStep)) {
|
||||
this._activationStep = 'verification';
|
||||
this._2FAVerification = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
setInputSecretKey(secretKey: string): void {
|
||||
this.inputSecretKey = secretKey;
|
||||
}
|
||||
|
||||
setInputOtpToken(otpToken: string): void {
|
||||
this.inputOtpToken = otpToken;
|
||||
}
|
||||
|
||||
enable2FA(): void {
|
||||
if (this.inputSecretKey === this._secretKey) {
|
||||
this.mfaGateway
|
||||
.enableMfa(this.inputSecretKey, this.inputOtpToken)
|
||||
.then(
|
||||
action(() => {
|
||||
this._2FAVerification = 'valid';
|
||||
this._enabled2FA();
|
||||
})
|
||||
)
|
||||
.catch(
|
||||
action(() => {
|
||||
this._2FAVerification = 'invalid';
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this._2FAVerification = 'invalid';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { TwoFactorActivation } from './model';
|
||||
import { TwoFactorActivation } from './TwoFactorActivation';
|
||||
import { SaveSecretKey } from './SaveSecretKey';
|
||||
import { ScanQRCode } from './ScanQRCode';
|
||||
import { Verification } from './Verification';
|
||||
@@ -9,10 +9,12 @@ export const TwoFactorActivationView: FunctionComponent<{
|
||||
activation: TwoFactorActivation;
|
||||
}> = observer(({ activation: act }) => (
|
||||
<>
|
||||
{act.step === 'scan-qr-code' && <ScanQRCode activation={act} />}
|
||||
{act.activationStep === 'scan-qr-code' && <ScanQRCode activation={act} />}
|
||||
|
||||
{act.step === 'save-secret-key' && <SaveSecretKey activation={act} />}
|
||||
{act.activationStep === 'save-secret-key' && (
|
||||
<SaveSecretKey activation={act} />
|
||||
)}
|
||||
|
||||
{act.step === 'verification' && <Verification activation={act} />}
|
||||
{act.activationStep === 'verification' && <Verification activation={act} />}
|
||||
</>
|
||||
));
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { action, makeAutoObservable, observable } from 'mobx';
|
||||
import { MfaGateway } from './MfaProps';
|
||||
import { TwoFactorActivation } from './TwoFactorActivation';
|
||||
|
||||
type TwoFactorStatus =
|
||||
| 'two-factor-enabled'
|
||||
| TwoFactorActivation
|
||||
| 'two-factor-disabled';
|
||||
|
||||
export const is2FADisabled = (s: TwoFactorStatus): s is 'two-factor-disabled' =>
|
||||
s === 'two-factor-disabled';
|
||||
|
||||
export const is2FAActivation = (s: TwoFactorStatus): s is TwoFactorActivation =>
|
||||
(s as any).type === 'two-factor-activation';
|
||||
|
||||
export const is2FAEnabled = (s: TwoFactorStatus): s is 'two-factor-enabled' =>
|
||||
s === 'two-factor-enabled';
|
||||
|
||||
export class TwoFactorAuth {
|
||||
private _status: TwoFactorStatus | 'fetching' = 'fetching';
|
||||
private _errorMessage: string | null;
|
||||
|
||||
constructor(private readonly mfaGateway: MfaGateway) {
|
||||
this._errorMessage = null;
|
||||
|
||||
makeAutoObservable<
|
||||
TwoFactorAuth,
|
||||
'_status' | '_errorMessage' | 'deactivateMfa' | 'startActivation'
|
||||
>(this, {
|
||||
_status: observable,
|
||||
_errorMessage: observable,
|
||||
deactivateMfa: action,
|
||||
startActivation: action,
|
||||
});
|
||||
}
|
||||
|
||||
private startActivation(): void {
|
||||
const setDisabled = action(() => (this._status = 'two-factor-disabled'));
|
||||
const setEnabled = action(() => (this._status = 'two-factor-enabled'));
|
||||
this.mfaGateway
|
||||
.generateMfaSecret()
|
||||
.then(
|
||||
action((secret) => {
|
||||
this._status = new TwoFactorActivation(
|
||||
this.mfaGateway,
|
||||
secret,
|
||||
setDisabled,
|
||||
setEnabled
|
||||
);
|
||||
})
|
||||
)
|
||||
.catch(
|
||||
action((e) => {
|
||||
this.setError(e.message);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private deactivate2FA(): void {
|
||||
this.mfaGateway
|
||||
.disableMfa()
|
||||
.then(
|
||||
action(() => {
|
||||
this._status = 'two-factor-disabled';
|
||||
})
|
||||
)
|
||||
.catch(
|
||||
action((e) => {
|
||||
this.setError(e.message);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private get isLoggedIn(): boolean {
|
||||
return this.mfaGateway.getUser() != undefined;
|
||||
}
|
||||
|
||||
fetchStatus(): void {
|
||||
this._status = 'fetching';
|
||||
|
||||
if (!this.isLoggedIn) {
|
||||
this.setError('To enable 2FA, sign in or register for an account.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.mfaGateway
|
||||
.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);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
setError(errorMessage: string | null): void {
|
||||
this._errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
toggle2FA(): void {
|
||||
if (!this.isLoggedIn) {
|
||||
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 {
|
||||
if (this._status === 'fetching') {
|
||||
return 'two-factor-disabled';
|
||||
}
|
||||
return this._status;
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,8 @@ import {
|
||||
is2FADisabled,
|
||||
is2FAEnabled,
|
||||
TwoFactorAuth,
|
||||
} from './model';
|
||||
} from './TwoFactorAuth';
|
||||
import { TwoFactorDisabledView } from './TwoFactorDisabledView';
|
||||
import { TwoFactorEnabledView } from './TwoFactorEnabledView';
|
||||
import { TwoFactorActivationView } from './TwoFactorActivationView';
|
||||
|
||||
export const TwoFactorAuthView: FunctionComponent<{
|
||||
@@ -28,6 +27,9 @@ export const TwoFactorAuthView: FunctionComponent<{
|
||||
<Text>
|
||||
An extra layer of security when logging in to your account.
|
||||
</Text>
|
||||
{auth.errorMessage != null && (
|
||||
<Text className="color-danger">{auth.errorMessage}</Text>
|
||||
)}
|
||||
</div>
|
||||
<Switch
|
||||
checked={!is2FADisabled(auth.status)}
|
||||
@@ -35,19 +37,15 @@ export const TwoFactorAuthView: FunctionComponent<{
|
||||
/>
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
<PreferencesSegment>
|
||||
{is2FAEnabled(auth.status) && (
|
||||
<TwoFactorEnabledView
|
||||
secretKey={auth.status.secretKey}
|
||||
authCode={auth.status.authCode}
|
||||
/>
|
||||
)}
|
||||
|
||||
{is2FAActivation(auth.status) && (
|
||||
<TwoFactorActivationView activation={auth.status} />
|
||||
)}
|
||||
{is2FAActivation(auth.status) ? (
|
||||
<TwoFactorActivationView activation={auth.status} />
|
||||
) : null}
|
||||
|
||||
{!is2FAEnabled(auth.status) && <TwoFactorDisabledView />}
|
||||
</PreferencesSegment>
|
||||
{!is2FAEnabled(auth.status) ? (
|
||||
<PreferencesSegment>
|
||||
<TwoFactorDisabledView />
|
||||
</PreferencesSegment>
|
||||
) : null}
|
||||
</PreferencesGroup>
|
||||
));
|
||||
|
||||
@@ -19,8 +19,8 @@ export const TwoFactorDialog: FunctionComponent<{
|
||||
return (
|
||||
<AlertDialog leastDestructiveRef={ldRef}>
|
||||
{/* sn-component is focusable by default, but doesn't stretch to child width
|
||||
resulting in a badly focused dialog. Utility classes are not available
|
||||
at the sn-component level, only below it. tabIndex -1 disables focus
|
||||
resulting in a badly focused dialog. Utility classes are not available
|
||||
at the sn-component level, only below it. tabIndex -1 disables focus
|
||||
and enables it on the child component */}
|
||||
<div tabIndex={-1} className="sn-component">
|
||||
<div
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { CircleProgressTime } from '@/components/CircleProgressTime';
|
||||
import { DecoratedInput } from '@/components/DecoratedInput';
|
||||
import { IconButton } from '@/components/IconButton';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { downloadSecretKey } from './download-secret-key';
|
||||
import { Text } from '../../components';
|
||||
|
||||
export const TwoFactorEnabledView: FunctionComponent<{
|
||||
secretKey: string;
|
||||
authCode: string;
|
||||
}> = ({ secretKey, authCode }) => {
|
||||
const download = (
|
||||
<IconButton
|
||||
icon="download"
|
||||
onClick={() => {
|
||||
downloadSecretKey(secretKey);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
const copy = (
|
||||
<IconButton
|
||||
icon="copy"
|
||||
onClick={() => {
|
||||
navigator?.clipboard?.writeText(secretKey);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
const progress = <CircleProgressTime time={30000} />;
|
||||
return (
|
||||
<div className="flex flex-row gap-4">
|
||||
<div className="flex-grow flex flex-col">
|
||||
<Text>Secret Key</Text>
|
||||
<DecoratedInput
|
||||
disabled={true}
|
||||
right={[copy, download]}
|
||||
text={secretKey}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-30 flex flex-col">
|
||||
<Text>Authentication Code</Text>
|
||||
<DecoratedInput disabled={true} text={authCode} right={[progress]} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import { Button } from '@/components/Button';
|
||||
import { DecoratedInput } from '@/components/DecoratedInput';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { TwoFactorActivation } from './model';
|
||||
import { TwoFactorActivation } from './TwoFactorActivation';
|
||||
import {
|
||||
TwoFactorDialog,
|
||||
TwoFactorDialogLabel,
|
||||
@@ -17,11 +17,7 @@ export const Verification: FunctionComponent<{
|
||||
act.verificationStatus === 'invalid' ? 'border-dark-red' : '';
|
||||
return (
|
||||
<TwoFactorDialog>
|
||||
<TwoFactorDialogLabel
|
||||
closeDialog={() => {
|
||||
act.cancelActivation();
|
||||
}}
|
||||
>
|
||||
<TwoFactorDialogLabel closeDialog={act.cancelActivation}>
|
||||
Step 3 of 3 - Verification
|
||||
</TwoFactorDialogLabel>
|
||||
<TwoFactorDialogDescription>
|
||||
@@ -30,20 +26,26 @@ export const Verification: FunctionComponent<{
|
||||
<div className="text-sm">
|
||||
・Enter your <b>secret key</b>:
|
||||
</div>
|
||||
<DecoratedInput className={borderInv} />
|
||||
<DecoratedInput
|
||||
className={borderInv}
|
||||
onChange={act.setInputSecretKey}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<div className="text-sm">
|
||||
・Verify the <b>authentication code</b> generated by your
|
||||
authenticator app:
|
||||
</div>
|
||||
<DecoratedInput className={`w-30 ${borderInv}`} />
|
||||
<DecoratedInput
|
||||
className={`w-30 ${borderInv}`}
|
||||
onChange={act.setInputOtpToken}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TwoFactorDialogDescription>
|
||||
<TwoFactorDialogButtons>
|
||||
{act.verificationStatus === 'invalid' && (
|
||||
<div className="text-sm color-dark-red">
|
||||
<div className="text-sm color-danger">
|
||||
Incorrect credentials, please try again.
|
||||
</div>
|
||||
)}
|
||||
@@ -51,13 +53,13 @@ export const Verification: FunctionComponent<{
|
||||
className="min-w-20"
|
||||
type="normal"
|
||||
label="Back"
|
||||
onClick={() => act.openSaveSecretKey()}
|
||||
onClick={act.openSaveSecretKey}
|
||||
/>
|
||||
<Button
|
||||
className="min-w-20"
|
||||
type="primary"
|
||||
label="Next"
|
||||
onClick={() => act.enable2FA('X', 'X')}
|
||||
onClick={act.enable2FA}
|
||||
/>
|
||||
</TwoFactorDialogButtons>
|
||||
</TwoFactorDialog>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
import { TwoFactorAuth } from './model';
|
||||
import { MfaProps } from './MfaProps';
|
||||
import { TwoFactorAuth } from './TwoFactorAuth';
|
||||
import { TwoFactorAuthView } from './TwoFactorAuthView';
|
||||
|
||||
export const TwoFactorAuthWrapper: FunctionComponent = () => {
|
||||
const [auth] = useState(() => new TwoFactorAuth());
|
||||
export const TwoFactorAuthWrapper: FunctionComponent<MfaProps> = ({
|
||||
mfaGateway,
|
||||
}) => {
|
||||
const [auth] = useState(() => new TwoFactorAuth(mfaGateway));
|
||||
auth.fetchStatus();
|
||||
return <TwoFactorAuthView auth={auth} />;
|
||||
};
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
import { action, makeAutoObservable, observable, untracked } from 'mobx';
|
||||
|
||||
function getNewAuthCode() {
|
||||
const MIN = 100000;
|
||||
const MAX = 999999;
|
||||
const code = Math.floor(Math.random() * (MAX - MIN) + MIN);
|
||||
return code.toString();
|
||||
}
|
||||
|
||||
const activationSteps = [
|
||||
'scan-qr-code',
|
||||
'save-secret-key',
|
||||
'verification',
|
||||
] as const;
|
||||
|
||||
type ActivationStep = typeof activationSteps[number];
|
||||
|
||||
export class TwoFactorActivation {
|
||||
public readonly type = 'two-factor-activation' as const;
|
||||
|
||||
private _step: ActivationStep;
|
||||
|
||||
private _secretKey: string;
|
||||
private _authCode: string;
|
||||
private _2FAVerification: 'none' | 'invalid' | 'valid' = 'none';
|
||||
|
||||
constructor(
|
||||
private _cancelActivation: () => void,
|
||||
private _enable2FA: (secretKey: string) => void
|
||||
) {
|
||||
this._secretKey = 'FHJJSAJKDASKW43KJS';
|
||||
this._authCode = getNewAuthCode();
|
||||
this._step = 'scan-qr-code';
|
||||
|
||||
makeAutoObservable<
|
||||
TwoFactorActivation,
|
||||
'_secretKey' | '_authCode' | '_step' | '_enable2FAVerification'
|
||||
>(
|
||||
this,
|
||||
{
|
||||
_secretKey: observable,
|
||||
_authCode: observable,
|
||||
_step: observable,
|
||||
_enable2FAVerification: observable,
|
||||
},
|
||||
{ autoBind: true }
|
||||
);
|
||||
}
|
||||
|
||||
get secretKey() {
|
||||
return this._secretKey;
|
||||
}
|
||||
|
||||
get authCode() {
|
||||
return this._authCode;
|
||||
}
|
||||
|
||||
get step() {
|
||||
return this._step;
|
||||
}
|
||||
|
||||
get verificationStatus() {
|
||||
return this._2FAVerification;
|
||||
}
|
||||
|
||||
cancelActivation() {
|
||||
this._cancelActivation();
|
||||
}
|
||||
|
||||
openScanQRCode() {
|
||||
this._step = 'scan-qr-code';
|
||||
}
|
||||
|
||||
openSaveSecretKey() {
|
||||
this._step = 'save-secret-key';
|
||||
}
|
||||
|
||||
openVerification() {
|
||||
this._step = 'verification';
|
||||
this._2FAVerification = 'none';
|
||||
}
|
||||
|
||||
enable2FA(secretKey: string, authCode: string) {
|
||||
if (secretKey === this._secretKey && authCode === this._authCode) {
|
||||
this._2FAVerification = 'valid';
|
||||
this._enable2FA(secretKey);
|
||||
return;
|
||||
}
|
||||
|
||||
// Change to invalid upon implementation
|
||||
this._2FAVerification = 'valid';
|
||||
// Remove after implementation
|
||||
this._enable2FA(secretKey);
|
||||
}
|
||||
}
|
||||
|
||||
export class TwoFactorEnabled {
|
||||
public readonly type = 'two-factor-enabled' as const;
|
||||
private _secretKey: string;
|
||||
private _authCode: string;
|
||||
|
||||
constructor(secretKey: string) {
|
||||
this._secretKey = secretKey;
|
||||
this._authCode = getNewAuthCode();
|
||||
|
||||
makeAutoObservable<TwoFactorEnabled, '_secretKey' | '_authCode'>(this, {
|
||||
_secretKey: observable,
|
||||
_authCode: observable,
|
||||
});
|
||||
}
|
||||
|
||||
get secretKey() {
|
||||
return this._secretKey;
|
||||
}
|
||||
|
||||
get authCode() {
|
||||
return this._authCode;
|
||||
}
|
||||
|
||||
refreshAuthCode() {
|
||||
this._authCode = getNewAuthCode();
|
||||
}
|
||||
}
|
||||
|
||||
type TwoFactorStatus =
|
||||
| TwoFactorEnabled
|
||||
| TwoFactorActivation
|
||||
| 'two-factor-disabled';
|
||||
|
||||
export const is2FADisabled = (s: TwoFactorStatus): s is 'two-factor-disabled' =>
|
||||
s === 'two-factor-disabled';
|
||||
|
||||
export const is2FAActivation = (s: TwoFactorStatus): s is TwoFactorActivation =>
|
||||
(s as any).type === 'two-factor-activation';
|
||||
|
||||
export const is2FAEnabled = (s: TwoFactorStatus): s is TwoFactorEnabled =>
|
||||
(s as any).type === 'two-factor-enabled';
|
||||
|
||||
export class TwoFactorAuth {
|
||||
private _status: TwoFactorStatus = 'two-factor-disabled';
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable<TwoFactorAuth, '_status'>(this, {
|
||||
_status: observable,
|
||||
});
|
||||
}
|
||||
|
||||
private startActivation() {
|
||||
const cancel = action(() => (this._status = 'two-factor-disabled'));
|
||||
const enable = action(
|
||||
(secretKey: string) => (this._status = new TwoFactorEnabled(secretKey))
|
||||
);
|
||||
this._status = new TwoFactorActivation(cancel, enable);
|
||||
}
|
||||
|
||||
private deactivate2FA() {
|
||||
this._status = 'two-factor-disabled';
|
||||
}
|
||||
|
||||
toggle2FA() {
|
||||
if (this._status === 'two-factor-disabled') this.startActivation();
|
||||
else this.deactivate2FA();
|
||||
}
|
||||
|
||||
get status() {
|
||||
return this._status;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user