diff options
author | Anton Tananaev <anton@traccar.org> | 2022-08-27 12:51:32 -0700 |
---|---|---|
committer | Anton Tananaev <anton@traccar.org> | 2022-08-27 12:51:32 -0700 |
commit | b4a769e15fb18d854af56c24e39b0b72fff273a0 (patch) | |
tree | eaf1d43df634de82dc7731a8e53636a91db5a94b | |
parent | c0fcac0d1956caa2c426f1b265ccab7bdfc007bc (diff) | |
download | trackermap-web-b4a769e15fb18d854af56c24e39b0b72fff273a0.tar.gz trackermap-web-b4a769e15fb18d854af56c24e39b0b72fff273a0.tar.bz2 trackermap-web-b4a769e15fb18d854af56c24e39b0b72fff273a0.zip |
Support biometric authentication
-rw-r--r-- | modern/src/common/components/BottomMenu.js | 2 | ||||
-rw-r--r-- | modern/src/common/components/NativeInterface.js | 33 | ||||
-rw-r--r-- | modern/src/login/LoginPage.js | 52 |
3 files changed, 66 insertions, 21 deletions
diff --git a/modern/src/common/components/BottomMenu.js b/modern/src/common/components/BottomMenu.js index 3ef464e0..68ccf056 100644 --- a/modern/src/common/components/BottomMenu.js +++ b/modern/src/common/components/BottomMenu.js @@ -14,6 +14,7 @@ import ExitToAppIcon from '@mui/icons-material/ExitToApp'; import { sessionActions } from '../../store'; import { useTranslation } from './LocalizationProvider'; import { useRestriction } from '../util/permissions'; +import { nativePostMessage } from './NativeInterface'; const BottomMenu = () => { const navigate = useNavigate(); @@ -70,6 +71,7 @@ const BottomMenu = () => { } await fetch('/api/session', { method: 'DELETE' }); + nativePostMessage('logout'); navigate('/login'); dispatch(sessionActions.updateUser(null)); }; diff --git a/modern/src/common/components/NativeInterface.js b/modern/src/common/components/NativeInterface.js index 4e6ad4fe..d43b678c 100644 --- a/modern/src/common/components/NativeInterface.js +++ b/modern/src/common/components/NativeInterface.js @@ -14,37 +14,40 @@ export const nativePostMessage = (message) => { } }; -const listeners = new Set(); +export const handleLoginTokenListeners = new Set(); +window.handleLoginToken = (token) => { + handleLoginTokenListeners.forEach((listener) => listener(token)); +}; + +const updateNotificationTokenListeners = new Set(); window.updateNotificationToken = (token) => { - listeners.forEach((listener) => listener(token)); + updateNotificationTokenListeners.forEach((listener) => listener(token)); }; const NativeInterface = () => { const dispatch = useDispatch(); const user = useSelector((state) => state.session.user); - const [token, setToken] = useState(null); + const [notificationToken, setNotificationToken] = useState(null); useEffect(() => { - const listener = (token) => setToken(token); - listeners.add(listener); - return () => { - listeners.delete(listener); - }; - }, [setToken]); + const listener = (token) => setNotificationToken(token); + updateNotificationTokenListeners.add(listener); + return () => updateNotificationTokenListeners.delete(listener); + }, [setNotificationToken]); useEffectAsync(async () => { - if (user && token) { - window.localStorage.setItem('notificationToken', token); - setToken(null); + if (user && notificationToken) { + window.localStorage.setItem('notificationToken', notificationToken); + setNotificationToken(null); const tokens = user.attributes.notificationTokens?.split(',') || []; - if (!tokens.includes(token)) { + if (!tokens.includes(notificationToken)) { const updatedUser = { ...user, attributes: { ...user.attributes, - notificationTokens: [...tokens.slice(-2), token].join(','), + notificationTokens: [...tokens.slice(-2), notificationToken].join(','), }, }; @@ -61,7 +64,7 @@ const NativeInterface = () => { } } } - }, [user, token, setToken]); + }, [user, notificationToken, setNotificationToken]); return null; }; diff --git a/modern/src/login/LoginPage.js b/modern/src/login/LoginPage.js index 9bb764a8..e35ca232 100644 --- a/modern/src/login/LoginPage.js +++ b/modern/src/login/LoginPage.js @@ -1,4 +1,5 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; +import moment from 'moment'; import { useMediaQuery, InputLabel, Select, MenuItem, FormControl, Button, TextField, Link, Snackbar, IconButton, Tooltip, } from '@mui/material'; @@ -12,8 +13,9 @@ import { sessionActions } from '../store'; import { useLocalization, useTranslation } from '../common/components/LocalizationProvider'; import LoginLayout from './LoginLayout'; import usePersistedState from '../common/util/usePersistedState'; -import { nativeEnvironment, nativePostMessage } from '../common/components/NativeInterface'; +import { handleLoginTokenListeners, nativeEnvironment, nativePostMessage } from '../common/components/NativeInterface'; import LogoImage from './LogoImage'; +import { useCatch } from '../reactHelper'; const useStyles = makeStyles((theme) => ({ options: { @@ -62,7 +64,26 @@ const LoginPage = () => { const [announcementShown, setAnnouncementShown] = useState(false); const announcement = useSelector((state) => state.session.server.announcement); - const handleSubmit = async (event) => { + const generateLoginToken = async () => { + if (nativeEnvironment) { + let token = ''; + try { + const expiration = moment().add(6, 'months').toISOString(); + const response = await fetch('/api/session/token', { + method: 'POST', + body: new URLSearchParams(`expiration=${expiration}`), + }); + if (response.ok) { + token = await response.text(); + } + } catch (error) { + token = ''; + } + nativePostMessage(`login|${token}`); + } + }; + + const handlePasswordLogin = async (event) => { event.preventDefault(); try { const response = await fetch('/api/session', { @@ -71,8 +92,8 @@ const LoginPage = () => { }); if (response.ok) { const user = await response.json(); + generateLoginToken(); dispatch(sessionActions.updateUser(user)); - nativePostMessage('login'); navigate('/'); } else { throw Error(await response.text()); @@ -83,12 +104,31 @@ const LoginPage = () => { } }; + const handleTokenLogin = useCatch(async (token) => { + const response = await fetch(`/api/session?token=${encodeURIComponent(token)}`); + if (response.ok) { + const user = await response.json(); + dispatch(sessionActions.updateUser(user)); + navigate('/'); + } else { + throw Error(await response.text()); + } + }); + const handleSpecialKey = (e) => { if (e.keyCode === 13 && email && password) { - handleSubmit(e); + handlePasswordLogin(e); } }; + useEffect(() => nativePostMessage('authentication'), []); + + useEffect(() => { + const listener = (token) => handleTokenLogin(token); + handleLoginTokenListeners.add(listener); + return () => handleLoginTokenListeners.delete(listener); + }, []); + return ( <LoginLayout> <div className={classes.options}> @@ -127,7 +167,7 @@ const LoginPage = () => { onKeyUp={handleSpecialKey} /> <Button - onClick={handleSubmit} + onClick={handlePasswordLogin} onKeyUp={handleSpecialKey} variant="contained" color="secondary" |