mirror of
https://github.com/balzack/databag.git
synced 2025-03-13 00:50:03 +00:00
support mfa setting in admin setup
This commit is contained in:
parent
db77fbd0a4
commit
9a3b9c4bd0
@ -124,4 +124,87 @@ export const styles = StyleSheet.create({
|
||||
controlSwitch: {
|
||||
transform: [{scaleX: 0.7}, {scaleY: 0.7}],
|
||||
},
|
||||
modal: {
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
blur: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
modalHeader: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
modalLabel: {
|
||||
fontSize: 20,
|
||||
},
|
||||
modalClose: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
modalControls: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: 16,
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
marginTop: 16,
|
||||
},
|
||||
modalDescription: {
|
||||
paddingTop: 16,
|
||||
},
|
||||
authMessage: {
|
||||
height: 24,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingTop: 8,
|
||||
},
|
||||
authMessageText: {
|
||||
fontSize: 16,
|
||||
color: Colors.danger,
|
||||
},
|
||||
modalContent: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
gap: 8,
|
||||
},
|
||||
modalSurface: {
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
},
|
||||
secretImage: {
|
||||
width: 192,
|
||||
height: 192,
|
||||
alignSelf: 'center',
|
||||
borderRadius: 8,
|
||||
margin: 16,
|
||||
},
|
||||
secretText: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingBottom: 16,
|
||||
paddingLeft: 8,
|
||||
paddingRight: 8,
|
||||
},
|
||||
secret: {
|
||||
paddingRight: 16,
|
||||
},
|
||||
secretIcon: {
|
||||
marginLeft: 8,
|
||||
},
|
||||
});
|
||||
|
@ -1,13 +1,21 @@
|
||||
import React from 'react';
|
||||
import {SafeAreaView, Image, View, Pressable} from 'react-native';
|
||||
import {ActivityIndicator, RadioButton, Switch, Surface, Divider, TextInput, Text} from 'react-native-paper';
|
||||
import React, { useState } from 'react';
|
||||
import {SafeAreaView, TouchableOpacity, Modal, Image, View, Pressable} from 'react-native';
|
||||
import {ActivityIndicator, Icon, Button, IconButton, RadioButton, Switch, Surface, Divider, TextInput, Text} from 'react-native-paper';
|
||||
import {styles} from './Setup.styled';
|
||||
import {useSetup} from './useSetup.hook';
|
||||
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
|
||||
import { Confirm } from '../confirm/Confirm';
|
||||
import { Colors } from '../constants/Colors';
|
||||
import { InputCode } from '../utils/InputCode';
|
||||
import {BlurView} from '@react-native-community/blur';
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
|
||||
export function Setup() {
|
||||
const { state, actions } = useSetup();
|
||||
const [mfaCode, setMfaCode] = useState('');
|
||||
const [updating, setUpdating] = useState(false);
|
||||
const [secretCopy, setSecretCopy] = useState(false);
|
||||
const [confirmingAuth, setConfirmingAuth] = useState(false);
|
||||
|
||||
const errorParams = {
|
||||
title: state.strings.operationFailed,
|
||||
@ -18,6 +26,36 @@ export function Setup() {
|
||||
},
|
||||
};
|
||||
|
||||
const confirmAuth = async () => {
|
||||
if (!confirmingAuth) {
|
||||
setConfirmingAuth(true);
|
||||
await actions.confirmMFAuth();
|
||||
setConfirmingAuth(false);
|
||||
}
|
||||
}
|
||||
|
||||
const copySecret = async () => {
|
||||
if (!secretCopy) {
|
||||
setSecretCopy(true);
|
||||
Clipboard.setString(state.secretText);
|
||||
setTimeout(() => {
|
||||
setSecretCopy(false);
|
||||
}, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMFAuth = async () => {
|
||||
if (!updating) {
|
||||
setUpdating(true);
|
||||
if (state.mfaEnabled) {
|
||||
await actions.disableMFAuth();
|
||||
} else {
|
||||
await actions.enableMFAuth();
|
||||
}
|
||||
setUpdating(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.setup}>
|
||||
<View style={styles.header}>
|
||||
@ -133,6 +171,10 @@ export function Setup() {
|
||||
<Text style={styles.label}>{state.strings.allowUnsealed}:</Text>
|
||||
<Switch style={styles.controlSwitch} value={state.setup?.allowUnsealed} disabled={state.loading} onValueChange={()=>actions.setAllowUnsealed(!state.setup?.allowUnsealed)} />
|
||||
</View>
|
||||
<View style={styles.option}>
|
||||
<Text style={styles.label}>{state.strings.mfaTitle}:</Text>
|
||||
<Switch style={styles.controlSwitch} value={state.mfaEnabled} disabled={state.loading} onValueChange={toggleMFAuth} />
|
||||
</View>
|
||||
<Divider style={styles.divider} bold={false} />
|
||||
<View style={styles.option}>
|
||||
<Text style={styles.label}>{state.strings.enableImage}:</Text>
|
||||
@ -264,6 +306,43 @@ export function Setup() {
|
||||
</KeyboardAwareScrollView>
|
||||
<Divider style={styles.line} bold={true} />
|
||||
<Confirm show={state.error} params={errorParams} />
|
||||
<Modal animationType="fade" transparent={true} supportedOrientations={['portrait', 'landscape']} visible={state.confirmingMFAuth} onRequestClose={actions.cancelMFAuth}>
|
||||
<View style={styles.modal}>
|
||||
<BlurView style={styles.blur} blurType="dark" blurAmount={2} reducedTransparencyFallbackColor="dark" />
|
||||
<KeyboardAwareScrollView enableOnAndroid={true} style={styles.container} contentContainerStyle={styles.modalContent}>
|
||||
<Surface elevation={4} mode="flat" style={styles.modalSurface}>
|
||||
<Text style={styles.modalLabel}>{state.strings.mfaTitle}</Text>
|
||||
<IconButton style={styles.modalClose} icon="close" size={24} onPress={actions.cancelMFAuth} />
|
||||
<Text style={styles.modalDescription}>{state.strings.mfaSteps}</Text>
|
||||
<Image style={styles.secretImage} resizeMode={'contain'} source={{uri: state.confirmMFAuthImage}} />
|
||||
|
||||
<View style={styles.secretText}>
|
||||
<Text style={styles.secret} selectable={true} adjustsFontSizeToFit={true} numberOfLines={1}>
|
||||
{state.confirmMFAuthText}
|
||||
</Text>
|
||||
<TouchableOpacity onPress={copySecret}>
|
||||
<Icon style={styles.secretIcon} size={18} source={secretCopy ? 'check' : 'content-copy'} color={Colors.primary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<InputCode onChangeText={actions.setMFAuthCode} />
|
||||
|
||||
<View style={styles.authMessage}>
|
||||
<Text style={styles.authMessageText}>{ state.mfaMessage }</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.modalControls}>
|
||||
<Button mode="outlined" onPress={actions.cancelMFAuth}>
|
||||
{state.strings.cancel}
|
||||
</Button>
|
||||
<Button mode="contained" loading={confirmingAuth} disabled={state.mfaCode.length !== 6} onPress={confirmAuth}>
|
||||
{state.strings.mfaConfirm}
|
||||
</Button>
|
||||
</View>
|
||||
</Surface>
|
||||
</KeyboardAwareScrollView>
|
||||
</View>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
@ -22,6 +22,12 @@ export function useSetup() {
|
||||
error: false,
|
||||
accountStorage: '',
|
||||
setup: null as null | Setup,
|
||||
mfaEnabled: false,
|
||||
mfaCode: '',
|
||||
mfaMessage: '',
|
||||
confirmingMFAuth: false,
|
||||
confirmMFAuthText: '',
|
||||
confirmMFAuthImage: '',
|
||||
});
|
||||
|
||||
const updateState = (value: any) => {
|
||||
@ -32,10 +38,11 @@ export function useSetup() {
|
||||
while (loading.current) {
|
||||
try {
|
||||
const service = app.state.service;
|
||||
const mfaEnabled = await service.checkMFAuth();
|
||||
setup.current = await service.getSetup();
|
||||
loading.current = false;
|
||||
const storage = Math.floor(setup.current.accountStorage / 1073741824);
|
||||
updateState({ setup: setup.current, accountStorage: storage.toString(), loading: false });
|
||||
updateState({ setup: setup.current, mfaEnabled, accountStorage: storage.toString(), loading: false });
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
await new Promise((r) => setTimeout(r, DELAY_MS));
|
||||
@ -83,6 +90,47 @@ export function useSetup() {
|
||||
clearError: () => {
|
||||
updateState({ error: false });
|
||||
},
|
||||
enableMFAuth: async () => {
|
||||
try {
|
||||
const service = app.state.service;
|
||||
const { text, image } = await service.enableMFAuth();
|
||||
updateState({ confirmingMFAuth: true, mfaCode: '', mfaMessage: '', confirmMFAuthText: text, confirmMFAuthImage: image });
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
updateState({ error: true });
|
||||
}
|
||||
},
|
||||
disableMFAuth: async () => {
|
||||
try {
|
||||
const service = app.state.service;
|
||||
await service.disableMFAuth();
|
||||
updateState({ mfaEnabled: false });
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
updateState({ error: true });
|
||||
}
|
||||
},
|
||||
confirmMFAuth: async () => {
|
||||
try {
|
||||
const service = app.state.service;
|
||||
await service.confirmMFAuth(state.mfaCode);
|
||||
updateState({ confirmingMFAuth: false, mfaEnabled: true });
|
||||
} catch (err) {
|
||||
if (err.message === '401') {
|
||||
updateState({ mfaMessage: state.strings.mfaError });
|
||||
} else if (err.message === '429') {
|
||||
updateState({ mfaMessage: state.strings.mfaDisabled });
|
||||
} else {
|
||||
updateState({ mfaMessage: state.strings.error });
|
||||
}
|
||||
}
|
||||
},
|
||||
cancelMFAuth: () => {
|
||||
updateState({ confirmingMFAuth: false });
|
||||
},
|
||||
setMFAuthCode: (mfaCode: string) => {
|
||||
updateState({ mfaCode });
|
||||
},
|
||||
setDomain: (domain: string) => {
|
||||
if (setup.current) {
|
||||
setup.current.domain = domain;
|
||||
|
@ -169,6 +169,7 @@ export interface Service {
|
||||
removeMember(accountId: number): Promise<void>;
|
||||
getSetup(): Promise<Setup>;
|
||||
setSetup(setup: Setup): Promise<void>;
|
||||
checkMFAuth(): Promise<boolean>;
|
||||
enableMFAuth(): Promise<{ image: string, text: string }>;
|
||||
confirmMFAuth(code: string): Promise<void>;
|
||||
disableMFAuth(): Promise<void>;
|
||||
|
@ -83,10 +83,16 @@ export class ServiceModule implements Service {
|
||||
await setNodeConfig(node, secure, token, entity);
|
||||
}
|
||||
|
||||
public async checkMFAuth(): Promise<boolean> {
|
||||
const { node, secure, token } = this;
|
||||
const enabled = await getAdminMFAuth(node, secure, token);
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public async enableMFAuth(): Promise<{ image: string, text: string}> {
|
||||
const { node, secure, token } = this;
|
||||
const { secretImage, secretText } = await addAdminMFAuth(node, secure, token);
|
||||
return { secretImage, secretText };
|
||||
return { image: secretImage, text: secretText };
|
||||
}
|
||||
|
||||
public async disableMFAuth(): Promise<void> {
|
||||
|
Loading…
Reference in New Issue
Block a user