diff --git a/app/mobile/src/access/login/Login.jsx b/app/mobile/src/access/login/Login.jsx
index f46b3055..ad64f954 100644
--- a/app/mobile/src/access/login/Login.jsx
+++ b/app/mobile/src/access/login/Login.jsx
@@ -6,6 +6,7 @@ import { Colors } from 'constants/Colors';
import MatIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { tos } from 'constants/TermsOfService';
import { BlurView } from "@react-native-community/blur";
+import { InputCode } from 'utils/InputCode';
export function Login() {
@@ -132,14 +133,38 @@ export function Login() {
supportedOrientations={['portrait', 'landscape']}
onRequestClose={actions.dismissMFA}
>
-
+
-
+
- Multi-Factor Authentication
- Enter your verification code
+ { state.strings.mfaTitle }
+ { state.strings.mfaEnter }
+
+
+ { state.mfaError == '403' && (
+ { 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/access/login/Login.styled.js b/app/mobile/src/access/login/Login.styled.js
index 34401765..cf2274db 100644
--- a/app/mobile/src/access/login/Login.styled.js
+++ b/app/mobile/src/access/login/Login.styled.js
@@ -26,6 +26,56 @@ export const styles = StyleSheet.create({
width: '100%',
height: '100%',
},
+ 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,
@@ -43,16 +93,26 @@ export const styles = StyleSheet.create({
width: '80%',
maxWidth: 400,
display: 'flex',
- gap: 16,
+ gap: 8,
alignItems: 'center',
borderRadius: 8,
padding: 16,
},
mfaTitle: {
fontSize: 20,
+ color: Colors.descriptionText,
+ paddingBottom: 8,
},
mfaDescription: {
- fontSize: 16,
+ fontSize: 14,
+ color: Colors.descriptionText,
+ },
+ mfaCode: {
+ width: 400,
+ borderWidth: 1,
+ borderColor: '#333333',
+ width: '100%',
+ opacity: 0,
},
tos: {
display: 'flex',
diff --git a/app/mobile/src/access/login/useLogin.hook.js b/app/mobile/src/access/login/useLogin.hook.js
index cf4697b2..1930fe19 100644
--- a/app/mobile/src/access/login/useLogin.hook.js
+++ b/app/mobile/src/access/login/useLogin.hook.js
@@ -19,7 +19,7 @@ export function useLogin() {
agree: false,
showTerms: false,
mfaModal: false,
- mfaCode: null,
+ mfaCode: '',
mfaError: null,
});
@@ -77,11 +77,12 @@ export function useLogin() {
if (!state.busy) {
updateState({ busy: true });
try {
- await app.actions.login(state.login.trim(), state.password);
+ await app.actions.login(state.login.trim(), state.password, state.mfaCode);
}
catch (err) {
+console.log(err);
if (err.message == '405' || err.message == '403' || err.message == '429') {
- updateState({ mfaModal: true, mfaError: err.message, mfaCode: '' });
+ updateState({ mfaModal: true, mfaError: err.message });
}
else {
console.log(err.message);
@@ -92,6 +93,9 @@ export function useLogin() {
updateState({ busy: false });
}
},
+ setCode: (mfaCode) => {
+ updateState({ mfaCode });
+ },
dismissMFA: () => {
updateState({ mfaModal: false });
},
diff --git a/app/mobile/src/api/fetchUtil.js b/app/mobile/src/api/fetchUtil.js
index dc230514..853735e7 100644
--- a/app/mobile/src/api/fetchUtil.js
+++ b/app/mobile/src/api/fetchUtil.js
@@ -8,6 +8,7 @@ export function createWebsocket(url) {
export function checkResponse(response) {
if(response.status >= 400 && response.status < 600) {
+ console.log(`${response.url} failed [${response?.status}]`);
throw new Error(response.status);
}
}
diff --git a/app/mobile/src/api/setLogin.js b/app/mobile/src/api/setLogin.js
index dc2cf15c..0550ecc1 100644
--- a/app/mobile/src/api/setLogin.js
+++ b/app/mobile/src/api/setLogin.js
@@ -1,13 +1,14 @@
import { checkResponse, fetchWithTimeout } from './fetchUtil';
import base64 from 'react-native-base64'
-export async function setLogin(username, server, password, appName, appVersion, platform, deviceToken, pushType, notifications) {
+export async function setLogin(username, server, password, code, appName, appVersion, platform, deviceToken, pushType, notifications) {
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}` : '';
let headers = new Headers()
headers.append('Authorization', 'Basic ' + base64.encode(username + ":" + password));
- let login = await fetchWithTimeout(`${protocol}://${server}/account/apps?appName=${appName}&appVersion=${appVersion}&platform=${platform}&deviceToken=${deviceToken}&pushType=${pushType}`, { method: 'POST', body: JSON.stringify(notifications), headers: headers })
+ let login = await fetchWithTimeout(`${protocol}://${server}/account/apps?appName=${appName}&appVersion=${appVersion}&platform=${platform}&deviceToken=${deviceToken}&pushType=${pushType}${mfa}`, { method: 'POST', body: JSON.stringify(notifications), headers: headers })
checkResponse(login)
return await login.json()
}
diff --git a/app/mobile/src/constants/Strings.js b/app/mobile/src/constants/Strings.js
index 18b48531..c0467e47 100644
--- a/app/mobile/src/constants/Strings.js
+++ b/app/mobile/src/constants/Strings.js
@@ -203,6 +203,13 @@ const Strings = [
reportMessage: 'Report Message',
select: 'Select',
selectTopic: 'Select Topic for Sharing',
+
+ mfaTitle: 'Multi-Factor Authentication',
+ mfaSteps: 'Store the secret and confirm the verification code',
+ mfaError: 'verification code error',
+ mfaDisabled: 'verification temporarily disabled',
+ mfaConfirm: 'Confirm',
+ mfaEnter: 'Enter your verification code',
},
{
languageCode: 'fr',
@@ -402,6 +409,13 @@ const Strings = [
reportMessage: 'Signaler le Message',
select: 'Choisir',
selectTopic: 'Choisissez le sujet à partager',
+
+ mfaTitle: 'Authentification Multi-Factor',
+ mfaSteps: 'Enregistrez le secret et confirmez le code de vérification',
+ mfaEnter: 'Entrez votre code de vérification',
+ mfaError: 'erreur de code de vérification',
+ mfaDisabled: 'vérification temporairement désactivée',
+ mfaConfirm: 'Confirmer',
},
{
languageCode: 'es',
@@ -602,6 +616,13 @@ const Strings = [
select: 'Elegir',
selectTopic: 'Elija un tema para compartir',
+
+ mfaTitle: 'Autenticación de Dos Factores',
+ mfaSteps: 'Guarde el secreto y confirme el código de verificación',
+ mfaEnter: 'Ingresa tu código de verificación',
+ mfaError: 'error de código de verificación',
+ mfaDisabled: 'verificación temporalmente deshabilitada',
+ mfaConfirm: 'Confirmar',
},
{
languageCode: 'de',
@@ -802,6 +823,13 @@ const Strings = [
select: 'Wählen',
selectTopic: 'Wählen Sie ein Thema zum Teilen aus',
+
+ mfaTitle: 'Zwei-Faktor-Authentifizierung',
+ mfaSteps: 'Speichern Sie das Geheimnis und bestätigen Sie den Verifizierungscode',
+ mfaEnter: 'Geben Sie Ihren Bestätigungs-Code ein',
+ mfaError: 'Verifizierungscodefehler',
+ mfaDisabled: 'Verifizierung vorübergehend deaktiviert',
+ mfaConfirm: 'Bestätigen',
},
{
languageCode: 'pt',
@@ -987,6 +1015,13 @@ const Strings = [
select: 'Escolher',
selectTopic: 'Escolha o tópico para compartilhar',
+
+ mfaTitle: 'Autenticação de Dois Fatores',
+ mfaSteps: 'Salve o segredo e confirme o código de verificação',
+ mfaEnter: 'Digite seu código de verificação',
+ mfaError: 'erro de código de verificação',
+ mfaDisabled: 'verificação temporariamente desativada',
+ mfaConfirm: 'Confirmar',
},
{
languageCode: 'ru',
@@ -1170,6 +1205,13 @@ const Strings = [
select: 'выбирать',
selectTopic: 'Выберите тему для обмена',
+
+ mfaTitle: 'Двухфакторная аутентификация',
+ mfaSteps: 'Сохраните секрет и подтвердите проверочный код',
+ mfaEnter: 'Введите Ваш верификационный код',
+ mfaError: 'ошибка проверочного кода',
+ mfaDisabled: 'проверка временно отключена',
+ mfaConfirm: 'Подтвердить',
}
];
diff --git a/app/mobile/src/context/useAppContext.hook.js b/app/mobile/src/context/useAppContext.hook.js
index abbdfaa6..13246365 100644
--- a/app/mobile/src/context/useAppContext.hook.js
+++ b/app/mobile/src/context/useAppContext.hook.js
@@ -121,7 +121,7 @@ export function useAppContext() {
await setDeviceToken();
updateState({ loggedOut: false });
await addAccount(server, username, password, token);
- const session = await setLogin(username, server, password, getApplicationName(), getVersion(), getDeviceId(), deviceToken.current, pushType.current, notifications)
+ const session = await setLogin(username, server, password, null, getApplicationName(), getVersion(), getDeviceId(), deviceToken.current, pushType.current, notifications)
access.current = { loginTimestamp: session.created, server, token: session.appToken, guid: session.guid };
await store.actions.setSession(access.current);
await setSession();
@@ -143,14 +143,14 @@ export function useAppContext() {
messaging().requestPermission().then(status => {})
}
},
- login: async (username, password) => {
+ login: async (username, password, code) => {
if (!init.current || access.current) {
throw new Error('invalid session state');
}
await setDeviceToken();
updateState({ loggedOut: false });
const acc = username.includes('/') ? username.split('/') : username.split('@');
- const session = await setLogin(acc[0], acc[1], password, getApplicationName(), getVersion(), getDeviceId(), deviceToken.current, pushType.current, notifications)
+ const session = await setLogin(acc[0], acc[1], password, code, getApplicationName(), getVersion(), getDeviceId(), deviceToken.current, pushType.current, notifications)
access.current = { loginTimestamp: session.created, server: acc[1], token: session.appToken, guid: session.guid };
await store.actions.setSession(access.current);
await setSession();
diff --git a/app/mobile/src/utils/InputCode.jsx b/app/mobile/src/utils/InputCode.jsx
new file mode 100644
index 00000000..612b7533
--- /dev/null
+++ b/app/mobile/src/utils/InputCode.jsx
@@ -0,0 +1,46 @@
+import { TextInput, Text, View, TouchableOpacity } from 'react-native';
+import { useState } from 'react';
+
+export function InputCode({ onChangeText, style }) {
+
+ const [code, setCode] = useState('');
+
+ const updateCode = (value) => {
+ if (value.length >= 6) {
+ onChangeText(value.slice(0, 6));
+ }
+ else {
+ onChangeText('');
+ }
+ setCode(value.slice(0, 6));
+ }
+
+ return (
+
+
+
+
+ { code.charAt(0) }
+
+
+ { code.charAt(1) }
+
+
+ { code.charAt(2) }
+
+
+ { code.charAt(3) }
+
+
+ { code.charAt(4) }
+
+
+ { code.charAt(5) }
+
+
+
+
+
+ );
+}
+