Added fingerprinting fixed + update firebase fingerprint on log in BOD-132

This commit is contained in:
Patrick Fic
2020-05-21 13:17:59 -07:00
parent 2ab2a27d27
commit 201e85d7db
9 changed files with 279 additions and 147 deletions

View File

@@ -13,3 +13,4 @@
1. $ firebase deploy --only functions 1. $ firebase deploy --only functions
7. Add the allowed domains. 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.

View File

@@ -4573,6 +4573,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </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> <concept_node>
<name>reset</name> <name>reset</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -4746,6 +4767,48 @@
</translation> </translation>
</translations> </translations>
</concept_node> </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> <concept_node>
<name>loading</name> <name>loading</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>

View 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>
);
}

View File

@@ -7,10 +7,15 @@ import ChatAffixContainer from "../../components/chat-affix/chat-affix.container
import ErrorBoundary from "../../components/error-boundary/error-boundary.component"; import ErrorBoundary from "../../components/error-boundary/error-boundary.component";
import FcmNotification from "../../components/fcm-notification/fcm-notification.component"; import FcmNotification from "../../components/fcm-notification/fcm-notification.component";
import FooterComponent from "../../components/footer/footer.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 //Component Imports
import HeaderContainer from "../../components/header/header.container"; import HeaderContainer from "../../components/header/header.container";
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component"; import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
import PrintCenterModalContainer from "../../components/print-center-modal/print-center-modal.container"; import PrintCenterModalContainer from "../../components/print-center-modal/print-center-modal.container";
import ConflictComponent from '../../components/conflict/conflict.component'
import "./manage.page.styles.scss"; import "./manage.page.styles.scss";
const ManageRootPage = lazy(() => const ManageRootPage = lazy(() =>
import("../manage-root/manage-root.page.container") 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; 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(); const { t } = useTranslation();
useEffect(() => { useEffect(() => {
@@ -105,145 +118,159 @@ export default function Manage({ match }) {
</Header> </Header>
<Layout> <Layout>
<Content <Content
className='content-container' className="content-container"
style={{ padding: "0em 4em 4em" }}> style={{ padding: "0em 4em 4em" }}
>
<FcmNotification /> <FcmNotification />
<ErrorBoundary> <ErrorBoundary>
<Suspense {conflict ? (
fallback={ <ConflictComponent />
<LoadingSpinner message={t("general.labels.loadingapp")} /> ) : (
}> <Suspense
<BreadCrumbs /> fallback={
<EnterInvoiceModalContainer /> <LoadingSpinner message={t("general.labels.loadingapp")} />
<EmailOverlayContainer /> }
<TimeTicketModalContainer /> >
<PrintCenterModalContainer /> <BreadCrumbs />
<Route exact path={`${match.path}`} component={ManageRootPage} /> <EnterInvoiceModalContainer />
<Route exact path={`${match.path}/jobs`} component={JobsPage} /> <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 <Route
exact exact
path={`${match.path}/jobs/:jobId/intake`} path={`${match.path}/courtesycars/`}
component={JobIntake} component={CourtesyCarsPage}
/>
<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}
/> />
<Switch>
<Route
exact
path={`${match.path}/courtesycars/new`}
component={CourtesyCarCreateContainer}
/>
<Route <Route
exact exact
path={`${match.path}/courtesycars/contracts`} path={`${match.path}/courtesycars/contracts`}
component={ContractsList} component={ContractsList}
/> />
<Route <Route
exact exact
path={`${match.path}/courtesycars/contracts/new`} path={`${match.path}/courtesycars/contracts/new`}
component={ContractCreatePage} component={ContractCreatePage}
/> />
<Route <Route
exact exact
path={`${match.path}/courtesycars/contracts/:contractId`} path={`${match.path}/courtesycars/contracts/:contractId`}
component={ContractDetailPage} component={ContractDetailPage}
/> />
<Route
exact
path={`${match.path}/courtesycars/:ccId`}
component={CourtesyCarDetailContainer}
/>
</Switch>
<Route <Route
exact exact
path={`${match.path}/courtesycars/:ccId`} path={`${match.path}/profile`}
component={CourtesyCarDetailContainer} component={ProfilePage}
/> />
</Switch> <Route
<Route exact
exact path={`${match.path}/vehicles`}
path={`${match.path}/profile`} component={VehiclesContainer}
component={ProfilePage} />
/> <Route
<Route exact
exact path={`${match.path}/production/list`}
path={`${match.path}/vehicles`} component={ProductionListPage}
component={VehiclesContainer} />
/> <Route
<Route exact
exact path={`${match.path}/production/board`}
path={`${match.path}/production/list`} component={ProductionBoardPage}
component={ProductionListPage} />
/> <Route
<Route exact
exact path={`${match.path}/vehicles/:vehId`}
path={`${match.path}/production/board`} component={VehiclesDetailContainer}
component={ProductionBoardPage} />
/> <Route
<Route exact
exact path={`${match.path}/invoices`}
path={`${match.path}/vehicles/:vehId`} component={InvoicesListPage}
component={VehiclesDetailContainer} />
/> <Route
<Route exact
exact path={`${match.path}/owners`}
path={`${match.path}/invoices`} component={OwnersContainer}
component={InvoicesListPage} />
/> <Route
<Route exact
exact path={`${match.path}/owners/:ownerId`}
path={`${match.path}/owners`} component={OwnersDetailContainer}
component={OwnersContainer} />
/> <Route
<Route exact
exact path={`${match.path}/schedule`}
path={`${match.path}/owners/:ownerId`} component={ScheduleContainer}
component={OwnersDetailContainer} />
/> <Route
<Route exact
exact path={`${match.path}/available`}
path={`${match.path}/schedule`} component={JobsAvailablePage}
component={ScheduleContainer} />
/> <Route
<Route exact
exact path={`${match.path}/shop/`}
path={`${match.path}/available`} component={ShopPage}
component={JobsAvailablePage} />
/> <Route
<Route exact path={`${match.path}/shop/`} component={ShopPage} /> exact
<Route path={`${match.path}/shop/templates`}
exact component={ShopTemplates}
path={`${match.path}/shop/templates`} />
component={ShopTemplates} <Route
/> exact
<Route path={`${match.path}/shop/vendors`}
exact component={ShopVendorPageContainer}
path={`${match.path}/shop/vendors`} />
component={ShopVendorPageContainer} </Suspense>
/> )}
</Suspense>
</ErrorBoundary> </ErrorBoundary>
</Content> </Content>
</Layout> </Layout>
@@ -255,3 +282,4 @@ export default function Manage({ match }) {
</Layout> </Layout>
); );
} }
export default connect(mapStateToProps, mapDispatchToProps)(Manage);

View File

@@ -12,6 +12,8 @@ const INITIAL_STATE = {
const userReducer = (state = INITIAL_STATE, action) => { const userReducer = (state = INITIAL_STATE, action) => {
switch (action.type) { switch (action.type) {
case UserActionTypes.SET_INSTANCE_ID:
return { ...state, conflict: false };
case UserActionTypes.SET_INSTANCE_CONFLICT: case UserActionTypes.SET_INSTANCE_CONFLICT:
return { ...state, conflict: true }; return { ...state, conflict: true };
case UserActionTypes.SIGN_IN_SUCCESS: case UserActionTypes.SIGN_IN_SUCCESS:

View File

@@ -26,7 +26,6 @@ export function* signInWithEmail({ payload: { email, password } }) {
const { user } = yield auth.signInWithEmailAndPassword(email, password); const { user } = yield auth.signInWithEmailAndPassword(email, password);
LogRocket.identify(user.email); LogRocket.identify(user.email);
yield put(setInstanceId("123"));
yield put( yield put(
signInSuccess({ signInSuccess({
uid: user.uid, uid: user.uid,
@@ -51,7 +50,7 @@ export function* isUserAuthenticated() {
yield put(unauthorizedUser()); yield put(unauthorizedUser());
return; return;
} }
yield put(setInstanceId(user.uid));
LogRocket.identify(user.email); LogRocket.identify(user.email);
yield put( yield put(
signInSuccess({ signInSuccess({
@@ -101,16 +100,15 @@ export function* setInstanceIdSaga({ payload: uid }) {
const userInstanceRef = firestore.doc(`userInstance/${uid}`); const userInstanceRef = firestore.doc(`userInstance/${uid}`);
const fingerprint = Fingerprint2.x64hash128( const fingerprint = Fingerprint2.x64hash128(
(yield Fingerprint2.getPromise({ (yield Fingerprint2.getPromise({})).map((c) => c.value).join(""),
excludes: { fonts: true, audio: true, webgl: true },
})).join(""),
31 31
); );
var result = window.confirm("Press a button!");
yield userInstanceRef.set({ if (result)
timestamp: new Date(), yield userInstanceRef.set({
fingerprint, timestamp: new Date(),
}); fingerprint,
});
yield delay(5000); yield delay(5000);
yield put(checkInstanceId(uid)); yield put(checkInstanceId(uid));
@@ -129,22 +127,17 @@ export function* checkInstanceIdSaga({ payload: uid }) {
const userInstanceRef = firestore.doc(`userInstance/${uid}`); const userInstanceRef = firestore.doc(`userInstance/${uid}`);
const fingerprint = Fingerprint2.x64hash128( const fingerprint = Fingerprint2.x64hash128(
(yield Fingerprint2.getPromise({ (yield Fingerprint2.getPromise({})).map((c) => c.value).join(""),
excludes: { fonts: true, audio: true, webgl: true },
})).join(""),
31 31
); );
const snapshot = yield userInstanceRef.get(); const snapshot = yield userInstanceRef.get();
console.log("function*checkInstanceIdSaga -> snapshot", snapshot.data());
console.log("fingerprint", fingerprint);
if (snapshot.data().fingerprint === fingerprint) { if (snapshot.data().fingerprint === fingerprint) {
console.log("Waiting and checking."); yield delay(30000);
yield delay(5000);
yield put(checkInstanceId(uid)); yield put(checkInstanceId(uid));
} else { } else {
console.log("Didnt match"); console.log("ERROR: Fingerprints do not match. Conflict detected.");
yield put(setInstanceConflict()); yield put(setInstanceConflict());
} }
// yield userInstanceRef.set({ // 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() { export function* userSagas() {
yield all([ yield all([
call(onEmailSignInStart), call(onEmailSignInStart),
@@ -166,5 +167,6 @@ export function* userSagas() {
call(onUpdateUserDetails), call(onUpdateUserDetails),
call(onSetInstanceId), call(onSetInstanceId),
call(onCheckInstanceId), call(onCheckInstanceId),
call(onSignInSuccess),
]); ]);
} }

View File

@@ -312,6 +312,7 @@
"create": "Create", "create": "Create",
"delete": "Delete", "delete": "Delete",
"edit": "Edit", "edit": "Edit",
"refresh": "Refresh",
"reset": "Reset to original.", "reset": "Reset to original.",
"save": "Save", "save": "Save",
"saveandnew": "Save and New", "saveandnew": "Save and New",
@@ -322,6 +323,8 @@
"areyousure": "Are you sure?", "areyousure": "Are you sure?",
"barcode": "Barcode", "barcode": "Barcode",
"in": "In", "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...", "loading": "Loading...",
"loadingapp": "Loading Bodyshop.app", "loadingapp": "Loading Bodyshop.app",
"loadingshop": "Loading shop data...", "loadingshop": "Loading shop data...",

View File

@@ -312,6 +312,7 @@
"create": "", "create": "",
"delete": "Borrar", "delete": "Borrar",
"edit": "Editar", "edit": "Editar",
"refresh": "",
"reset": "Restablecer a original.", "reset": "Restablecer a original.",
"save": "Salvar", "save": "Salvar",
"saveandnew": "", "saveandnew": "",
@@ -322,6 +323,8 @@
"areyousure": "", "areyousure": "",
"barcode": "código de barras", "barcode": "código de barras",
"in": "en", "in": "en",
"instanceconflictext": "",
"instanceconflictitle": "",
"loading": "Cargando...", "loading": "Cargando...",
"loadingapp": "Cargando Bodyshop.app", "loadingapp": "Cargando Bodyshop.app",
"loadingshop": "Cargando datos de la tienda ...", "loadingshop": "Cargando datos de la tienda ...",

View File

@@ -312,6 +312,7 @@
"create": "", "create": "",
"delete": "Effacer", "delete": "Effacer",
"edit": "modifier", "edit": "modifier",
"refresh": "",
"reset": "Rétablir l'original.", "reset": "Rétablir l'original.",
"save": "sauvegarder", "save": "sauvegarder",
"saveandnew": "", "saveandnew": "",
@@ -322,6 +323,8 @@
"areyousure": "", "areyousure": "",
"barcode": "code à barre", "barcode": "code à barre",
"in": "dans", "in": "dans",
"instanceconflictext": "",
"instanceconflictitle": "",
"loading": "Chargement...", "loading": "Chargement...",
"loadingapp": "Chargement de Bodyshop.app", "loadingapp": "Chargement de Bodyshop.app",
"loadingshop": "Chargement des données de la boutique ...", "loadingshop": "Chargement des données de la boutique ...",