admin Settings & reset MFA
This commit is contained in:
parent
3f7c90a381
commit
35661a0672
|
@ -20,6 +20,7 @@ import Contributions from 'components/Contributions';
|
|||
import ContributionForm from 'components/ContributionForm';
|
||||
import ContributionDetail from 'components/ContributionDetail';
|
||||
import Moderation from 'components/Moderation';
|
||||
import Settings from 'components/Settings';
|
||||
|
||||
import 'styles/style.less';
|
||||
|
||||
|
@ -55,6 +56,8 @@ class Routes extends React.Component<Props> {
|
|||
<Route path="/contributions" component={Contributions} />
|
||||
<Route path="/emails/:type?" component={Emails} />
|
||||
<Route path="/moderation" component={Moderation} />
|
||||
<Route path="/settings/2fa-reset" render={() => <MFAuth isReset={true} />} />
|
||||
<Route path="/settings" component={Settings} />
|
||||
</Switch>
|
||||
)}
|
||||
</Template>
|
||||
|
|
|
@ -4,6 +4,14 @@
|
|||
|
||||
h1 {
|
||||
margin-top: 1rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
& .ant-alert {
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
&-codes,
|
||||
|
@ -48,7 +56,7 @@
|
|||
margin-top: 1rem;
|
||||
text-align: right;
|
||||
|
||||
& > button + button {
|
||||
& > * + * {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
@ -61,10 +69,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
& > div {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { ReactNode } from 'react';
|
||||
import { Link, Redirect } from 'react-router-dom';
|
||||
import { view } from 'react-easy-state';
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
import QRCode from 'qrcode.react';
|
||||
|
@ -14,6 +15,10 @@ import store, {
|
|||
import { downloadString } from 'src/util/file';
|
||||
import './index.less';
|
||||
|
||||
interface Props {
|
||||
isReset?: boolean;
|
||||
}
|
||||
|
||||
const STATE = {
|
||||
// remote
|
||||
isLoginFresh: false,
|
||||
|
@ -29,6 +34,7 @@ const STATE = {
|
|||
hasReadSetup: false,
|
||||
initializing: false,
|
||||
hasSavedCodes: false,
|
||||
hasVerified: false,
|
||||
password: '',
|
||||
verifyCode: '',
|
||||
isVerifying: false,
|
||||
|
@ -36,18 +42,18 @@ const STATE = {
|
|||
};
|
||||
type State = typeof STATE;
|
||||
|
||||
class MFAuth extends React.Component<{}, State> {
|
||||
class MFAuth extends React.Component<Props, State> {
|
||||
state = STATE;
|
||||
componentDidMount() {
|
||||
this.update2faStateFromServer();
|
||||
}
|
||||
render() {
|
||||
if (store.is2faAuthed) return 'You should not be here.';
|
||||
const {
|
||||
loaded,
|
||||
hasReadSetup,
|
||||
password,
|
||||
hasSavedCodes,
|
||||
hasVerified,
|
||||
verifyCode,
|
||||
isLoginFresh,
|
||||
has2fa,
|
||||
|
@ -61,6 +67,7 @@ class MFAuth extends React.Component<{}, State> {
|
|||
backupCodeCount,
|
||||
isEmailVerified,
|
||||
} = this.state;
|
||||
const { isReset } = this.props;
|
||||
|
||||
const emailNotVerifiedWarning = loaded &&
|
||||
!isEmailVerified && (
|
||||
|
@ -93,6 +100,9 @@ class MFAuth extends React.Component<{}, State> {
|
|||
<div className="MFAuth">
|
||||
{emailNotVerifiedWarning || (
|
||||
<>
|
||||
<h1>
|
||||
{isReset ? 'Reset two-factor authentication' : 'Two-factor authentication'}
|
||||
</h1>
|
||||
{lowBackupCodesWarning}
|
||||
{children}
|
||||
</>
|
||||
|
@ -105,34 +115,17 @@ class MFAuth extends React.Component<{}, State> {
|
|||
return wrap(<Spin tip="Loading security details..." />);
|
||||
}
|
||||
|
||||
// STEP 1 (outline)
|
||||
if (!has2fa && !hasReadSetup) {
|
||||
return wrap(
|
||||
<div>
|
||||
{!has2fa && (
|
||||
<Alert type="warning" message="Administration requires 2fa setup." />
|
||||
)}
|
||||
<h1>Two-factor Authentication Setup</h1>
|
||||
<p>Please be prepared to perform the following steps:</p>
|
||||
<ol>
|
||||
<li>Save two-factor recovery codes</li>
|
||||
<li>
|
||||
Setup up TOTP authentication device, typically a smartphone with Google
|
||||
Authenticator, Authy, 1Password or other compatible authenticator app.
|
||||
</li>
|
||||
</ol>
|
||||
<Button onClick={this.handleReadSetup} type="primary">
|
||||
I'm ready
|
||||
</Button>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
// STEP 2 (if login is stale)
|
||||
if (!has2fa && !isLoginFresh) {
|
||||
// STEP 0. (if login is stale)
|
||||
if ((!has2fa || isReset) && !isLoginFresh) {
|
||||
return wrap(
|
||||
<>
|
||||
<h1>Please verify your password</h1>
|
||||
<h2>
|
||||
<Icon type="unlock" /> Please verify your password
|
||||
</h2>
|
||||
<p>
|
||||
Too much time has elapsed since you last affirmed your credentials, please
|
||||
enter your password below.
|
||||
</p>
|
||||
<Input.Password
|
||||
onPressEnter={this.handleSubmitPassword}
|
||||
onChange={e => this.setState({ password: e.target.value })}
|
||||
|
@ -151,14 +144,53 @@ class MFAuth extends React.Component<{}, State> {
|
|||
);
|
||||
}
|
||||
|
||||
// STEP 3 (recovery codes)
|
||||
if (!has2fa && !hasSavedCodes) {
|
||||
// STEP 1 (outline)
|
||||
if ((!has2fa || isReset) && !hasReadSetup) {
|
||||
return wrap(
|
||||
<div>
|
||||
{!has2fa && <Alert type="info" message="Administration requires 2fa setup." />}
|
||||
{isReset && (
|
||||
<Alert
|
||||
type="warning"
|
||||
message={
|
||||
<>
|
||||
Your current recovery codes and authenticator app setup will be
|
||||
invalidated when you continue.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<h2>1. Two-factor Authentication Setup</h2>
|
||||
<p>Please be prepared to perform the following steps:</p>
|
||||
<ol>
|
||||
<li>Save two-factor recovery codes</li>
|
||||
<li>
|
||||
Setup up TOTP authentication device, typically a smartphone with Google
|
||||
Authenticator, Authy, 1Password or other compatible authenticator app.
|
||||
</li>
|
||||
</ol>
|
||||
<div className="MFAuth-controls">
|
||||
{isReset && (
|
||||
<Link to="/settings">
|
||||
<Button>Cancel</Button>
|
||||
</Link>
|
||||
)}
|
||||
<Button onClick={this.handleReadSetup} type="primary">
|
||||
I'm ready
|
||||
</Button>
|
||||
</div>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
// STEP 2 (recovery codes)
|
||||
if ((!has2fa || isReset) && !hasSavedCodes) {
|
||||
return wrap(
|
||||
((initializing || !backupCodes.length) && (
|
||||
<Spin tip="Loading 2fa setup..." />
|
||||
)) || (
|
||||
<div>
|
||||
<h1>Recovery codes</h1>
|
||||
<h2>2. Recovery codes</h2>
|
||||
<p>
|
||||
Please copy, download or print these codes and keep them safe. Treat them
|
||||
with the same care as passwords.
|
||||
|
@ -207,10 +239,10 @@ class MFAuth extends React.Component<{}, State> {
|
|||
}
|
||||
|
||||
// STEP 4 (totp setup/verify)
|
||||
if (!has2fa && hasSavedCodes) {
|
||||
if ((!has2fa || isReset) && !hasVerified) {
|
||||
return wrap(
|
||||
<div>
|
||||
<h1>Set up Authenticator</h1>
|
||||
<h2>3. Set up Authenticator</h2>
|
||||
<p>
|
||||
Please scan the barcode with your athenticator application. If you cannot
|
||||
scan, please{' '}
|
||||
|
@ -256,18 +288,18 @@ class MFAuth extends React.Component<{}, State> {
|
|||
disabled={verifyCode.length === 0}
|
||||
loading={isVerifying}
|
||||
>
|
||||
Enable
|
||||
{isReset ? 'Reset' : 'Enable'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
// FINAL & unauthed
|
||||
// unauthed
|
||||
if (has2fa && !is2faAuthed) {
|
||||
return wrap(
|
||||
<>
|
||||
<h1>2FAuthentication required</h1>
|
||||
<h2>2FAuthentication required</h2>
|
||||
<p>
|
||||
Enter the current code from your authenticator application. Enter a backup
|
||||
code if you do not have access to your authenticator application.
|
||||
|
@ -278,6 +310,7 @@ class MFAuth extends React.Component<{}, State> {
|
|||
placeholder="123456"
|
||||
value={verifyCode}
|
||||
onChange={e => this.setState({ verifyCode: e.target.value })}
|
||||
onPressEnter={this.handleVerify}
|
||||
/>
|
||||
</div>
|
||||
<div className="MFAuth-controls">
|
||||
|
@ -294,15 +327,15 @@ class MFAuth extends React.Component<{}, State> {
|
|||
);
|
||||
}
|
||||
|
||||
return 'should not get here';
|
||||
return isReset ? <Redirect to="/settings" /> : 'should not get here';
|
||||
}
|
||||
|
||||
private update2faStateFromServer = () => {
|
||||
get2fa()
|
||||
.then(x => {
|
||||
.then(state => {
|
||||
this.setState({
|
||||
loaded: true,
|
||||
...x,
|
||||
...state,
|
||||
});
|
||||
})
|
||||
.catch(handleApiError);
|
||||
|
@ -314,8 +347,8 @@ class MFAuth extends React.Component<{}, State> {
|
|||
};
|
||||
|
||||
private handleReadSetup = async () => {
|
||||
this.setState({ hasReadSetup: true });
|
||||
if (this.state.isLoginFresh) {
|
||||
this.setState({ hasReadSetup: true });
|
||||
this.loadSetup();
|
||||
}
|
||||
};
|
||||
|
@ -323,6 +356,7 @@ class MFAuth extends React.Component<{}, State> {
|
|||
private handleSubmitPassword = async () => {
|
||||
const { password } = this.state;
|
||||
try {
|
||||
// refresh the login
|
||||
await refresh(password);
|
||||
// will set fresh login
|
||||
await this.update2faStateFromServer();
|
||||
|
@ -351,8 +385,9 @@ class MFAuth extends React.Component<{}, State> {
|
|||
this.setState({ isVerifying: true });
|
||||
try {
|
||||
await post2faEnable({ backupCodes, totpSecret, verifyCode });
|
||||
message.success('2FA setup complete!');
|
||||
message.success('Two-factor setup complete!');
|
||||
store.checkLogin(); // should return authenticated status
|
||||
this.setState({ hasVerified: true });
|
||||
// await this.update2faStateFromServer();
|
||||
} catch (e) {
|
||||
handleApiError(e);
|
||||
|
@ -365,7 +400,7 @@ class MFAuth extends React.Component<{}, State> {
|
|||
this.setState({ isVerifying: true });
|
||||
try {
|
||||
await post2faVerify({ verifyCode });
|
||||
message.success('2FAuthentication verified!');
|
||||
message.success('Two-factor authentication verified');
|
||||
store.checkLogin(); // should return authenticated status
|
||||
} catch (e) {
|
||||
handleApiError(e);
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
.Settings {
|
||||
max-width: 600px;
|
||||
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
& > div {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
import React from 'react';
|
||||
import { view } from 'react-easy-state';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button } from 'antd';
|
||||
import './index.less';
|
||||
|
||||
class Settings extends React.Component {
|
||||
state = {
|
||||
username: '',
|
||||
password: '',
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="Settings">
|
||||
<div>
|
||||
<h1>Two-Factor Authentication</h1>
|
||||
<p>
|
||||
This will require saving new recovery codes and setting up an Authenticator
|
||||
application.{' '}
|
||||
<b>Current recovery and Authenticator codes will be invalidated.</b>
|
||||
</p>
|
||||
<Link to="/settings/2fa-reset">
|
||||
<Button>Setup 2FA</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default view(Settings);
|
|
@ -10,6 +10,10 @@
|
|||
text-align: center;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
& .ant-menu-item-divider {
|
||||
background-color: #3d3d3d;
|
||||
}
|
||||
}
|
||||
|
||||
&-layout {
|
||||
|
|
|
@ -75,6 +75,13 @@ class Template extends React.Component<Props> {
|
|||
<span className="nav-text">Moderation</span>
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Item key="settings">
|
||||
<Link to="/settings">
|
||||
<Icon type="setting" />
|
||||
<span className="nav-text">Settings</span>
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="logout" onClick={store.logout}>
|
||||
<Icon type="logout" />
|
||||
<span className="nav-text">Logout</span>
|
||||
|
|
|
@ -112,7 +112,7 @@ def admin_auth_required(f):
|
|||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
user = get_authed_user()
|
||||
if admin_is_authed() and admin_is_2fa_authed():
|
||||
if admin_is_authed() and admin_is_2fa_authed() and is_email_verified():
|
||||
return f(*args, **kwargs)
|
||||
else:
|
||||
return {"message": "Authentication required"}, 401
|
||||
|
|
Loading…
Reference in New Issue