aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnton Tananaev <anton@traccar.org>2022-08-27 12:51:32 -0700
committerAnton Tananaev <anton@traccar.org>2022-08-27 12:51:32 -0700
commitb4a769e15fb18d854af56c24e39b0b72fff273a0 (patch)
treeeaf1d43df634de82dc7731a8e53636a91db5a94b
parentc0fcac0d1956caa2c426f1b265ccab7bdfc007bc (diff)
downloadtrackermap-web-b4a769e15fb18d854af56c24e39b0b72fff273a0.tar.gz
trackermap-web-b4a769e15fb18d854af56c24e39b0b72fff273a0.tar.bz2
trackermap-web-b4a769e15fb18d854af56c24e39b0b72fff273a0.zip
Support biometric authentication
-rw-r--r--modern/src/common/components/BottomMenu.js2
-rw-r--r--modern/src/common/components/NativeInterface.js33
-rw-r--r--modern/src/login/LoginPage.js52
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"