Skip to content

Commit

Permalink
Merge pull request #659 from microsoft/625-front-end-graphql-storage
Browse files Browse the repository at this point in the history
625 front end graphql storage
  • Loading branch information
GutherieDEV authored Jul 15, 2022
2 parents 4c4295a + e6e8629 commit 2e20334
Show file tree
Hide file tree
Showing 9 changed files with 206 additions and 24 deletions.
3 changes: 3 additions & 0 deletions packages/webapp/config/ci.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
},
"takePhotoMode": {
"enabled": false
},
"durableCache": {
"enabled": true
}
}
}
3 changes: 3 additions & 0 deletions packages/webapp/config/custom-environment-variables.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
},
"takePhotoMode": {
"enabled": "TAKE_PHOTO_MODE_ENABLED"
},
"durableCache": {
"enabled": "DURABLE_CACHE_ENABLED"
}
}
}
4 changes: 4 additions & 0 deletions packages/webapp/src/api/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,7 @@ export function getCache(reloadCache = false) {
}
return cache
}

export const isCacheInitialized = (): boolean => {
return isDurableCacheInitialized
}
10 changes: 9 additions & 1 deletion packages/webapp/src/components/forms/LoginForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@ import { Checkbox } from '@fluentui/react'
import { noop } from '~utils/noop'
import { useNavCallback } from '~hooks/useNavCallback'
import { ApplicationRoute } from '~types/ApplicationRoute'
import { testPassword, APOLLO_KEY, getUser } from '~utils/localCrypto'
import {
getUser,
testPassword,
APOLLO_KEY,
setPreQueueLoadRequired,
setCurrentUserId
} from '~utils/localCrypto'
import { createLogger } from '~utils/createLogger'
import localforage from 'localforage'
import { config } from '~utils/config'
Expand Down Expand Up @@ -62,6 +68,8 @@ export const LoginForm: StandardFC<LoginFormProps> = wrap(function LoginForm({
const resp = await login(values.username, values.password)

if (isDurableCacheEnabled) {
setPreQueueLoadRequired()
setCurrentUserId(values.username)
const onlineAuthStatus = resp.status === 'SUCCESS'
const offlineAuthStatus = testPassword(values.username, values.password)
localUserStore.username = values.username
Expand Down
2 changes: 1 addition & 1 deletion packages/webapp/src/components/ui/ActionBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ export interface ActionBarProps {
export const ActionBar: StandardFC<ActionBarProps> = memo(function ActionBar({ title }) {
const { isMD } = useWindowSize()
const { c } = useTranslation()

const showEnvironmentInfo = 'show-environment-info'

function hideEnvironmentInfo(event: React.MouseEvent<HTMLElement>) {
// We are only interested on the header
const header = (event?.target as HTMLElement)?.closest('header')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
*/
import { useRef, useState, useCallback, memo } from 'react'
import type { StandardFC } from '~types/StandardFC'
import type {
IDatePicker} from '@fluentui/react';
import type { IDatePicker } from '@fluentui/react'
import {
DatePicker,
mergeStyleSets,
Expand Down
59 changes: 56 additions & 3 deletions packages/webapp/src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import { MyRequestsList } from '~lists/MyRequestsList'
import { RequestList } from '~lists/RequestList'
import { InactiveRequestList } from '~lists/InactiveRequestList'
import { Namespace, useTranslation } from '~hooks/useTranslation'
import { useCallback, useMemo, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useCurrentUser } from '~hooks/api/useCurrentUser'
import { PageTopButtons } from '~components/ui/PageTopButtons'
import { Title } from '~components/ui/Title'
import { NewFormPanel } from '~components/ui/NewFormPanel'
import { useOffline } from '~hooks/useOffline'

// Types
import type { Engagement } from '@cbosuite/schema/dist/client-types'
Expand All @@ -25,14 +26,28 @@ import { useQuery } from '@apollo/client'
// Utils
import { wrap } from '~utils/appinsights'
import { sortByDuration, sortByIsLocal } from '~utils/engagements'
import { isCacheInitialized } from '../api/cache'
import {
clearPreQueueLoadRequired,
getPreQueueLoadRequired,
getPreQueueRequest,
setPreQueueRequest
} from '~utils/localCrypto'
import { config } from '~utils/config'

const HomePage: FC = wrap(function Home() {
const { t } = useTranslation(Namespace.Requests)
const { userId, orgId } = useCurrentUser()
const { addEngagement } = useEngagementList(orgId, userId)

const isOffline = useOffline()
const [openNewFormPanel, setOpenNewFormPanel] = useState(false)
const [newFormName, setNewFormName] = useState(null)
const isDurableCacheEnabled = Boolean(config.features.durableCache.enabled)
const saveQueuedData = (value) => {
const queue: any[] = getPreQueueRequest()
queue.push(value)
setPreQueueRequest(queue)
}

const buttons: IPageTopButtons[] = [
{
Expand Down Expand Up @@ -70,11 +85,14 @@ const HomePage: FC = wrap(function Home() {
(values: any) => {
switch (newFormName) {
case 'addRequestForm':
if (isOffline && isDurableCacheEnabled) {
saveQueuedData(values)
}
addEngagement(values)
break
}
},
[addEngagement, newFormName]
[addEngagement, newFormName, isOffline, isDurableCacheEnabled]
)

// Fetch allEngagements
Expand Down Expand Up @@ -121,6 +139,41 @@ const HomePage: FC = wrap(function Home() {
// Memoized the Engagements to only update when useQuery is triggered
const engagements: Engagement[] = useMemo(() => [...(data?.allEngagements ?? [])], [data])

// If the browser has been restarted/reloaded and persistent pending values
// are available, requeue them.
useEffect(() => {
if (isCacheInitialized() && getPreQueueLoadRequired()) {
// Find the Optimistic Responses, and stringify the values `preQueued`
// from the `createEngagement` form
const localEngagements = engagements
.filter((engagement) => engagement.id.includes('LOCAL'))
.map((engagement) => {
return JSON.stringify({
title: engagement.title,
userId: engagement.user.id,
contactIds: engagement.contacts.map((contact) => contact.id),
endDate: new Date(engagement.endDate).valueOf(),
description: engagement.description
})
})

getPreQueueRequest().forEach((item) => {
// Only add missing engagements
const itemInfo = JSON.stringify({
title: item.title,
userId: item.userId,
contactIds: item.contactIds,
endDate: new Date(item.endDate).valueOf(),
description: item.description
})
if (!localEngagements.includes(itemInfo)) {
addEngagement(item)
}
})
clearPreQueueLoadRequired()
}
}, [addEngagement, engagements])

// Split the engagements per lists
const { userEngagements, otherEngagements, inactivesEngagements } = useMemo(
() =>
Expand Down
138 changes: 122 additions & 16 deletions packages/webapp/src/utils/localCrypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@
*/
import * as bcrypt from 'bcryptjs'
import * as CryptoJS from 'crypto-js'
import { currentUserStore } from '~utils/current-user-store'
import type { User } from '@cbosuite/schema/dist/client-types'

const APOLLO_KEY = '-apollo-cache-persist'
const SALT_KEY = '-hash-salt'
const SALT_ROUNDS = 12
const HASH_PWD_KEY = '-hash-pwd'
const CURRENT_USER_KEY = 'current-user'
const REQUEST_QUEUE_KEY = '-request-queue'
const PRE_QUEUE_REQUEST_KEY = '-pre-queue-request'
const VERIFY_TEXT = 'DECRYPT ME'
const VERIFY_TEXT_KEY = '-verify'
const PRE_QUEUE_LOAD_REQUIRED = 'pre-queue-load-required'
const USER_KEY = '-user'
const ACCESS_TOKEN_KEY = '-access-token'

Expand All @@ -27,16 +31,19 @@ const ACCESS_TOKEN_KEY = '-access-token'
* @returns boolean - value that indicates if the salt had to be created (false) or exists (true)
*/
const checkSalt = (userid: string): boolean => {
const saltKey = userid.concat(SALT_KEY)
const salt = window.localStorage.getItem(saltKey)
if (userid) {
const saltKey = userid.concat(SALT_KEY)
const salt = window.localStorage.getItem(saltKey)

if (!salt) {
const saltNew = bcrypt.genSaltSync(SALT_ROUNDS)
setSalt(saltKey, saltNew)
if (!salt) {
const saltNew = bcrypt.genSaltSync(SALT_ROUNDS)
setSalt(saltKey, saltNew)

return false
return false
}
return true
}
return true
return false
}

const setSalt = (saltKey: string, value: string) => {
Expand All @@ -48,16 +55,19 @@ const getSalt = (saltKey: string): string | void => {
}

const setPwdHash = (uid: string, pwd: string): boolean => {
const salt = getSalt(uid.concat(SALT_KEY))
if (!salt) {
return false
if (uid) {
const salt = getSalt(uid.concat(SALT_KEY))
if (!salt) {
return false
}
const hashPwd = bcrypt.hashSync(pwd, salt)
window.localStorage.setItem(uid.concat(HASH_PWD_KEY), hashPwd)

const edata = CryptoJS.AES.encrypt(VERIFY_TEXT, getPwdHash(uid)).toString()
window.localStorage.setItem(uid.concat(VERIFY_TEXT_KEY), edata)
return true
}
const hashPwd = bcrypt.hashSync(pwd, salt)
window.localStorage.setItem(uid.concat(HASH_PWD_KEY), hashPwd)

const edata = CryptoJS.AES.encrypt(VERIFY_TEXT, getPwdHash(uid)).toString()
window.localStorage.setItem(uid.concat(VERIFY_TEXT_KEY), edata)
return true
return false
}

const getPwdHash = (uid: string): string => {
Expand Down Expand Up @@ -125,6 +135,93 @@ const clearUser = (uid: string): void => {
window.localStorage.removeItem(uid.concat(SALT_KEY))
}

const setCurrentRequestQueue = (queue: string): boolean => {
return setQueue(queue, REQUEST_QUEUE_KEY)
}

const setPreQueueRequest = (queue: any[]): boolean => {
return setQueue(JSON.stringify(queue), PRE_QUEUE_REQUEST_KEY)
}

const setQueue = (queue: string, key: string): boolean => {
const uid = getCurrentUserId()
if (uid && queue) {
const hash = getPwdHash(uid)
if (hash) {
const edata = CryptoJS.AES.encrypt(queue, currentUserStore.state.sessionPassword).toString()
window.localStorage.setItem(uid.concat(key), edata)
return true
}
}
return false
}

const getCurrentRequestQueue = (): string => {
return getQueue(REQUEST_QUEUE_KEY)
}

const getPreQueueRequest = (): any[] => {
const requests = getQueue(PRE_QUEUE_REQUEST_KEY)
if (requests) {
return JSON.parse(requests)
}
return []
}

const getQueue = (key: string): string => {
const empty = '[]'
const uid = getCurrentUserId()
if (uid) {
const hash = getPwdHash(uid)
if (hash) {
const edata = window.localStorage.getItem(uid.concat(key))
if (!edata) {
setQueue(empty, key)
} else {
const sessionKey = currentUserStore.state.sessionPassword
const dataBytes = CryptoJS.AES.decrypt(edata, sessionKey)
return dataBytes.toString(CryptoJS.enc.Utf8)
}
}
}
return empty
}

const clearCurrentRequestQueue = (): boolean => {
const uid = getCurrentUserId()
if (uid) {
window.localStorage.removeItem(uid.concat(REQUEST_QUEUE_KEY))
return true
}
return false
}

const clearPreQueueRequest = (): boolean => {
const uid = getCurrentUserId()

if (uid) {
window.localStorage.removeItem(uid.concat(PRE_QUEUE_REQUEST_KEY))
return true
}
return false
}

const setPreQueueLoadRequired = (): void => {
window.localStorage.setItem(PRE_QUEUE_LOAD_REQUIRED, 'true')
}

const clearPreQueueLoadRequired = (): void => {
window.localStorage.setItem(PRE_QUEUE_LOAD_REQUIRED, 'false')
}

const getPreQueueLoadRequired = (): boolean => {
const setting = window.localStorage.getItem(PRE_QUEUE_LOAD_REQUIRED)
if (setting) {
return setting === 'true'
}
return false
}

export {
setCurrentUserId,
getCurrentUserId,
Expand All @@ -137,6 +234,15 @@ export {
getPwdHash,
testPassword,
clearUser,
getCurrentRequestQueue,
setCurrentRequestQueue,
clearCurrentRequestQueue,
getPreQueueRequest,
setPreQueueRequest,
clearPreQueueRequest,
setPreQueueLoadRequired,
clearPreQueueLoadRequired,
getPreQueueLoadRequired,
getAccessToken,
setAccessToken,
APOLLO_KEY
Expand Down
8 changes: 7 additions & 1 deletion packages/webapp/src/utils/queueLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { Operation, FetchResult, NextLink, DocumentNode } from '@apollo/cli
import { ApolloLink } from '@apollo/client/link/core'
import type { Observer } from '@apollo/client/utilities'
import { Observable } from '@apollo/client/utilities'
import { clearPreQueueRequest } from '~utils/localCrypto'

export interface OperationQueueEntry {
operation: Operation
Expand Down Expand Up @@ -46,8 +47,10 @@ export default class QueueLink extends ApolloLink {

public open() {
this.isOpen = true
const opQueueCopy = [...this.opQueue]
let opQueueCopy = []
opQueueCopy = [...this.opQueue]
this.opQueue = []

opQueueCopy.forEach(({ operation, forward, observer }) => {
const key: string = QueueLink.key(operation.operationName, 'dequeue')
if (key in QueueLink.listeners) {
Expand All @@ -61,8 +64,11 @@ export default class QueueLink extends ApolloLink {
listener({ operation, forward, observer })
})
}

forward(operation).subscribe(observer)
})

clearPreQueueRequest()
}

public static addLinkQueueEventListener = (
Expand Down

0 comments on commit 2e20334

Please sign in to comment.