diff --git a/app/mobile/src/access/admin/useAdmin.hook.js b/app/mobile/src/access/admin/useAdmin.hook.js
index 81994f2f..2aa06ef0 100644
--- a/app/mobile/src/access/admin/useAdmin.hook.js
+++ b/app/mobile/src/access/admin/useAdmin.hook.js
@@ -4,7 +4,7 @@ import { useNavigate } from 'react-router-dom';
import { AppContext } from 'context/AppContext';
import { getNodeStatus } from 'api/getNodeStatus';
import { setNodeStatus } from 'api/setNodeStatus';
-import { getNodeConfig } from 'api/getNodeConfig';
+import { setNodeAccess } from 'api/setNodeAccess';
import { getLanguageStrings } from 'constants/Strings';
export function useAdmin() {
@@ -22,6 +22,8 @@ export function useAdmin() {
version: null,
agree: false,
showTerms: false,
+
+ mfaCode: '',
});
const updateState = (value) => {
@@ -80,14 +82,13 @@ export function useAdmin() {
try {
updateState({ busy: true });
const node = state.server.trim();
- const token = state.token;
const unclaimed = await getNodeStatus(node);
if (unclaimed) {
- await setNodeStatus(node, token);
+ await setNodeStatus(node, state.token);
}
- const config = await getNodeConfig(node, token);
+ const session = await setNodeAccess(node, state.token, state.mfaCode);
updateState({ server: node, busy: false });
- navigate('/dashboard', { state: { config, server: node, token }});
+ navigate('/dashboard', { state: { server: node, token: session }});
}
catch (err) {
console.log(err);
diff --git a/app/mobile/src/api/addAdminMFAuth.js b/app/mobile/src/api/addAdminMFAuth.js
new file mode 100644
index 00000000..66f22416
--- /dev/null
+++ b/app/mobile/src/api/addAdminMFAuth.js
@@ -0,0 +1,11 @@
+import { checkResponse, fetchWithTimeout } from './fetchUtil';
+
+export async function addAdminMFAuth(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}/admin/mfauth?token=${token}`, { method: 'POST' })
+ checkResponse(mfa);
+ return mfa.json();
+}
+
diff --git a/app/mobile/src/api/getAdminMFAuth.js b/app/mobile/src/api/getAdminMFAuth.js
new file mode 100644
index 00000000..2723dd03
--- /dev/null
+++ b/app/mobile/src/api/getAdminMFAuth.js
@@ -0,0 +1,11 @@
+import { checkResponse, fetchWithTimeout } from './fetchUtil';
+
+export async function getAdminMFAuth(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}/admin/mfauth?token=${encodeURIComponent(token)}`, { method: 'GET' });
+ checkResponse(mfa);
+ return await mfa.json();
+}
+
diff --git a/app/mobile/src/api/removeAdminMFAuth.js b/app/mobile/src/api/removeAdminMFAuth.js
new file mode 100644
index 00000000..f08fde4d
--- /dev/null
+++ b/app/mobile/src/api/removeAdminMFAuth.js
@@ -0,0 +1,10 @@
+import { checkResponse, fetchWithTimeout } from './fetchUtil';
+
+export async function removeAdminMFAuth(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}/admin/mfauth?token=${token}`, { method: 'DELETE' })
+ checkResponse(mfa);
+}
+
diff --git a/app/mobile/src/api/setAdminMFAuth.js b/app/mobile/src/api/setAdminMFAuth.js
new file mode 100644
index 00000000..99b7e3b1
--- /dev/null
+++ b/app/mobile/src/api/setAdminMFAuth.js
@@ -0,0 +1,10 @@
+import { checkResponse, fetchWithTimeout } from './fetchUtil';
+
+export async function setAdminMFAuth(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}/admin/mfauth?token=${token}&code=${code}`, { method: 'PUT' })
+ checkResponse(mfa);
+}
+
diff --git a/app/mobile/src/api/setNodeAccess.js b/app/mobile/src/api/setNodeAccess.js
new file mode 100644
index 00000000..bb14e4ef
--- /dev/null
+++ b/app/mobile/src/api/setNodeAccess.js
@@ -0,0 +1,12 @@
+import { checkResponse, fetchWithTimeout } from './fetchUtil';
+
+export async function setNodeAccess(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 = code ? `&code=${code}` : '';
+
+ const access = await fetchWithTimeout(`${protocol}://${server}/admin/access?token=${encodeURIComponent(token)}${mfa}`, { method: 'PUT' });
+ checkResponse(access);
+ return access.json()
+}
+
diff --git a/app/mobile/src/constants/Strings.js b/app/mobile/src/constants/Strings.js
index f13e040b..52dfba50 100644
--- a/app/mobile/src/constants/Strings.js
+++ b/app/mobile/src/constants/Strings.js
@@ -210,6 +210,10 @@ const Strings = [
mfaDisabled: 'verification temporarily disabled',
mfaConfirm: 'Confirm',
mfaEnter: 'Enter your verification code',
+
+ disable: 'Disable',
+ confirmDisable: 'Disabling Multi-Factor Authentication',
+ disablePrompt: 'Are you sure you want to disable multi-factor authentication',
},
{
languageCode: 'fr',
@@ -416,6 +420,10 @@ const Strings = [
mfaError: 'erreur de code de vérification',
mfaDisabled: 'vérification temporairement désactivée',
mfaConfirm: 'Confirmer',
+
+ disable: 'Désactiver',
+ confirmDisable: 'Désactivation de l\'authentification multi-facteurs',
+ disablePrompt: 'Êtes-vous sûr de vouloir désactiver l\'authentification multi-facteurs',
},
{
languageCode: 'es',
@@ -623,6 +631,10 @@ const Strings = [
mfaError: 'error de código de verificación',
mfaDisabled: 'verificación temporalmente deshabilitada',
mfaConfirm: 'Confirmar',
+
+ disable: 'Desactivar',
+ confirmDisable: 'Desactivación de la autenticación de dos factores',
+ disablePrompt: '¿Estás seguro de que quieres desactivar la autenticación de dos factores?',
},
{
languageCode: 'de',
@@ -830,6 +842,10 @@ const Strings = [
mfaError: 'Verifizierungscodefehler',
mfaDisabled: 'Verifizierung vorübergehend deaktiviert',
mfaConfirm: 'Bestätigen',
+
+ disable: 'Deaktivieren',
+ confirmDisable: 'Deaktivierung der Zwei-Faktor-Authentifizierung',
+ disablePrompt: 'Sind Sie sicher, dass Sie die Zwei-Faktor-Authentifizierung deaktivieren möchten?',
},
{
languageCode: 'pt',
@@ -1022,6 +1038,10 @@ const Strings = [
mfaError: 'erro de código de verificação',
mfaDisabled: 'verificação temporariamente desativada',
mfaConfirm: 'Confirmar',
+
+ disable: 'Desativar',
+ confirmDisable: 'Desativando Autenticação de Dois Fatores',
+ disablePrompt: 'Tem certeza de que deseja desativar a autenticação de dois fatores?',
},
{
languageCode: 'ru',
@@ -1212,6 +1232,10 @@ const Strings = [
mfaError: 'ошибка проверочного кода',
mfaDisabled: 'проверка временно отключена',
mfaConfirm: 'Подтвердить',
+
+ disable: 'Отключить',
+ confirmDisable: 'Отключение двухфакторной аутентификации',
+ disablePrompt: 'Вы уверены, что хотите отключить двухфакторную аутентификацию?',
}
];
diff --git a/app/mobile/src/dashboard/Dashboard.jsx b/app/mobile/src/dashboard/Dashboard.jsx
index 88b58047..15e82e62 100644
--- a/app/mobile/src/dashboard/Dashboard.jsx
+++ b/app/mobile/src/dashboard/Dashboard.jsx
@@ -1,4 +1,4 @@
-import { ScrollView, TextInput, Alert, Switch, TouchableOpacity, View, Text, Modal, FlatList, KeyboardAvoidingView } from 'react-native';
+import { ScrollView, Image, TextInput, Alert, Switch, ActivityIndicator, TouchableOpacity, View, Text, Modal, FlatList, KeyboardAvoidingView } from 'react-native';
import Clipboard from '@react-native-clipboard/clipboard';
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
import AntIcon from 'react-native-vector-icons/AntDesign';
@@ -9,12 +9,50 @@ import { useDashboard } from './useDashboard.hook';
import { Logo } from 'utils/Logo';
import { BlurView } from "@react-native-community/blur";
import { InputField } from 'utils/InputField';
+import Colors from 'constants/Colors';
+import { InputCode } from 'utils/InputCode';
export function Dashboard(props) {
const location = useLocation();
- const { config, server, token } = location.state;
- const { state, actions } = useDashboard(config, server, token);
+ const { server, token } = location.state;
+ const { state, actions } = useDashboard(server, token);
+
+ const enableMFA = async () => {
+ try {
+ await actions.enableMFA();
+ }
+ catch (err) {
+ Alert.alert(
+ state.strings.error,
+ state.strings.tryAgain,
+ );
+ }
+ }
+
+ const disableMFA = async () => {
+ try {
+ await actions.disableMFA();
+ }
+ catch (err) {
+ Alert.alert(
+ state.strings.error,
+ state.strings.tryAgain,
+ );
+ }
+ }
+
+ const confirmMFA = async () => {
+ try {
+ await actions.confirmMFA();
+ }
+ catch (err) {
+ Alert.alert(
+ state.strings.error,
+ state.strings.tryAgain,
+ );
+ }
+ }
const saveConfig = async () => {
try {
@@ -24,8 +62,8 @@ export function Dashboard(props) {
catch (err) {
console.log(err);
Alert.alert(
- 'Failed to Save Settings',
- 'Please try again.',
+ state.strings.error,
+ state.strings.tryAgain,
);
}
}
@@ -37,8 +75,8 @@ export function Dashboard(props) {
catch (err) {
console.log(err);
Alert.alert(
- 'Failed to Generate Access Token',
- 'Please try again.',
+ state.strings.error,
+ state.strings.tryAgain,
);
}
}
@@ -50,8 +88,8 @@ export function Dashboard(props) {
catch (err) {
console.log(err);
Alert.alert(
- 'Failed to Generate Access Token',
- 'Please try again.',
+ state.strings.error,
+ state.strings.tryAgain,
);
}
}
@@ -67,8 +105,8 @@ export function Dashboard(props) {
catch (err) {
console.log(err);
Alert.alert(
- 'Failed to Update Account',
- 'Please try again.',
+ state.strings.error,
+ state.strings.tryAgain,
);
}
}
@@ -83,6 +121,16 @@ export function Dashboard(props) {
+ { !state.mfaEnabled && (
+
+
+
+ )}
+ { state.mfaEnabled && (
+
+
+
+ )}
@@ -340,6 +388,60 @@ export function Dashboard(props) {
+
+
+
+
+
+ { state.strings.mfaTitle }
+ { state.strings.mfaSteps }
+ { state.mfaImage && (
+
+ )}
+ { !state.mfaImage && !state.mfaText && (
+
+ )}
+ { state.mfaText && (
+ Clipboard.setString(state.mfaText)}>
+ { state.mfaText }
+
+
+ )}
+
+
+ { state.mfaError == '401' && (
+ { state.strings.mfaError }
+ )}
+ { state.mfaError == '429' && (
+ { state.strings.mfaDisabled }
+ )}
+
+
+
+ { state.strings.cancel }
+
+ { state.mfaCode != '' && (
+
+ { state.strings.mfaConfirm }
+
+ )}
+ { state.mfaCode == '' && (
+
+ { state.strings.mfaConfirm }
+
+ )}
+
+
+
+
+
+
)
}
diff --git a/app/mobile/src/dashboard/Dashboard.styled.js b/app/mobile/src/dashboard/Dashboard.styled.js
index a6aa36fa..40b6e1b7 100644
--- a/app/mobile/src/dashboard/Dashboard.styled.js
+++ b/app/mobile/src/dashboard/Dashboard.styled.js
@@ -302,4 +302,110 @@ export const styles = StyleSheet.create({
marginBottom: 8,
},
},
+ mfaOverlay: {
+ width: '100%',
+ height: '100%',
+ },
+ mfaSecret: {
+ display: 'flex',
+ flexDirection: 'row',
+ gap: 4,
+ },
+ mfaText: {
+ fontSize: 11,
+ color: Colors.labelText,
+ },
+ 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,
+ },
});
diff --git a/app/mobile/src/dashboard/useDashboard.hook.js b/app/mobile/src/dashboard/useDashboard.hook.js
index 1237c4e9..2342505d 100644
--- a/app/mobile/src/dashboard/useDashboard.hook.js
+++ b/app/mobile/src/dashboard/useDashboard.hook.js
@@ -15,11 +15,15 @@ import { addAccountAccess } from 'api/addAccountAccess';
import { DisplayContext } from 'context/DisplayContext';
import { getLanguageStrings } from 'constants/Strings';
-export function useDashboard(config, server, token) {
+import { getAdminMFAuth } from 'api/getAdminMFAuth';
+import { addAdminMFAuth } from 'api/addAdminMFAuth';
+import { setAdminMFAuth } from 'api/setAdminMFAuth';
+import { removeAdminMFAuth } from 'api/removeAdminMFAuth';
+
+export function useDashboard(server, token) {
const [state, setState] = useState({
strings: getLanguageStrings(),
- config: null,
accounts: [],
editConfig: false,
addUser: false,
@@ -40,6 +44,13 @@ export function useDashboard(config, server, token) {
iceUrl: null,
iceUsername: null,
icePassword: null,
+
+ mfaModal: false,
+ mfaImage: null,
+ mfaText: null,
+ mfaCode: '',
+ mfaError: null,
+ mfaEnabled: false,
});
const navigate = useNavigate();
@@ -62,19 +73,24 @@ export function useDashboard(config, server, token) {
return { logo, name, handle, accountId, disabled };
}
- const refreshAccounts = async () => {
- const accounts = await getNodeAccounts(server, token);
- updateState({ accounts: accounts.map(setAccountItem) });
- };
-
- useEffect(() => {
- const { keyType, accountStorage, domain, enableImage, enableAudio, enableVideo, enableBinary, transformSupported, allowUnsealed, pushSupported, enableIce, iceUrl, iceUsername, icePassword } = config;
+ const syncNode = async () => {
+ const mfaEnabled = await getAdminMFAuth(server, token);
+ const config = await getNodeConfig(server, token);
+ const nodeAccounts = await getNodeAccounts(server, token);
+ const accounts = nodeAccounts.map(setAccountItem);
+ const { keyType, accountStorage, domain, enableImage, enableAudio, enableVideo, enableBinary, transformSupported, allowUnsealed, pushSupported, enableIce, iceUrl, iceUsername, icePassword } = config || {};
const storage = Math.ceil(accountStorage / 1073741824);
- updateState({ keyType, storage: storage.toString(), domain, enableImage, enableAudio, enableVideo, enableBinary, transformSupported, allowUnsealed, pushSupported, enableIce, iceUrl, iceUsername, icePassword });
- }, [config]);
+ updateState({ keyType, storage: storage.toString(), domain, enableImage, enableAudio, enableVideo, enableBinary, transformSupported, allowUnsealed, pushSupported, enableIce, iceUrl, iceUsername, icePassword, accounts, mfaEnabled });
+ }
+
+ const refreshAccounts = async () => {
+ const nodeAccounts = await getNodeAccounts(server, token);
+ const accounts = nodeAccounts.map(setAccountItem);
+ updateState({ accounts });
+ }
useEffect(() => {
- refreshAccounts();
+ syncNode();
}, []);
const actions = {
@@ -155,8 +171,8 @@ export function useDashboard(config, server, token) {
},
promptRemove: (accountId) => {
display.actions.showPrompt({
- title: 'Delete User',
- ok: { label: 'Delete', action: async () => {
+ title: state.strings.deleteAccount,
+ ok: { label: state.strings.delete, action: async () => {
await removeAccount(server, token, accountId);
await refreshAccounts();
} , failed: () => {
@@ -168,6 +184,41 @@ export function useDashboard(config, server, token) {
cancel: { label: state.strings.cancel },
});
},
+ enableMFA: async () => {
+ updateState({ mfaModal: true, mfaImage: null, mfaText: null, mfaCode: '', mfaError: '' });
+ const mfa = await addAdminMFAuth(server, token);
+ updateState({ mfaImage: mfa.secretImage, mfaText: mfa.secretText });
+ },
+ disableMFA: async () => {
+ display.actions.showPrompt({
+ title: state.strings.confirmDisable,
+ ok: { label: state.strings.disable, action: async () => {
+ await removeAdminMFAuth(server, token);
+ updateState({ mfaEnabled: false });
+ } , failed: () => {
+ Alert.alert(
+ state.strings.error,
+ state.strings.tryAgain,
+ );
+ }},
+ cancel: { label: state.strings.cancel },
+ });
+ },
+ confirmMFA: async () => {
+ try {
+ await setAdminMFAuth(server, token, state.mfaCode);
+ updateState({ mfaEnabled: true, mfaModal: false });
+ }
+ catch (err) {
+ updateState({ mfaError: err.message});
+ }
+ },
+ dismissMFA: () => {
+ updateState({ mfaModal: false });
+ },
+ setCode: (mfaCode) => {
+ updateState({ mfaCode });
+ },
};
return { state, actions };