mirror of
https://github.com/balzack/databag.git
synced 2025-04-20 08:35:15 +00:00
adding mobile mfa settings control
This commit is contained in:
parent
97f375472e
commit
a1041fb225
11
app/mobile/src/api/addAccountMFA.js
Normal file
11
app/mobile/src/api/addAccountMFA.js
Normal file
@ -0,0 +1,11 @@
|
||||
import { checkResponse, fetchWithTimeout } from './fetchUtil';
|
||||
|
||||
export async function addAccountMFA(server, token) {
|
||||
const insecure = /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|:\d+$|$)){4}$/.test(server);
|
||||
const protocol = insecure ? 'http' : 'https';
|
||||
|
||||
const mfa = await fetchWithTimeout(`${protocol}://${server}/account/mfauth?agent=${token}`, { method: 'POST' })
|
||||
checkResponse(mfa);
|
||||
return mfa.json();
|
||||
}
|
||||
|
10
app/mobile/src/api/removeAccountMFA.js
Normal file
10
app/mobile/src/api/removeAccountMFA.js
Normal file
@ -0,0 +1,10 @@
|
||||
import { checkResponse, fetchWithTimeout } from './fetchUtil';
|
||||
|
||||
export async function removeAccountMFA(server, token) {
|
||||
const insecure = /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|:\d+$|$)){4}$/.test(server);
|
||||
const protocol = insecure ? 'http' : 'https';
|
||||
|
||||
const mfa = await fetchWithTimeout(`${protocol}://${server}/account/mfauth?agent=${token}`, { method: 'DELETE' });
|
||||
checkResponse(mfa);
|
||||
}
|
||||
|
10
app/mobile/src/api/setAccountMFA.js
Normal file
10
app/mobile/src/api/setAccountMFA.js
Normal file
@ -0,0 +1,10 @@
|
||||
import { checkResponse, fetchWithTimeout } from './fetchUtil';
|
||||
|
||||
export async function setAccountMFA(server, token, code) {
|
||||
const insecure = /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|:\d+$|$)){4}$/.test(server);
|
||||
const protocol = insecure ? 'http' : 'https';
|
||||
|
||||
const mfa = await fetchWithTimeout(`${protocol}://${server}/account/mfauth?agent=${token}&code=${code}`, { method: 'PUT' })
|
||||
checkResponse(mfa);
|
||||
}
|
||||
|
@ -5,6 +5,9 @@ import { setAccountSearchable } from 'api/setAccountSearchable';
|
||||
import { setAccountNotifications } from 'api/setAccountNotifications';
|
||||
import { getAccountStatus } from 'api/getAccountStatus';
|
||||
import { setAccountLogin } from 'api/setAccountLogin';
|
||||
import { addAccountMFA } from 'api/addAccountMFA';
|
||||
import { setAccountMFA } from 'api/setAccountMFA';
|
||||
import { removeAccountMFA } from 'api/removeAccountMFA';
|
||||
|
||||
export function useAccountContext() {
|
||||
const [state, setState] = useState({
|
||||
@ -75,6 +78,19 @@ export function useAccountContext() {
|
||||
const { server, token } = access.current || {};
|
||||
await setAccountSearchable(server, token, flag);
|
||||
},
|
||||
enableMFA: async () => {
|
||||
const { server, token } = access.current || {};
|
||||
const secret = await addAccountMFA(server, token);
|
||||
return secret;
|
||||
},
|
||||
disableMFA: async () => {
|
||||
const { server, token } = access.current || {};
|
||||
await removeAccountMFA(server, token);
|
||||
},
|
||||
confirmMFA: async (code) => {
|
||||
const { server, token } = access.current || {};
|
||||
await setAccountMFA(server, token, code);
|
||||
},
|
||||
setAccountSeal: async (seal, key) => {
|
||||
const { guid, server, token } = access.current || {};
|
||||
await setAccountSeal(server, token, seal);
|
||||
|
@ -1,14 +1,17 @@
|
||||
import { useState } from 'react';
|
||||
import { Linking, ActivityIndicator, FlatList, KeyboardAvoidingView, Modal, ScrollView, View, Switch, Text, TextInput, TouchableOpacity, Alert } from 'react-native';
|
||||
import { Linking, ActivityIndicator, FlatList, Image, KeyboardAvoidingView, Modal, ScrollView, View, Switch, Text, TextInput, TouchableOpacity, Alert } from 'react-native';
|
||||
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { styles } from './Settings.styled';
|
||||
import { useSettings } from './useSettings.hook';
|
||||
import AntIcon from 'react-native-vector-icons/AntDesign';
|
||||
import MatIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import Colors from 'constants/Colors';
|
||||
import { BlurView } from "@react-native-community/blur";
|
||||
import { InputField } from 'utils/InputField';
|
||||
import { Logo } from 'utils/Logo';
|
||||
import { InputCode } from 'utils/InputCode';
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
|
||||
export function Settings({ drawer }) {
|
||||
|
||||
@ -99,6 +102,28 @@ export function Settings({ drawer }) {
|
||||
}
|
||||
}
|
||||
|
||||
const toggleMFA = async () => {
|
||||
if (!busy) {
|
||||
try {
|
||||
setBusy(true);
|
||||
if (state.mfaEnabled) {
|
||||
await actions.disableMFA();
|
||||
}
|
||||
else {
|
||||
await actions.enableMFA();
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
Alert.alert(
|
||||
state.strings.error,
|
||||
state.strings.tryAgain,
|
||||
);
|
||||
}
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
const deleteAccount = async () => {
|
||||
if (!busy) {
|
||||
try {
|
||||
@ -199,6 +224,20 @@ export function Settings({ drawer }) {
|
||||
<Text style={styles.optionLink}>{ state.strings.logout }</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={styles.drawerEntry} activeOpacity={1}>
|
||||
<View style={styles.icon}>
|
||||
<MatIcons name="ticket-confirmation-outline" size={20} color={Colors.text} />
|
||||
</View>
|
||||
<View style={styles.optionControl}>
|
||||
<TouchableOpacity activeOpacity={1} onPress={actions.toggleMFA}>
|
||||
<Text style={styles.optionText}>{ state.strings.mfaTitle }</Text>
|
||||
</TouchableOpacity>
|
||||
<Switch value={state.mfaEnabled} style={Platform.OS==='ios' ? styles.notifications : {}} thumbColor={Colors.sliderGrip} ios_backgroundColor={Colors.idleFill}
|
||||
trackColor={styles.track} onValueChange={actions.toggleMFA} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={styles.drawerEntry} activeOpacity={1} onPress={actions.showLogin}>
|
||||
<View style={styles.icon}>
|
||||
<MatIcons name="login" size={20} color={Colors.text} />
|
||||
@ -354,6 +393,19 @@ export function Settings({ drawer }) {
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.divider} />
|
||||
<TouchableOpacity style={styles.entry} activeOpacity={1}>
|
||||
<View style={styles.icon}>
|
||||
<MatIcons name="ticket-confirmation-outline" size={20} color={Colors.linkText} />
|
||||
</View>
|
||||
<View style={styles.optionControl}>
|
||||
<TouchableOpacity activeOpacity={1} onPress={toggleMFA}>
|
||||
<Text style={styles.optionLink}>{ state.strings.mfaTitle }</Text>
|
||||
</TouchableOpacity>
|
||||
<Switch value={state.mfaEnabled} style={Platform.OS==='ios' ? styles.notifications : {}} thumbColor={Colors.sliderGrip} ios_backgroundColor={Colors.disabledIndicator}
|
||||
trackColor={styles.track} onValueChange={toggleMFA} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.divider} />
|
||||
<TouchableOpacity style={styles.entry} activeOpacity={1} onPress={actions.showLogin}>
|
||||
<View style={styles.icon}>
|
||||
<MatIcons name="login" size={20} color={Colors.linkText} />
|
||||
@ -833,6 +885,60 @@ export function Settings({ drawer }) {
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
animationType="fade"
|
||||
transparent={true}
|
||||
visible={state.mfaModal}
|
||||
supportedOrientations={['portrait', 'landscape']}
|
||||
onRequestClose={actions.dismissMFA}
|
||||
>
|
||||
<View>
|
||||
<BlurView style={styles.mfaOverlay} blurType={Colors.overlay} blurAmount={2} reducedTransparencyFallbackColor="black" />
|
||||
<View style={styles.mfaBase}>
|
||||
<View style={styles.mfaContainer}>
|
||||
<Text style={styles.mfaTitle}>{ state.strings.mfaTitle }</Text>
|
||||
<Text style={styles.mfaDescription}>{ state.strings.mfaSteps }</Text>
|
||||
{ state.mfaImage && (
|
||||
<Image source={{ uri: state.mfaImage }} style={{ width: 128, height: 128 }} />
|
||||
)}
|
||||
{ !state.mfaImage && !state.mfaText && (
|
||||
<ActivityIndicator style={styles.modalBusy} animating={true} color={Colors.primaryButton} />
|
||||
)}
|
||||
{ state.mfaText && (
|
||||
<TouchableOpacity style={styles.mfaSecret} onPress={() => Clipboard.setString(state.mfaText)}>
|
||||
<Text style={styles.mfaText}>{ state.mfaText }</Text>
|
||||
<AntIcon style={styles.mfaIcon} name={'copy1'} size={20} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<InputCode style={{ width: '100%' }} onChangeText={actions.setCode} />
|
||||
<View style={styles.mfaError}>
|
||||
{ state.mfaError == '401' && (
|
||||
<Text style={styles.mfaErrorLabel}>{ state.strings.mfaError }</Text>
|
||||
)}
|
||||
{ state.mfaError == '429' && (
|
||||
<Text style={styles.mfaErrorLabel}>{ state.strings.mfaDisabled }</Text>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.mfaControl}>
|
||||
<TouchableOpacity style={styles.mfaCancel} onPress={actions.dismissMFA}>
|
||||
<Text style={styles.mfaCancelLabel}>{ state.strings.cancel }</Text>
|
||||
</TouchableOpacity>
|
||||
{ state.mfaCode != '' && (
|
||||
<TouchableOpacity style={styles.mfaConfirm} onPress={actions.confirmMFA}>
|
||||
<Text style={styles.mfaConfirmLabel}>{ state.strings.mfaConfirm }</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{ state.mfaCode == '' && (
|
||||
<View style={styles.mfaDisabled}>
|
||||
<Text style={styles.mfaDisabledLabel}>{ state.strings.mfaConfirm }</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -15,6 +15,112 @@ export const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
padding: 8,
|
||||
},
|
||||
mfaOverlay: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
mfaSecret: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: 4,
|
||||
},
|
||||
mfaText: {
|
||||
fontSize: 11,
|
||||
color: Colors.mainText,
|
||||
},
|
||||
mfaIcon: {
|
||||
fontSize: 14,
|
||||
color: Colors.primaryButton,
|
||||
},
|
||||
mfaError: {
|
||||
width: '100%',
|
||||
height: 24,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
mfaErrorLabel: {
|
||||
color: Colors.dangerText,
|
||||
},
|
||||
mfaControl: {
|
||||
height: 32,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
width: '100%',
|
||||
justifyContent: 'flex-end',
|
||||
gap: 16,
|
||||
},
|
||||
mfaCancel: {
|
||||
width: 72,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: Colors.cancelButton,
|
||||
borderRadius: 4,
|
||||
},
|
||||
mfaCancelLabel: {
|
||||
color: Colors.cancelButtonText,
|
||||
},
|
||||
mfaConfirm: {
|
||||
width: 72,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: Colors.primaryButton,
|
||||
borderRadius: 4,
|
||||
},
|
||||
mfaConfirmLabel: {
|
||||
color: Colors.primaryButtonText,
|
||||
},
|
||||
mfaDisabled: {
|
||||
width: 72,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: Colors.disabledButton,
|
||||
borderRadius: 4,
|
||||
},
|
||||
mfaDisabledLabel: {
|
||||
color: Colors.disabledButtonText,
|
||||
},
|
||||
mfaBase: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
mfaContainer: {
|
||||
backgroundColor: Colors.modalBase,
|
||||
borderColor: Colors.modalBorder,
|
||||
borderWidth: 1,
|
||||
width: '80%',
|
||||
maxWidth: 400,
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
alignItems: 'center',
|
||||
borderRadius: 8,
|
||||
padding: 16,
|
||||
},
|
||||
mfaTitle: {
|
||||
fontSize: 20,
|
||||
color: Colors.descriptionText,
|
||||
paddingBottom: 8,
|
||||
},
|
||||
mfaDescription: {
|
||||
fontSize: 14,
|
||||
color: Colors.descriptionText,
|
||||
textAlign: 'center',
|
||||
},
|
||||
mfaCode: {
|
||||
width: 400,
|
||||
borderWidth: 1,
|
||||
borderColor: '#333333',
|
||||
width: '100%',
|
||||
opacity: 0,
|
||||
},
|
||||
content: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
|
@ -56,6 +56,14 @@ export function useSettings() {
|
||||
contacts: [],
|
||||
topics: [],
|
||||
messages: [],
|
||||
|
||||
mfaModal: false,
|
||||
mfaEnabled: false,
|
||||
mfaSecret: false,
|
||||
mfaError: null,
|
||||
mfaCode: '',
|
||||
mfaText: null,
|
||||
mfaImage: null,
|
||||
});
|
||||
|
||||
const updateState = (value) => {
|
||||
@ -69,11 +77,11 @@ export function useSettings() {
|
||||
}, [profile.state]);
|
||||
|
||||
useEffect(() => {
|
||||
const { seal, sealable, pushEnabled } = account.state.status;
|
||||
const { seal, sealable, pushEnabled, mfaEnabled } = account.state.status;
|
||||
const sealKey = account.state.sealKey;
|
||||
const sealEnabled = seal?.publicKey != null;
|
||||
const sealUnlocked = seal?.publicKey === sealKey?.public && sealKey?.private && sealKey?.public;
|
||||
updateState({ sealable, seal, sealKey, sealEnabled, sealUnlocked, pushEnabled });
|
||||
updateState({ sealable, seal, sealKey, sealEnabled, sealUnlocked, pushEnabled, mfaEnabled });
|
||||
}, [account.state]);
|
||||
|
||||
const setCardItem = (item) => {
|
||||
@ -375,6 +383,37 @@ export function useSettings() {
|
||||
updateState({ messages: state.messages.filter(item => item.channelId !== channelId || item.topicId !== topicId) });
|
||||
}
|
||||
},
|
||||
enableMFA: async () => {
|
||||
updateState({ mfaModal: true, mfaSecret: false, mfaCode: '' });
|
||||
const mfa = await account.actions.enableMFA();
|
||||
updateState({ mfaImage: mfa.secretImage, mfaText: mfa.secretText });
|
||||
},
|
||||
disableMFA: async () => {
|
||||
updateState({ mfaEnabled: false });
|
||||
try {
|
||||
await account.actions.disableMFA();
|
||||
}
|
||||
catch (err) {
|
||||
updateState({ mfaEnabled: true });
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
confirmMFA: async () => {
|
||||
try {
|
||||
updateState({ mfaEnabled: true });
|
||||
await account.actions.confirmMFA(state.mfaCode);
|
||||
updateState({ mfaModal: false });
|
||||
}
|
||||
catch (err) {
|
||||
updateState({ mfaEnabled: false, mfaError: err.message});
|
||||
}
|
||||
},
|
||||
dismissMFA: () => {
|
||||
updateState({ mfaModal: false });
|
||||
},
|
||||
setCode: (mfaCode) => {
|
||||
updateState({ mfaCode });
|
||||
},
|
||||
};
|
||||
|
||||
return { state, actions };
|
||||
|
@ -77,15 +77,12 @@ export function useAccountContext() {
|
||||
},
|
||||
enableMFA: async () => {
|
||||
const secret = await addAccountMFA(access.current);
|
||||
console.log("SECRET ", secret);
|
||||
return secret;
|
||||
},
|
||||
disableMFA: async () => {
|
||||
await removeAccountMFA(access.current);
|
||||
},
|
||||
confirmMFA: async (code) => {
|
||||
console.log("CONFIRMING: ", code);
|
||||
|
||||
await setAccountMFA(access.current, code);
|
||||
},
|
||||
setSeal: async (seal, sealKey) => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user