Added fingerprinting fixed + update firebase fingerprint on log in BOD-132
This commit is contained in:
@@ -12,4 +12,5 @@
|
||||
6. Deploy the function
|
||||
1. $ firebase deploy --only functions
|
||||
7. Add the allowed domains.
|
||||
8. Update server variables including FIREBASE_ADMINSDK_JSON, FIREBASE_DATABASE_URL
|
||||
8. Update server variables including FIREBASE_ADMINSDK_JSON, FIREBASE_DATABASE_URL
|
||||
9. Create the firestore and copy the rules from dev for userinstances.
|
||||
@@ -4573,6 +4573,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>refresh</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>reset</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -4746,6 +4767,48 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>instanceconflictext</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>instanceconflictitle</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>loading</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
|
||||
27
client/src/components/conflict/conflict.component.jsx
Normal file
27
client/src/components/conflict/conflict.component.jsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from "react";
|
||||
import { Result, Button } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function ConflictComponent() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
<Result
|
||||
status="warning"
|
||||
title={t("general.labels.instanceconflictitle")}
|
||||
extra={
|
||||
<div>
|
||||
<div>{t("general.labels.instanceconflictext")}</div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
{t("general.actions.refresh")}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -7,10 +7,15 @@ import ChatAffixContainer from "../../components/chat-affix/chat-affix.container
|
||||
import ErrorBoundary from "../../components/error-boundary/error-boundary.component";
|
||||
import FcmNotification from "../../components/fcm-notification/fcm-notification.component";
|
||||
import FooterComponent from "../../components/footer/footer.component";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectInstanceConflict } from "../../redux/user/user.selectors";
|
||||
//Component Imports
|
||||
import HeaderContainer from "../../components/header/header.container";
|
||||
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
|
||||
import PrintCenterModalContainer from "../../components/print-center-modal/print-center-modal.container";
|
||||
import ConflictComponent from '../../components/conflict/conflict.component'
|
||||
|
||||
import "./manage.page.styles.scss";
|
||||
const ManageRootPage = lazy(() =>
|
||||
import("../manage-root/manage-root.page.container")
|
||||
@@ -91,7 +96,15 @@ const JobsClose = lazy(() => import("../jobs-close/jobs-close.container"));
|
||||
|
||||
const { Header, Content, Footer } = Layout;
|
||||
|
||||
export default function Manage({ match }) {
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
conflict: selectInstanceConflict,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export function Manage({ match, conflict }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -105,145 +118,159 @@ export default function Manage({ match }) {
|
||||
</Header>
|
||||
<Layout>
|
||||
<Content
|
||||
className='content-container'
|
||||
style={{ padding: "0em 4em 4em" }}>
|
||||
className="content-container"
|
||||
style={{ padding: "0em 4em 4em" }}
|
||||
>
|
||||
<FcmNotification />
|
||||
<ErrorBoundary>
|
||||
<Suspense
|
||||
fallback={
|
||||
<LoadingSpinner message={t("general.labels.loadingapp")} />
|
||||
}>
|
||||
<BreadCrumbs />
|
||||
<EnterInvoiceModalContainer />
|
||||
<EmailOverlayContainer />
|
||||
<TimeTicketModalContainer />
|
||||
<PrintCenterModalContainer />
|
||||
<Route exact path={`${match.path}`} component={ManageRootPage} />
|
||||
<Route exact path={`${match.path}/jobs`} component={JobsPage} />
|
||||
{conflict ? (
|
||||
<ConflictComponent />
|
||||
) : (
|
||||
<Suspense
|
||||
fallback={
|
||||
<LoadingSpinner message={t("general.labels.loadingapp")} />
|
||||
}
|
||||
>
|
||||
<BreadCrumbs />
|
||||
<EnterInvoiceModalContainer />
|
||||
<EmailOverlayContainer />
|
||||
<TimeTicketModalContainer />
|
||||
<PrintCenterModalContainer />
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}`}
|
||||
component={ManageRootPage}
|
||||
/>
|
||||
<Route exact path={`${match.path}/jobs`} component={JobsPage} />
|
||||
|
||||
<Switch>
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/jobs/:jobId/intake`}
|
||||
component={JobIntake}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/jobs/:jobId/close`}
|
||||
component={JobsClose}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/jobs/all`}
|
||||
component={AllJobs}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/jobs/new`}
|
||||
component={JobsCreateContainerPage}
|
||||
/>
|
||||
<Route
|
||||
path={`${match.path}/jobs/:jobId`}
|
||||
component={JobsDetailPage}
|
||||
/>
|
||||
</Switch>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/jobs/:jobId/intake`}
|
||||
component={JobIntake}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/jobs/:jobId/close`}
|
||||
component={JobsClose}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/jobs/all`}
|
||||
component={AllJobs}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/jobs/new`}
|
||||
component={JobsCreateContainerPage}
|
||||
/>
|
||||
<Route
|
||||
path={`${match.path}/jobs/:jobId`}
|
||||
component={JobsDetailPage}
|
||||
/>
|
||||
</Switch>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/courtesycars/`}
|
||||
component={CourtesyCarsPage}
|
||||
/>
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/courtesycars/new`}
|
||||
component={CourtesyCarCreateContainer}
|
||||
path={`${match.path}/courtesycars/`}
|
||||
component={CourtesyCarsPage}
|
||||
/>
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/courtesycars/new`}
|
||||
component={CourtesyCarCreateContainer}
|
||||
/>
|
||||
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/courtesycars/contracts`}
|
||||
component={ContractsList}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/courtesycars/contracts`}
|
||||
component={ContractsList}
|
||||
/>
|
||||
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/courtesycars/contracts/new`}
|
||||
component={ContractCreatePage}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/courtesycars/contracts/:contractId`}
|
||||
component={ContractDetailPage}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/courtesycars/contracts/new`}
|
||||
component={ContractCreatePage}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/courtesycars/contracts/:contractId`}
|
||||
component={ContractDetailPage}
|
||||
/>
|
||||
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/courtesycars/:ccId`}
|
||||
component={CourtesyCarDetailContainer}
|
||||
/>
|
||||
</Switch>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/courtesycars/:ccId`}
|
||||
component={CourtesyCarDetailContainer}
|
||||
path={`${match.path}/profile`}
|
||||
component={ProfilePage}
|
||||
/>
|
||||
</Switch>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/profile`}
|
||||
component={ProfilePage}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/vehicles`}
|
||||
component={VehiclesContainer}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/production/list`}
|
||||
component={ProductionListPage}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/production/board`}
|
||||
component={ProductionBoardPage}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/vehicles/:vehId`}
|
||||
component={VehiclesDetailContainer}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/invoices`}
|
||||
component={InvoicesListPage}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/owners`}
|
||||
component={OwnersContainer}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/owners/:ownerId`}
|
||||
component={OwnersDetailContainer}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/schedule`}
|
||||
component={ScheduleContainer}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/available`}
|
||||
component={JobsAvailablePage}
|
||||
/>
|
||||
<Route exact path={`${match.path}/shop/`} component={ShopPage} />
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/shop/templates`}
|
||||
component={ShopTemplates}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/shop/vendors`}
|
||||
component={ShopVendorPageContainer}
|
||||
/>
|
||||
</Suspense>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/vehicles`}
|
||||
component={VehiclesContainer}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/production/list`}
|
||||
component={ProductionListPage}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/production/board`}
|
||||
component={ProductionBoardPage}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/vehicles/:vehId`}
|
||||
component={VehiclesDetailContainer}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/invoices`}
|
||||
component={InvoicesListPage}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/owners`}
|
||||
component={OwnersContainer}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/owners/:ownerId`}
|
||||
component={OwnersDetailContainer}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/schedule`}
|
||||
component={ScheduleContainer}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/available`}
|
||||
component={JobsAvailablePage}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/shop/`}
|
||||
component={ShopPage}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/shop/templates`}
|
||||
component={ShopTemplates}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/shop/vendors`}
|
||||
component={ShopVendorPageContainer}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
</Content>
|
||||
</Layout>
|
||||
@@ -255,3 +282,4 @@ export default function Manage({ match }) {
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Manage);
|
||||
|
||||
@@ -12,6 +12,8 @@ const INITIAL_STATE = {
|
||||
|
||||
const userReducer = (state = INITIAL_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case UserActionTypes.SET_INSTANCE_ID:
|
||||
return { ...state, conflict: false };
|
||||
case UserActionTypes.SET_INSTANCE_CONFLICT:
|
||||
return { ...state, conflict: true };
|
||||
case UserActionTypes.SIGN_IN_SUCCESS:
|
||||
|
||||
@@ -26,7 +26,6 @@ export function* signInWithEmail({ payload: { email, password } }) {
|
||||
const { user } = yield auth.signInWithEmailAndPassword(email, password);
|
||||
LogRocket.identify(user.email);
|
||||
|
||||
yield put(setInstanceId("123"));
|
||||
yield put(
|
||||
signInSuccess({
|
||||
uid: user.uid,
|
||||
@@ -51,7 +50,7 @@ export function* isUserAuthenticated() {
|
||||
yield put(unauthorizedUser());
|
||||
return;
|
||||
}
|
||||
yield put(setInstanceId(user.uid));
|
||||
|
||||
LogRocket.identify(user.email);
|
||||
yield put(
|
||||
signInSuccess({
|
||||
@@ -101,16 +100,15 @@ export function* setInstanceIdSaga({ payload: uid }) {
|
||||
const userInstanceRef = firestore.doc(`userInstance/${uid}`);
|
||||
|
||||
const fingerprint = Fingerprint2.x64hash128(
|
||||
(yield Fingerprint2.getPromise({
|
||||
excludes: { fonts: true, audio: true, webgl: true },
|
||||
})).join(""),
|
||||
(yield Fingerprint2.getPromise({})).map((c) => c.value).join(""),
|
||||
31
|
||||
);
|
||||
|
||||
yield userInstanceRef.set({
|
||||
timestamp: new Date(),
|
||||
fingerprint,
|
||||
});
|
||||
var result = window.confirm("Press a button!");
|
||||
if (result)
|
||||
yield userInstanceRef.set({
|
||||
timestamp: new Date(),
|
||||
fingerprint,
|
||||
});
|
||||
|
||||
yield delay(5000);
|
||||
yield put(checkInstanceId(uid));
|
||||
@@ -129,22 +127,17 @@ export function* checkInstanceIdSaga({ payload: uid }) {
|
||||
const userInstanceRef = firestore.doc(`userInstance/${uid}`);
|
||||
|
||||
const fingerprint = Fingerprint2.x64hash128(
|
||||
(yield Fingerprint2.getPromise({
|
||||
excludes: { fonts: true, audio: true, webgl: true },
|
||||
})).join(""),
|
||||
(yield Fingerprint2.getPromise({})).map((c) => c.value).join(""),
|
||||
31
|
||||
);
|
||||
|
||||
const snapshot = yield userInstanceRef.get();
|
||||
console.log("function*checkInstanceIdSaga -> snapshot", snapshot.data());
|
||||
console.log("fingerprint", fingerprint);
|
||||
|
||||
if (snapshot.data().fingerprint === fingerprint) {
|
||||
console.log("Waiting and checking.");
|
||||
yield delay(5000);
|
||||
yield delay(30000);
|
||||
yield put(checkInstanceId(uid));
|
||||
} else {
|
||||
console.log("Didnt match");
|
||||
console.log("ERROR: Fingerprints do not match. Conflict detected.");
|
||||
yield put(setInstanceConflict());
|
||||
}
|
||||
// yield userInstanceRef.set({
|
||||
@@ -158,6 +151,14 @@ export function* checkInstanceIdSaga({ payload: uid }) {
|
||||
}
|
||||
}
|
||||
|
||||
export function* onSignInSuccess() {
|
||||
yield takeLatest(UserActionTypes.SIGN_IN_SUCCESS, signInSuccessSaga);
|
||||
}
|
||||
|
||||
export function* signInSuccessSaga({ payload }) {
|
||||
yield put(setInstanceId(payload.uid));
|
||||
}
|
||||
|
||||
export function* userSagas() {
|
||||
yield all([
|
||||
call(onEmailSignInStart),
|
||||
@@ -166,5 +167,6 @@ export function* userSagas() {
|
||||
call(onUpdateUserDetails),
|
||||
call(onSetInstanceId),
|
||||
call(onCheckInstanceId),
|
||||
call(onSignInSuccess),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -312,6 +312,7 @@
|
||||
"create": "Create",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"refresh": "Refresh",
|
||||
"reset": "Reset to original.",
|
||||
"save": "Save",
|
||||
"saveandnew": "Save and New",
|
||||
@@ -322,6 +323,8 @@
|
||||
"areyousure": "Are you sure?",
|
||||
"barcode": "Barcode",
|
||||
"in": "In",
|
||||
"instanceconflictext": "Your $t(titles.app) account can only be used on one device at any given time. Refresh your session to take control.",
|
||||
"instanceconflictitle": "Your account is being used elsewhere.",
|
||||
"loading": "Loading...",
|
||||
"loadingapp": "Loading Bodyshop.app",
|
||||
"loadingshop": "Loading shop data...",
|
||||
|
||||
@@ -312,6 +312,7 @@
|
||||
"create": "",
|
||||
"delete": "Borrar",
|
||||
"edit": "Editar",
|
||||
"refresh": "",
|
||||
"reset": "Restablecer a original.",
|
||||
"save": "Salvar",
|
||||
"saveandnew": "",
|
||||
@@ -322,6 +323,8 @@
|
||||
"areyousure": "",
|
||||
"barcode": "código de barras",
|
||||
"in": "en",
|
||||
"instanceconflictext": "",
|
||||
"instanceconflictitle": "",
|
||||
"loading": "Cargando...",
|
||||
"loadingapp": "Cargando Bodyshop.app",
|
||||
"loadingshop": "Cargando datos de la tienda ...",
|
||||
|
||||
@@ -312,6 +312,7 @@
|
||||
"create": "",
|
||||
"delete": "Effacer",
|
||||
"edit": "modifier",
|
||||
"refresh": "",
|
||||
"reset": "Rétablir l'original.",
|
||||
"save": "sauvegarder",
|
||||
"saveandnew": "",
|
||||
@@ -322,6 +323,8 @@
|
||||
"areyousure": "",
|
||||
"barcode": "code à barre",
|
||||
"in": "dans",
|
||||
"instanceconflictext": "",
|
||||
"instanceconflictitle": "",
|
||||
"loading": "Chargement...",
|
||||
"loadingapp": "Chargement de Bodyshop.app",
|
||||
"loadingshop": "Chargement des données de la boutique ...",
|
||||
|
||||
Reference in New Issue
Block a user