mirror of
https://github.com/balzack/databag.git
synced 2025-02-12 03:29:16 +00:00
integrated admin login with mfa
This commit is contained in:
parent
51306e92c4
commit
0001f6c8c9
@ -34,7 +34,7 @@ func AddAdminMFAuth(w http.ResponseWriter, r *http.Request) {
|
|||||||
if res := tx.Clauses(clause.OnConflict{
|
if res := tx.Clauses(clause.OnConflict{
|
||||||
Columns: []clause.Column{{Name: "config_id"}},
|
Columns: []clause.Column{{Name: "config_id"}},
|
||||||
DoUpdates: clause.AssignmentColumns([]string{"bool_value"}),
|
DoUpdates: clause.AssignmentColumns([]string{"bool_value"}),
|
||||||
}).Create(&store.Config{ConfigID: CNFMFAEnabled, BoolValue: false}).Error; res != nil {
|
}).Create(&store.Config{ConfigID: CNFMFAEnabled, BoolValue: true}).Error; res != nil {
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,7 +42,7 @@ func AddAdminMFAuth(w http.ResponseWriter, r *http.Request) {
|
|||||||
if res := tx.Clauses(clause.OnConflict{
|
if res := tx.Clauses(clause.OnConflict{
|
||||||
Columns: []clause.Column{{Name: "config_id"}},
|
Columns: []clause.Column{{Name: "config_id"}},
|
||||||
DoUpdates: clause.AssignmentColumns([]string{"bool_value"}),
|
DoUpdates: clause.AssignmentColumns([]string{"bool_value"}),
|
||||||
}).Create(&store.Config{ConfigID: CNFMFAConfirmed, BoolValue: true}).Error; res != nil {
|
}).Create(&store.Config{ConfigID: CNFMFAConfirmed, BoolValue: false}).Error; res != nil {
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ func AddMultiFactorAuth(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
key, err := totp.Generate(totp.GenerateOpts{
|
key, err := totp.Generate(totp.GenerateOpts{
|
||||||
Issuer: APPMFAIssuer,
|
Issuer: APPMFAIssuer,
|
||||||
AccountName: account.GUID,
|
AccountName: account.Handle,
|
||||||
Digits: otp.DigitsSix,
|
Digits: otp.DigitsSix,
|
||||||
Algorithm: otp.AlgorithmSHA256,
|
Algorithm: otp.AlgorithmSHA256,
|
||||||
})
|
})
|
||||||
|
@ -19,13 +19,13 @@ func RemoveAdminMFAuth(w http.ResponseWriter, r *http.Request) {
|
|||||||
err := store.DB.Transaction(func(tx *gorm.DB) error {
|
err := store.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
if res := tx.Clauses(clause.OnConflict{
|
if res := tx.Clauses(clause.OnConflict{
|
||||||
Columns: []clause.Column{{Name: "config_id"}},
|
Columns: []clause.Column{{Name: "config_id"}},
|
||||||
DoUpdates: clause.AssignmentColumns([]string{"str_value"}),
|
DoUpdates: clause.AssignmentColumns([]string{"bool_value"}),
|
||||||
}).Create(&store.Config{ConfigID: CNFMFAConfirmed, BoolValue: false}).Error; res != nil {
|
}).Create(&store.Config{ConfigID: CNFMFAConfirmed, BoolValue: false}).Error; res != nil {
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
if res := tx.Clauses(clause.OnConflict{
|
if res := tx.Clauses(clause.OnConflict{
|
||||||
Columns: []clause.Column{{Name: "config_id"}},
|
Columns: []clause.Column{{Name: "config_id"}},
|
||||||
DoUpdates: clause.AssignmentColumns([]string{"str_value"}),
|
DoUpdates: clause.AssignmentColumns([]string{"bool_value"}),
|
||||||
}).Create(&store.Config{ConfigID: CNFMFAEnabled, BoolValue: false}).Error; res != nil {
|
}).Create(&store.Config{ConfigID: CNFMFAEnabled, BoolValue: false}).Error; res != nil {
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,10 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"github.com/pquerna/otp"
|
||||||
|
"github.com/pquerna/otp/totp"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
//SetAdminAccess begins a session for admin access
|
//SetAdminAccess begins a session for admin access
|
||||||
@ -18,6 +22,60 @@ func SetAdminAccess(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check mfa
|
||||||
|
curTime := time.Now().Unix()
|
||||||
|
failedTime := getNumConfigValue(CNFMFAFailedTime, 0);
|
||||||
|
failedCount := getNumConfigValue(CNFMFAFailedCount, 0);
|
||||||
|
if failedTime + APPMFAFailPeriod > curTime && failedCount > APPMFAFailCount {
|
||||||
|
ErrResponse(w, http.StatusTooManyRequests, errors.New("temporarily locked"))
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mfaEnabled := getBoolConfigValue(CNFMFAEnabled, false);
|
||||||
|
mfaConfirmed := getBoolConfigValue(CNFMFAConfirmed, false);
|
||||||
|
if mfaEnabled && mfaConfirmed {
|
||||||
|
code := r.FormValue("code")
|
||||||
|
if code == "" {
|
||||||
|
ErrResponse(w, http.StatusMethodNotAllowed, errors.New("totp code required"))
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
secret := getStrConfigValue(CNFMFASecret, "");
|
||||||
|
opts := totp.ValidateOpts{Period: 30, Skew: 1, Digits: otp.DigitsSix, Algorithm: otp.AlgorithmSHA256}
|
||||||
|
if valid, _ := totp.ValidateCustom(code, secret, time.Now(), opts); !valid {
|
||||||
|
err := store.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
|
if failedTime + APPMFAFailPeriod > curTime {
|
||||||
|
if res := tx.Clauses(clause.OnConflict{
|
||||||
|
Columns: []clause.Column{{Name: "config_id"}},
|
||||||
|
DoUpdates: clause.AssignmentColumns([]string{"num_value"}),
|
||||||
|
}).Create(&store.Config{ConfigID: CNFMFAFailedCount, NumValue: failedCount + 1}).Error; res != nil {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if res := tx.Clauses(clause.OnConflict{
|
||||||
|
Columns: []clause.Column{{Name: "config_id"}},
|
||||||
|
DoUpdates: clause.AssignmentColumns([]string{"num_value"}),
|
||||||
|
}).Create(&store.Config{ConfigID: CNFMFAFailedTime, NumValue: curTime}).Error; res != nil {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
if res := tx.Clauses(clause.OnConflict{
|
||||||
|
Columns: []clause.Column{{Name: "config_id"}},
|
||||||
|
DoUpdates: clause.AssignmentColumns([]string{"num_value"}),
|
||||||
|
}).Create(&store.Config{ConfigID: CNFMFAFailedCount, NumValue: failedCount + 1}).Error; res != nil {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
LogMsg("failed to increment fail count");
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrResponse(w, http.StatusForbidden, errors.New("invalid code"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// gernate app token
|
// gernate app token
|
||||||
data, err := securerandom.Bytes(APPTokenSize)
|
data, err := securerandom.Bytes(APPTokenSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -77,7 +77,7 @@ func SetAdminMFAuth(w http.ResponseWriter, r *http.Request) {
|
|||||||
// upsert mfa confirmed
|
// upsert mfa confirmed
|
||||||
if res := tx.Clauses(clause.OnConflict{
|
if res := tx.Clauses(clause.OnConflict{
|
||||||
Columns: []clause.Column{{Name: "config_id"}},
|
Columns: []clause.Column{{Name: "config_id"}},
|
||||||
DoUpdates: clause.AssignmentColumns([]string{"str_value"}),
|
DoUpdates: clause.AssignmentColumns([]string{"bool_value"}),
|
||||||
}).Create(&store.Config{ConfigID: CNFMFAConfirmed, BoolValue: true}).Error; res != nil {
|
}).Create(&store.Config{ConfigID: CNFMFAConfirmed, BoolValue: true}).Error; res != nil {
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Button, Modal, Form, Input } from 'antd';
|
import { Button, Modal, Form, Input } from 'antd';
|
||||||
import { UserOutlined, LockOutlined } from '@ant-design/icons';
|
import { UserOutlined, LockOutlined } from '@ant-design/icons';
|
||||||
import { AdminWrapper } from './Admin.styled';
|
import { AdminWrapper, MFAModal } from './Admin.styled';
|
||||||
import { useAdmin } from './useAdmin.hook';
|
import { useAdmin } from './useAdmin.hook';
|
||||||
|
|
||||||
export function Admin() {
|
export function Admin() {
|
||||||
@ -55,6 +55,26 @@ export function Admin() {
|
|||||||
|
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
<Modal centerd closable={false} footer={null} visible={state.mfaModal} destroyOnClose={true} bodyStyle={{ borderRadius: 8, padding: 16, ...state.menuStyle }} onCancel={actions.dismissMFA}>
|
||||||
|
<MFAModal>
|
||||||
|
<div className="title">{state.strings.mfaTitle}</div>
|
||||||
|
<div className="description">{state.strings.mfaEnter}</div>
|
||||||
|
<Input.OTP onChange={actions.setCode} />
|
||||||
|
<div className="alert">
|
||||||
|
{ state.mfaError === '403' && (
|
||||||
|
<span>{state.strings.mfaError}</span>
|
||||||
|
)}
|
||||||
|
{ state.mfaError === '429' && (
|
||||||
|
<span>{state.strings.mfaDisabled}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="controls">
|
||||||
|
<Button key="back" onClick={actions.dismissMFA}>{state.strings.cancel}</Button>
|
||||||
|
<Button key="save" type="primary" className={state.mfaCode ? 'saveEnabled' : 'saveDisabled'} onClick={login}
|
||||||
|
disabled={!state.mfaCode} loading={state.busy}>{state.strings.login}</Button>
|
||||||
|
</div>
|
||||||
|
</MFAModal>
|
||||||
|
</Modal>
|
||||||
</AdminWrapper>
|
</AdminWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -72,3 +72,53 @@ export const AdminWrapper = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
||||||
|
export const MFAModal = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 1.0rem;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
padding-top: 4px;
|
||||||
|
border-bottom: 1px solid ${props => props.theme.sectionBorder};
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
height: 24px;
|
||||||
|
color: ${props => props.theme.alertText};
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.saveDisabled {
|
||||||
|
background-color: ${props => props.theme.disabledArea};
|
||||||
|
|
||||||
|
button {
|
||||||
|
color: ${props => props.theme.idleText};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.saveEnabled {
|
||||||
|
background-color: ${props => props.theme.enabledArea};
|
||||||
|
|
||||||
|
button {
|
||||||
|
color: ${props => props.theme.activeText};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
@ -16,6 +16,9 @@ export function useAdmin() {
|
|||||||
busy: false,
|
busy: false,
|
||||||
strings: {},
|
strings: {},
|
||||||
menuStyle: {},
|
menuStyle: {},
|
||||||
|
mfaModal: false,
|
||||||
|
mfaCode: null,
|
||||||
|
mfaError: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -53,11 +56,23 @@ export function useAdmin() {
|
|||||||
if (state.unclaimed === true) {
|
if (state.unclaimed === true) {
|
||||||
await setNodeStatus(state.password);
|
await setNodeStatus(state.password);
|
||||||
}
|
}
|
||||||
const session = await setNodeAccess(state.password);
|
try {
|
||||||
|
const session = await setNodeAccess(state.password, state.mfaCode);
|
||||||
updateState({ busy: false });
|
|
||||||
app.actions.setAdmin(session);
|
app.actions.setAdmin(session);
|
||||||
}
|
}
|
||||||
|
catch (err) {
|
||||||
|
const msg = err?.message;
|
||||||
|
if (msg === '405' || msg === '403' || msg === '429') {
|
||||||
|
updateState({ busy: false, mfaModal: true, mfaError: msg });
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log(err);
|
||||||
|
updateState({ busy: false })
|
||||||
|
throw new Error('login failed: check your username and password');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateState({ busy: false });
|
||||||
|
}
|
||||||
catch(err) {
|
catch(err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
updateState({ busy: false });
|
updateState({ busy: false });
|
||||||
@ -65,6 +80,12 @@ export function useAdmin() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setCode: (mfaCode) => {
|
||||||
|
updateState({ mfaCode });
|
||||||
|
},
|
||||||
|
dismissMFA: () => {
|
||||||
|
updateState({ mfaModal: false, mfaCode: null });
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -93,7 +93,6 @@ console.log(state.mfaError);
|
|||||||
<Button key="save" type="primary" className={state.mfaCode ? 'saveEnabled' : 'saveDisabled'} onClick={login}
|
<Button key="save" type="primary" className={state.mfaCode ? 'saveEnabled' : 'saveDisabled'} onClick={login}
|
||||||
disabled={!state.mfaCode} loading={state.busy}>{state.strings.login}</Button>
|
disabled={!state.mfaCode} loading={state.busy}>{state.strings.login}</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</MFAModal>
|
</MFAModal>
|
||||||
</Modal>
|
</Modal>
|
||||||
</LoginWrapper>
|
</LoginWrapper>
|
||||||
|
8
net/web/src/api/addAdminMFAuth.js
Normal file
8
net/web/src/api/addAdminMFAuth.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { checkResponse, fetchWithTimeout } from './fetchUtil';
|
||||||
|
|
||||||
|
export async function addAdminMFAuth(token) {
|
||||||
|
const mfa = await fetchWithTimeout(`/admin/mfauth?token=${token}`, { method: 'POST' })
|
||||||
|
checkResponse(mfa);
|
||||||
|
return mfa.json();
|
||||||
|
}
|
||||||
|
|
7
net/web/src/api/removeAdminMFAuth.js
Normal file
7
net/web/src/api/removeAdminMFAuth.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { checkResponse, fetchWithTimeout } from './fetchUtil';
|
||||||
|
|
||||||
|
export async function removeAdminMFAuth(token) {
|
||||||
|
const mfa = await fetchWithTimeout(`/admin/mfauth?token=${token}`, { method: 'DELETE' })
|
||||||
|
checkResponse(mfa);
|
||||||
|
}
|
||||||
|
|
7
net/web/src/api/setAdminMFAuth.js
Normal file
7
net/web/src/api/setAdminMFAuth.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { checkResponse, fetchWithTimeout } from './fetchUtil';
|
||||||
|
|
||||||
|
export async function setAdminMFAuth(token, code) {
|
||||||
|
const mfa = await fetchWithTimeout(`/admin/mfauth?token=${token}&code=${code}`, { method: 'PUT' })
|
||||||
|
checkResponse(mfa);
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,8 @@
|
|||||||
import { checkResponse, fetchWithTimeout } from './fetchUtil';
|
import { checkResponse, fetchWithTimeout } from './fetchUtil';
|
||||||
|
|
||||||
export async function setNodeAccess(token) {
|
export async function setNodeAccess(token, code) {
|
||||||
const access = await fetchWithTimeout(`/admin/access?token=${encodeURIComponent(token)}`, { method: 'PUT' });
|
const mfa = code ? `&code=${code}` : '';
|
||||||
|
const access = await fetchWithTimeout(`/admin/access?token=${encodeURIComponent(token)}${mfa}`, { method: 'PUT' });
|
||||||
checkResponse(access);
|
checkResponse(access);
|
||||||
return access.json()
|
return access.json()
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { AlertIcon, DashboardWrapper, SettingsButton, AddButton, SettingsLayout, CreateLayout } from './Dashboard.styled';
|
import { AlertIcon, MFAModal, DashboardWrapper, SettingsButton, AddButton, SettingsLayout, CreateLayout } from './Dashboard.styled';
|
||||||
import { Tooltip, Switch, Select, Button, Space, Modal, Input, InputNumber, List } from 'antd';
|
import { Tooltip, Switch, Select, Button, Space, Modal, Input, InputNumber, List } from 'antd';
|
||||||
import { ExclamationCircleOutlined, SettingOutlined, UserAddOutlined, LogoutOutlined, ReloadOutlined, LockOutlined, UnlockOutlined } from '@ant-design/icons';
|
import { ExclamationCircleOutlined, SettingOutlined, UserAddOutlined, LogoutOutlined, ReloadOutlined, LockOutlined, UnlockOutlined } from '@ant-design/icons';
|
||||||
import { ThemeProvider } from "styled-components";
|
import { ThemeProvider } from "styled-components";
|
||||||
@ -19,7 +19,7 @@ export function Dashboard() {
|
|||||||
return window.location.origin + '/#/create?add=' + state.createToken;
|
return window.location.origin + '/#/create?add=' + state.createToken;
|
||||||
};
|
};
|
||||||
|
|
||||||
const disableMFA = () => {
|
const confirmDisableMFA = () => {
|
||||||
modal.confirm({
|
modal.confirm({
|
||||||
title: <span style={state.menuStyle}>{state.strings.confirmDisable}</span>,
|
title: <span style={state.menuStyle}>{state.strings.confirmDisable}</span>,
|
||||||
content: <span style={state.menuStyle}>{state.strings.disablePrompt}</span>,
|
content: <span style={state.menuStyle}>{state.strings.disablePrompt}</span>,
|
||||||
@ -28,12 +28,53 @@ export function Dashboard() {
|
|||||||
okText: state.strings.disable,
|
okText: state.strings.disable,
|
||||||
cancelText: state.strings.cancel,
|
cancelText: state.strings.cancel,
|
||||||
onOk() {
|
onOk() {
|
||||||
actions.disableMFA();
|
disableMFA();
|
||||||
},
|
},
|
||||||
onCancel() {},
|
onCancel() {},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const disableMFA = async () => {
|
||||||
|
try {
|
||||||
|
await actions.disableMFA();
|
||||||
|
}
|
||||||
|
catch(err) {
|
||||||
|
console.log(err);
|
||||||
|
modal.error({
|
||||||
|
title: <span style={state.menuStyle}>{state.strings.operationFailed}</span>,
|
||||||
|
content: <span style={state.menuStyle}>{state.strings.tryAgain}</span>,
|
||||||
|
bodyStyle: { borderRadius: 8, padding: 16, ...state.menuStyle },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const enableMFA = async () => {
|
||||||
|
try {
|
||||||
|
await actions.enableMFA();
|
||||||
|
}
|
||||||
|
catch(err) {
|
||||||
|
console.log(err);
|
||||||
|
modal.error({
|
||||||
|
title: <span style={state.menuStyle}>{state.strings.operationFailed}</span>,
|
||||||
|
content: <span style={state.menuStyle}>{state.strings.tryAgain}</span>,
|
||||||
|
bodyStyle: { borderRadius: 8, padding: 16, ...state.menuStyle },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmMFA = async () => {
|
||||||
|
try {
|
||||||
|
await actions.confirmMFA();
|
||||||
|
}
|
||||||
|
catch(err) {
|
||||||
|
console.log(err);
|
||||||
|
modal.error({
|
||||||
|
title: <span style={state.menuStyle}>{state.strings.operationFailed}</span>,
|
||||||
|
content: <span style={state.menuStyle}>{state.strings.tryAgain}</span>,
|
||||||
|
bodyStyle: { borderRadius: 8, padding: 16, ...state.menuStyle },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={state.colors}>
|
<ThemeProvider theme={state.colors}>
|
||||||
@ -55,13 +96,13 @@ export function Dashboard() {
|
|||||||
{ (state.mfAuthSet && state.mfaAuthEnabled) && (
|
{ (state.mfAuthSet && state.mfaAuthEnabled) && (
|
||||||
<div className="settings">
|
<div className="settings">
|
||||||
<SettingsButton type="text" size="small" icon={<UnlockOutlined />}
|
<SettingsButton type="text" size="small" icon={<UnlockOutlined />}
|
||||||
onClick={disableMFA}></SettingsButton>
|
onClick={confirmDisableMFA}></SettingsButton>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{ (state.mfAuthSet && !state.mfaAuthEnabled) && (
|
{ (state.mfAuthSet && !state.mfaAuthEnabled) && (
|
||||||
<div className="settings">
|
<div className="settings">
|
||||||
<SettingsButton type="text" size="small" icon={<LockOutlined />}
|
<SettingsButton type="text" size="small" icon={<LockOutlined />}
|
||||||
onClick={actions.enableMFA}></SettingsButton>
|
onClick={enableMFA}></SettingsButton>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="settings">
|
<div className="settings">
|
||||||
@ -97,7 +138,7 @@ export function Dashboard() {
|
|||||||
<div className="settings">
|
<div className="settings">
|
||||||
<Tooltip placement="topRight" title={state.strings.disableMultifactor}>
|
<Tooltip placement="topRight" title={state.strings.disableMultifactor}>
|
||||||
<SettingsButton type="text" size="small" icon={<LockOutlined />}
|
<SettingsButton type="text" size="small" icon={<LockOutlined />}
|
||||||
onClick={disableMFA}></SettingsButton>
|
onClick={confirmDisableMFA}></SettingsButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -105,7 +146,7 @@ export function Dashboard() {
|
|||||||
<div className="settings">
|
<div className="settings">
|
||||||
<Tooltip placement="topRight" title={state.strings.enableMultifactor}>
|
<Tooltip placement="topRight" title={state.strings.enableMultifactor}>
|
||||||
<SettingsButton type="text" size="small" icon={<UnlockOutlined />}
|
<SettingsButton type="text" size="small" icon={<UnlockOutlined />}
|
||||||
onClick={actions.enableMFA}></SettingsButton>
|
onClick={enableMFA}></SettingsButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -264,6 +305,32 @@ export function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
</CreateLayout>
|
</CreateLayout>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<Modal bodyStyle={{ borderRadius: 8, padding: 16, ...state.menuStyle }} closable={false} visible={state.mfaModal} centered width="fitContent"
|
||||||
|
destroyOnClose={true} footer={null} onCancel={actions.dismissMFA}>
|
||||||
|
<MFAModal>
|
||||||
|
<div className="title">{state.strings.mfaTitle}</div>
|
||||||
|
<div className="description">{state.strings.mfaSteps}</div>
|
||||||
|
<img src={state.mfaImage} alt="QRCode" />
|
||||||
|
<div className="secret">
|
||||||
|
<div className="label">{ state.mfaText }</div>
|
||||||
|
<CopyButton onCopy={async () => await navigator.clipboard.writeText(state.mfaText)} />
|
||||||
|
</div>
|
||||||
|
<Input.OTP onChange={actions.setCode} />
|
||||||
|
<div className="alert">
|
||||||
|
{ state.mfaError === '401' && (
|
||||||
|
<span>{state.strings.mfaError}</span>
|
||||||
|
)}
|
||||||
|
{ state.mfaError === '429' && (
|
||||||
|
<span>{state.strings.mfaDisabled}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="controls">
|
||||||
|
<Button key="back" onClick={actions.dismissMFA}>{state.strings.cancel}</Button>
|
||||||
|
<Button key="save" type="primary" className={state.mfaCode ? 'saveEnabled' : 'saveDisabled'} onClick={confirmMFA}
|
||||||
|
disabled={!state.mfaCode} loading={state.busy}>{state.strings.mfaConfirm}</Button>
|
||||||
|
</div>
|
||||||
|
</MFAModal>
|
||||||
|
</Modal>
|
||||||
</DashboardWrapper>
|
</DashboardWrapper>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
@ -163,3 +163,72 @@ export const CreateLayout = styled.div`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
export const MFAModal = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
text-aling: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 1.0rem;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
padding-top: 4px;
|
||||||
|
border-bottom: 1px solid ${props => props.theme.sectionBorder};
|
||||||
|
}
|
||||||
|
|
||||||
|
.codeLabel {
|
||||||
|
padding-top: 4px;
|
||||||
|
font-size: 0.9.rem;
|
||||||
|
color: ${props => props.theme.mainText};
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
height: 24px;
|
||||||
|
color: ${props => props.theme.alertText};
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.saveDisabled {
|
||||||
|
background-color: ${props => props.theme.disabledArea};
|
||||||
|
|
||||||
|
button {
|
||||||
|
color: ${props => props.theme.idleText};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.saveEnabled {
|
||||||
|
background-color: ${props => props.theme.enabledArea};
|
||||||
|
|
||||||
|
button {
|
||||||
|
color: ${props => props.theme.activeText};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
@ -9,7 +9,9 @@ import { AppContext } from 'context/AppContext';
|
|||||||
import { SettingsContext } from 'context/SettingsContext';
|
import { SettingsContext } from 'context/SettingsContext';
|
||||||
|
|
||||||
import { getAdminMFAuth } from 'api/getAdminMFAuth';
|
import { getAdminMFAuth } from 'api/getAdminMFAuth';
|
||||||
|
import { addAdminMFAuth } from 'api/addAdminMFAuth';
|
||||||
|
import { setAdminMFAuth } from 'api/setAdminMFAuth';
|
||||||
|
import { removeAdminMFAuth } from 'api/removeAdminMFAuth';
|
||||||
|
|
||||||
export function useDashboard(token) {
|
export function useDashboard(token) {
|
||||||
|
|
||||||
@ -42,11 +44,13 @@ export function useDashboard(token) {
|
|||||||
menuStyle: {},
|
menuStyle: {},
|
||||||
strings: {},
|
strings: {},
|
||||||
|
|
||||||
|
mfaModal: false,
|
||||||
mfAuthSet: false,
|
mfAuthSet: false,
|
||||||
mfAuthEnabled: false,
|
mfAuthEnabled: false,
|
||||||
mfAuthSecretText: null,
|
mfAuthSecretText: null,
|
||||||
mfAuthSecretImage: null,
|
mfAuthSecretImage: null,
|
||||||
mfaAuthError: null,
|
mfaAuthError: null,
|
||||||
|
mfaCode: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -149,9 +153,29 @@ export function useDashboard(token) {
|
|||||||
await syncConfig();
|
await syncConfig();
|
||||||
await syncAccounts();
|
await syncAccounts();
|
||||||
},
|
},
|
||||||
|
setCode: async (code) => {
|
||||||
|
updateState({ mfaCode: code });
|
||||||
|
},
|
||||||
enableMFA: async () => {
|
enableMFA: async () => {
|
||||||
|
const mfa = await addAdminMFAuth(app.state.adminToken);
|
||||||
|
updateState({ mfaModal: true, mfaError: false, mfaText: mfa.secretText, mfaImage: mfa.secretImage, mfaCode: '' });
|
||||||
},
|
},
|
||||||
disableMFA: async () => {
|
disableMFA: async () => {
|
||||||
|
const mfa = await removeAdminMFAuth(app.state.adminToken);
|
||||||
|
updateState({ mfaAuthEnabled: false });
|
||||||
|
},
|
||||||
|
confirmMFA: async () => {
|
||||||
|
try {
|
||||||
|
await setAdminMFAuth(app.state.adminToken, state.mfaCode);
|
||||||
|
updateState({ mfaAuthEnabled: true, mfaModal: false });
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
const msg = err?.message;
|
||||||
|
updateState({ mfaError: msg });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissMFA: async () => {
|
||||||
|
updateState({ mfaModal: false });
|
||||||
},
|
},
|
||||||
setSettings: async () => {
|
setSettings: async () => {
|
||||||
if (!state.busy) {
|
if (!state.busy) {
|
||||||
|
Loading…
Reference in New Issue
Block a user