mirror of
https://github.com/balzack/databag.git
synced 2025-02-12 03:29:16 +00:00
adding mfa login for mobile
This commit is contained in:
parent
0d9fd2724e
commit
97f375472e
@ -6,6 +6,7 @@ import { Colors } from 'constants/Colors';
|
|||||||
import MatIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
import MatIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||||
import { tos } from 'constants/TermsOfService';
|
import { tos } from 'constants/TermsOfService';
|
||||||
import { BlurView } from "@react-native-community/blur";
|
import { BlurView } from "@react-native-community/blur";
|
||||||
|
import { InputCode } from 'utils/InputCode';
|
||||||
|
|
||||||
export function Login() {
|
export function Login() {
|
||||||
|
|
||||||
@ -132,14 +133,38 @@ export function Login() {
|
|||||||
supportedOrientations={['portrait', 'landscape']}
|
supportedOrientations={['portrait', 'landscape']}
|
||||||
onRequestClose={actions.dismissMFA}
|
onRequestClose={actions.dismissMFA}
|
||||||
>
|
>
|
||||||
<View style={styles.mfaOverlay}>
|
<View>
|
||||||
<BlurView style={styles.mfaOverlay} blurType={Colors.overlay} blurAmount={2} reducedTransparencyFallbackColor="black" />
|
<BlurView style={styles.mfaOverlay} blurType={Colors.overlay} blurAmount={2} reducedTransparencyFallbackColor="black" />
|
||||||
<KeyboardAvoidingView style={styles.mfaBase} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}>
|
<View style={styles.mfaBase}>
|
||||||
<View style={styles.mfaContainer}>
|
<View style={styles.mfaContainer}>
|
||||||
<Text style={styles.mfaTitle}>Multi-Factor Authentication</Text>
|
<Text style={styles.mfaTitle}>{ state.strings.mfaTitle }</Text>
|
||||||
<Text style={styles.mfaDescription}>Enter your verification code</Text>
|
<Text style={styles.mfaDescription}>{ state.strings.mfaEnter }</Text>
|
||||||
|
<InputCode style={{ width: '100%' }} onChangeText={actions.setCode} />
|
||||||
|
<View style={styles.mfaError}>
|
||||||
|
{ state.mfaError == '403' && (
|
||||||
|
<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.login}>
|
||||||
|
<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>
|
||||||
</KeyboardAvoidingView>
|
|
||||||
</View>
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
@ -26,6 +26,56 @@ export const styles = StyleSheet.create({
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
height: '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: {
|
mfaBase: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
@ -43,16 +93,26 @@ export const styles = StyleSheet.create({
|
|||||||
width: '80%',
|
width: '80%',
|
||||||
maxWidth: 400,
|
maxWidth: 400,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: 16,
|
gap: 8,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
},
|
},
|
||||||
mfaTitle: {
|
mfaTitle: {
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
|
color: Colors.descriptionText,
|
||||||
|
paddingBottom: 8,
|
||||||
},
|
},
|
||||||
mfaDescription: {
|
mfaDescription: {
|
||||||
fontSize: 16,
|
fontSize: 14,
|
||||||
|
color: Colors.descriptionText,
|
||||||
|
},
|
||||||
|
mfaCode: {
|
||||||
|
width: 400,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#333333',
|
||||||
|
width: '100%',
|
||||||
|
opacity: 0,
|
||||||
},
|
},
|
||||||
tos: {
|
tos: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
@ -19,7 +19,7 @@ export function useLogin() {
|
|||||||
agree: false,
|
agree: false,
|
||||||
showTerms: false,
|
showTerms: false,
|
||||||
mfaModal: false,
|
mfaModal: false,
|
||||||
mfaCode: null,
|
mfaCode: '',
|
||||||
mfaError: null,
|
mfaError: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -77,11 +77,12 @@ export function useLogin() {
|
|||||||
if (!state.busy) {
|
if (!state.busy) {
|
||||||
updateState({ busy: true });
|
updateState({ busy: true });
|
||||||
try {
|
try {
|
||||||
await app.actions.login(state.login.trim(), state.password);
|
await app.actions.login(state.login.trim(), state.password, state.mfaCode);
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
|
console.log(err);
|
||||||
if (err.message == '405' || err.message == '403' || err.message == '429') {
|
if (err.message == '405' || err.message == '403' || err.message == '429') {
|
||||||
updateState({ mfaModal: true, mfaError: err.message, mfaCode: '' });
|
updateState({ mfaModal: true, mfaError: err.message });
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
console.log(err.message);
|
console.log(err.message);
|
||||||
@ -92,6 +93,9 @@ export function useLogin() {
|
|||||||
updateState({ busy: false });
|
updateState({ busy: false });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setCode: (mfaCode) => {
|
||||||
|
updateState({ mfaCode });
|
||||||
|
},
|
||||||
dismissMFA: () => {
|
dismissMFA: () => {
|
||||||
updateState({ mfaModal: false });
|
updateState({ mfaModal: false });
|
||||||
},
|
},
|
||||||
|
@ -8,6 +8,7 @@ export function createWebsocket(url) {
|
|||||||
|
|
||||||
export function checkResponse(response) {
|
export function checkResponse(response) {
|
||||||
if(response.status >= 400 && response.status < 600) {
|
if(response.status >= 400 && response.status < 600) {
|
||||||
|
console.log(`${response.url} failed [${response?.status}]`);
|
||||||
throw new Error(response.status);
|
throw new Error(response.status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import { checkResponse, fetchWithTimeout } from './fetchUtil';
|
import { checkResponse, fetchWithTimeout } from './fetchUtil';
|
||||||
import base64 from 'react-native-base64'
|
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 insecure = /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|:\d+$|$)){4}$/.test(server);
|
||||||
const protocol = insecure ? 'http' : 'https';
|
const protocol = insecure ? 'http' : 'https';
|
||||||
|
const mfa = code ? `&code=${code}` : '';
|
||||||
|
|
||||||
let headers = new Headers()
|
let headers = new Headers()
|
||||||
headers.append('Authorization', 'Basic ' + base64.encode(username + ":" + password));
|
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)
|
checkResponse(login)
|
||||||
return await login.json()
|
return await login.json()
|
||||||
}
|
}
|
||||||
|
@ -203,6 +203,13 @@ const Strings = [
|
|||||||
reportMessage: 'Report Message',
|
reportMessage: 'Report Message',
|
||||||
select: 'Select',
|
select: 'Select',
|
||||||
selectTopic: 'Select Topic for Sharing',
|
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',
|
languageCode: 'fr',
|
||||||
@ -402,6 +409,13 @@ const Strings = [
|
|||||||
reportMessage: 'Signaler le Message',
|
reportMessage: 'Signaler le Message',
|
||||||
select: 'Choisir',
|
select: 'Choisir',
|
||||||
selectTopic: 'Choisissez le sujet à partager',
|
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',
|
languageCode: 'es',
|
||||||
@ -602,6 +616,13 @@ const Strings = [
|
|||||||
|
|
||||||
select: 'Elegir',
|
select: 'Elegir',
|
||||||
selectTopic: 'Elija un tema para compartir',
|
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',
|
languageCode: 'de',
|
||||||
@ -802,6 +823,13 @@ const Strings = [
|
|||||||
|
|
||||||
select: 'Wählen',
|
select: 'Wählen',
|
||||||
selectTopic: 'Wählen Sie ein Thema zum Teilen aus',
|
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',
|
languageCode: 'pt',
|
||||||
@ -987,6 +1015,13 @@ const Strings = [
|
|||||||
|
|
||||||
select: 'Escolher',
|
select: 'Escolher',
|
||||||
selectTopic: 'Escolha o tópico para compartilhar',
|
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',
|
languageCode: 'ru',
|
||||||
@ -1170,6 +1205,13 @@ const Strings = [
|
|||||||
|
|
||||||
select: 'выбирать',
|
select: 'выбирать',
|
||||||
selectTopic: 'Выберите тему для обмена',
|
selectTopic: 'Выберите тему для обмена',
|
||||||
|
|
||||||
|
mfaTitle: 'Двухфакторная аутентификация',
|
||||||
|
mfaSteps: 'Сохраните секрет и подтвердите проверочный код',
|
||||||
|
mfaEnter: 'Введите Ваш верификационный код',
|
||||||
|
mfaError: 'ошибка проверочного кода',
|
||||||
|
mfaDisabled: 'проверка временно отключена',
|
||||||
|
mfaConfirm: 'Подтвердить',
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -121,7 +121,7 @@ export function useAppContext() {
|
|||||||
await setDeviceToken();
|
await setDeviceToken();
|
||||||
updateState({ loggedOut: false });
|
updateState({ loggedOut: false });
|
||||||
await addAccount(server, username, password, token);
|
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 };
|
access.current = { loginTimestamp: session.created, server, token: session.appToken, guid: session.guid };
|
||||||
await store.actions.setSession(access.current);
|
await store.actions.setSession(access.current);
|
||||||
await setSession();
|
await setSession();
|
||||||
@ -143,14 +143,14 @@ export function useAppContext() {
|
|||||||
messaging().requestPermission().then(status => {})
|
messaging().requestPermission().then(status => {})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
login: async (username, password) => {
|
login: async (username, password, code) => {
|
||||||
if (!init.current || access.current) {
|
if (!init.current || access.current) {
|
||||||
throw new Error('invalid session state');
|
throw new Error('invalid session state');
|
||||||
}
|
}
|
||||||
await setDeviceToken();
|
await setDeviceToken();
|
||||||
updateState({ loggedOut: false });
|
updateState({ loggedOut: false });
|
||||||
const acc = username.includes('/') ? username.split('/') : username.split('@');
|
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 };
|
access.current = { loginTimestamp: session.created, server: acc[1], token: session.appToken, guid: session.guid };
|
||||||
await store.actions.setSession(access.current);
|
await store.actions.setSession(access.current);
|
||||||
await setSession();
|
await setSession();
|
||||||
|
46
app/mobile/src/utils/InputCode.jsx
Normal file
46
app/mobile/src/utils/InputCode.jsx
Normal file
@ -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 (
|
||||||
|
<View style={style}>
|
||||||
|
<View style={{ width: '100%', height: 32 }}>
|
||||||
|
<View style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'row', justifyContent: 'space-between' }}>
|
||||||
|
<View style={{ width: 32, height: '100%', borderWidth: 1, borderRadius: 4, backgroundColor: '#dddddd', borderColor: '#aaaaaa', alignItems: 'center', justifyContent: 'center', display: 'flex' }}>
|
||||||
|
<Text style={{ fontSize: 20 }}>{ code.charAt(0) }</Text>
|
||||||
|
</View>
|
||||||
|
<View style={{ width: 32, height: '100%', borderWidth: 1, borderRadius: 4, backgroundColor: '#dddddd', borderColor: '#aaaaaa', alignItems: 'center', justifyContent: 'center', display: 'flex' }}>
|
||||||
|
<Text style={{ fontSize: 20 }}>{ code.charAt(1) }</Text>
|
||||||
|
</View>
|
||||||
|
<View style={{ width: 32, height: '100%', borderWidth: 1, borderRadius: 4, backgroundColor: '#dddddd', borderColor: '#aaaaaa', alignItems: 'center', justifyContent: 'center', display: 'flex' }}>
|
||||||
|
<Text style={{ fontSize: 20 }}>{ code.charAt(2) }</Text>
|
||||||
|
</View>
|
||||||
|
<View style={{ width: 32, height: '100%', borderWidth: 1, borderRadius: 4, backgroundColor: '#dddddd', borderColor: '#aaaaaa', alignItems: 'center', justifyContent: 'center', display: 'flex' }}>
|
||||||
|
<Text style={{ fontSize: 20 }}>{ code.charAt(3) }</Text>
|
||||||
|
</View>
|
||||||
|
<View style={{ width: 32, height: '100%', borderWidth: 1, borderRadius: 4, backgroundColor: '#dddddd', borderColor: '#aaaaaa', alignItems: 'center', justifyContent: 'center', display: 'flex' }}>
|
||||||
|
<Text style={{ fontSize: 20 }}>{ code.charAt(4) }</Text>
|
||||||
|
</View>
|
||||||
|
<View style={{ width: 32, height: '100%', borderWidth: 1, borderRadius: 4, backgroundColor: '#dddddd', borderColor: '#aaaaaa', alignItems: 'center', justifyContent: 'center', display: 'flex' }}>
|
||||||
|
<Text style={{ fontSize: 20 }}>{ code.charAt(5) }</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<TextInput style={{ width: '100%', height: '100%', opacity: 0, position: 'absolute', top: 0, left: 0 }} onChangeText={updateCode} autoCorrect={false} autoCapitalize="none" maxLength={6} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user