Skip to content

Commit

Permalink
feat(startup): send signin verification code to email
Browse files Browse the repository at this point in the history
  • Loading branch information
rhahao committed Mar 8, 2023
1 parent c07bce8 commit b7c1a50
Show file tree
Hide file tree
Showing 8 changed files with 383 additions and 11 deletions.
123 changes: 123 additions & 0 deletions src/api/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,129 @@ export const apiHandleVerifyOTP = async (userOTP, isSetup, trustedDevice) => {
}
};

export const apiHandleVerifyEmailOTP = async (userOTP) => {
try {
const { t } = getI18n();

const { apiHost, visitorID } = await getProfile();

const auth = await getAuth();
const user = auth.currentUser;

if (userOTP.length === 6) {
if (apiHost !== '') {
const res = await fetch(`${apiHost}verify-otp-code`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
visitorid: visitorID,
uid: user.uid,
},
body: JSON.stringify({ code: userOTP }),
});

const data = await res.json();

if (res.status !== 200) {
if (data.message) {
if (data.message === 'TOKEN_INVALID') data.message = t('mfaTokenInvalidExpired', { ns: 'ui' });
if (data.message === 'EMAIL_OTP_INVALID') data.message = t('emailOTPInvalidExpired', { ns: 'ui' });
await promiseSetRecoil(appMessageState, data.message);
await promiseSetRecoil(appSeverityState, 'warning');
await promiseSetRecoil(appSnackOpenState, true);
return {};
}
}

const { id, cong_id, cong_name, cong_role, cong_number, pocket_members } = data;

if (cong_name.length === 0) return { createCongregation: true };

if (cong_role.length === 0) return { unauthorized: true };

if (!cong_role.includes('lmmo') && !cong_role.includes('lmmo-backup')) return { unauthorized: true };

backupWorkerInstance.setCongID(cong_id);
await promiseSetRecoil(congIDState, cong_id);

const settings = await dbGetAppSettings();
if (settings.isCongUpdated2 === undefined) {
return { updateCongregation: true };
}

if (cong_role.includes('admin')) {
await promiseSetRecoil(isAdminCongState, true);
}

const isMainDb = await isDbExist('cpe_sws');
if (!isMainDb) await initAppDb();

// save congregation update if any
let obj = {};
obj.username = data.username;
obj.cong_name = cong_name;
obj.cong_number = cong_number;
obj.isLoggedOut = false;
obj.pocket_members = pocket_members;
obj.cong_role = cong_role;
obj.account_type = 'vip';
await dbUpdateAppSettings(obj);

await promiseSetRecoil(userIDState, id);
await promiseSetRecoil(pocketMembersState, pocket_members);
await promiseSetRecoil(accountTypeState, 'vip');
await promiseSetRecoil(congRoleState, cong_role);

await loadApp();

return { success: true };
}
}
} catch (err) {
await promiseSetRecoil(appMessageState, err.message);
await promiseSetRecoil(appSeverityState, 'error');
await promiseSetRecoil(appSnackOpenState, true);
return {};
}
};

export const apiRequestTempOTPCode = async (uid) => {
const { t } = getI18n();

try {
const { apiHost, appLang, visitorID } = await getProfile();

if (apiHost !== '' && uid !== '') {
const res = await fetch(`${apiHost}request-otp-code`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
applanguage: appLang,
uid: uid,
visitorid: visitorID,
},
});

const data = await res.json();
if (res.status === 200) {
return { success: true };
} else {
if (data.message) {
await promiseSetRecoil(appMessageState, data.message);
await promiseSetRecoil(appSeverityState, 'warning');
await promiseSetRecoil(appSnackOpenState, true);
return {};
}
}
}
} catch (err) {
await promiseSetRecoil(appMessageState, t('sendEmailError', { ns: 'ui' }));
await promiseSetRecoil(appSeverityState, 'error');
await promiseSetRecoil(appSnackOpenState, true);
return {};
}
};

export const apiRequestPasswordlesssLink = async (email, uid) => {
const { t } = getI18n();

Expand Down
1 change: 1 addition & 0 deletions src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const {
apiPocketValidate,
apiFetchPocketSessions,
apiPocketDeviceDelete,
apiRequestTempOTPCode,
} = await import('./auth.js');

export const { apiFetchCountries, apiFetchCongregations, apiCreateCongregation, apiUpdateCongregation } = await import(
Expand Down
202 changes: 202 additions & 0 deletions src/features/startup/vip/EmailOTP.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import { useEffect, useRef, useState } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { useTranslation } from 'react-i18next';
import { MuiOtpInput } from 'mui-one-time-password-input';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import CircularProgress from '@mui/material/CircularProgress';
import Container from '@mui/material/Container';
import Link from '@mui/material/Link';
import Typography from '@mui/material/Typography';
import {
currentMFAStageState,
isAppLoadState,
isCongAccountCreateState,
isSetupState,
isUnauthorizedRoleState,
isUserEmailOTPState,
isUserMfaSetupState,
isUserMfaVerifyState,
offlineOverrideState,
visitorIDState,
} from '../../../states/main';
import { apiHandleVerifyEmailOTP } from '../../../api/auth';
import { appMessageState, appSeverityState, appSnackOpenState } from '../../../states/notification';
import { congAccountConnectedState } from '../../../states/congregation';
import { runUpdater } from '../../../utils/updater';
import { apiRequestTempOTPCode } from '../../../api';
import useFirebaseAuth from '../../../hooks/useFirebaseAuth';

const matchIsNumeric = (text) => {
return !isNaN(Number(text));
};

const validateChar = (value, index) => {
return matchIsNumeric(value);
};

const EmailOTP = () => {
const cancel = useRef();

const { t } = useTranslation('ui');

const setAppSnackOpen = useSetRecoilState(appSnackOpenState);
const setAppSeverity = useSetRecoilState(appSeverityState);
const setAppMessage = useSetRecoilState(appMessageState);
const setIsSetup = useSetRecoilState(isSetupState);
const setIsAppLoad = useSetRecoilState(isAppLoadState);
const setCongAccountConnected = useSetRecoilState(congAccountConnectedState);
const setOfflineOverride = useSetRecoilState(offlineOverrideState);
const setIsUnauthorizedRole = useSetRecoilState(isUnauthorizedRoleState);
const setIsCongAccountCreate = useSetRecoilState(isCongAccountCreateState);
const setIsUserEmailOTP = useSetRecoilState(isUserEmailOTPState);
const setIsUserMfaSetup = useSetRecoilState(isUserMfaSetupState);
const setIsUserMfaVerify = useSetRecoilState(isUserMfaVerifyState);

const visitorID = useRecoilValue(visitorIDState);
const currentMFAStage = useRecoilValue(currentMFAStageState);

const [userOTP, setUserOTP] = useState('');
const [isProcessing, setIsProcessing] = useState(false);
const [isProcessingOTP, setIsProcessingOTP] = useState(false);

const { user } = useFirebaseAuth();

const handleOtpChange = async (newValue) => {
setUserOTP(newValue);
};

const handleSendEmailOTP = async () => {
try {
setIsProcessingOTP(true);
cancel.current = false;

const response = await apiRequestTempOTPCode(user.uid);

if (!cancel.current) {
if (response.success) {
setAppMessage(t('emailOTPCodeSent'));
setAppSeverity('success');
setAppSnackOpen(true);
}
setIsProcessingOTP(false);
}
} catch (err) {
if (!cancel.current) {
setIsProcessingOTP(false);
setAppMessage(err.message);
setAppSeverity('error');
setAppSnackOpen(true);
}
}
};

const handleVerifyEmailOTP = async () => {
try {
setIsProcessing(true);
cancel.current = false;

const response = await apiHandleVerifyEmailOTP(userOTP);

if (!cancel.current) {
if (response.success) {
setIsSetup(false);

await runUpdater();
setTimeout(() => {
setOfflineOverride(false);
setCongAccountConnected(true);
setIsAppLoad(false);
}, [2000]);
}

if (response.unauthorized) {
setIsUserEmailOTP(false);
setIsUnauthorizedRole(true);
}

if (response.createCongregation) {
setIsUserEmailOTP(false);
setIsCongAccountCreate(true);
}

setIsProcessing(false);
}
} catch (err) {
if (!cancel.current) {
setIsProcessing(false);
setAppMessage(err.message);
setAppSeverity('error');
setAppSnackOpen(true);
}
}
};

const handleAuthenticatorApp = () => {
setIsUserEmailOTP(false);
if (currentMFAStage === 'setup') setIsUserMfaSetup(true);
if (currentMFAStage === 'verify') setIsUserMfaVerify(true);
};

useEffect(() => {
return () => {
cancel.current = true;
};
}, []);

return (
<Container sx={{ marginTop: '20px' }}>
<Typography variant="h4" sx={{ marginBottom: '15px' }}>
{t('sendOTPEmail')}
</Typography>

<Typography sx={{ marginBottom: '15px' }}>{t('sendOTPEmailDesc')}</Typography>

<Button
variant="contained"
disabled={isProcessingOTP || visitorID.length === 0}
onClick={handleSendEmailOTP}
endIcon={isProcessingOTP ? <CircularProgress size={25} /> : null}
>
{t('sendOTPCode')}
</Button>

<Box sx={{ width: '100%', maxWidth: '450px', marginTop: '20px' }}>
<MuiOtpInput
value={userOTP}
onChange={handleOtpChange}
length={6}
display="flex"
gap={1}
validateChar={validateChar}
TextFieldsProps={{ autoComplete: 'off' }}
/>
</Box>

<Box
sx={{
marginTop: '15px',
display: 'flex',
alignItems: 'flex-end',
flexWrap: 'wrap',
gap: '20px',
}}
>
<Button
variant="contained"
disabled={isProcessing || visitorID.length === 0}
onClick={handleVerifyEmailOTP}
endIcon={isProcessing ? <CircularProgress size={25} /> : null}
>
{t('mfaVerify')}
</Button>

<Link component="button" underline="none" variant="body1" onClick={handleAuthenticatorApp}>
{t('useAuthenticatorApp')}
</Link>
</Box>
</Container>
);
};

export default EmailOTP;
17 changes: 14 additions & 3 deletions src/features/startup/vip/SetupMFA.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
isReEnrollMFAState,
isSetupState,
isUnauthorizedRoleState,
isUserEmailOTPState,
isUserMfaSetupState,
offlineOverrideState,
qrCodePathState,
Expand Down Expand Up @@ -69,6 +70,7 @@ const SetupMFA = () => {
const setIsAppLoad = useSetRecoilState(isAppLoadState);
const setCongAccountConnected = useSetRecoilState(congAccountConnectedState);
const setOfflineOverride = useSetRecoilState(offlineOverrideState);
const setIsUserEmailOTP = useSetRecoilState(isUserEmailOTPState);

const qrCodePath = useRecoilValue(qrCodePathState);
const token = useRecoilValue(secretTokenPathState);
Expand Down Expand Up @@ -139,6 +141,11 @@ const SetupMFA = () => {
setUserOTP(newValue);
};

const handleEmailOTP = () => {
setIsUserEmailOTP(true);
setIsUserMfaSetup(false);
};

useEffect(() => {
if (userOTP.length === 6) {
handleVerifyOTP();
Expand Down Expand Up @@ -270,9 +277,9 @@ const SetupMFA = () => {
sx={{
marginTop: '20px',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
gap: '10px',
alignItems: 'flex-end',
gap: '20px',
flexWrap: 'wrap',
}}
>
<Button
Expand All @@ -283,6 +290,10 @@ const SetupMFA = () => {
>
{t('mfaVerify')}
</Button>

<Link component="button" underline="none" variant="body1" onClick={handleEmailOTP}>
{t('sendOTPEmail')}
</Link>
</Box>
</Container>
);
Expand Down
Loading

0 comments on commit b7c1a50

Please sign in to comment.