Web Worker Sign Out and Use Go API (#13)

* Calculate checksum and send with file upload

* Limit file upload and display rejected file error

* Add sign in timeout

* Check auth token structure before setting
Clean up

* Use web worker timer to sign out
Remove checksum
Point to Go ota update

* Remove checksum dependency
This commit is contained in:
John Wu
2021-02-08 15:23:36 -08:00
committed by GitHub
parent 1ddf9fe813
commit e1f0006d5e
13 changed files with 1630 additions and 2173 deletions

3601
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -30,7 +30,6 @@ exports[`App Route / authenticated 1`] = `
<input <input
accept="" accept=""
autocomplete="off" autocomplete="off"
multiple=""
style="display: none;" style="display: none;"
tabindex="-1" tabindex="-1"
type="file" type="file"
@@ -263,7 +262,6 @@ exports[`App Route /home authenticated 1`] = `
<input <input
accept="" accept=""
autocomplete="off" autocomplete="off"
multiple=""
style="display: none;" style="display: none;"
tabindex="-1" tabindex="-1"
type="file" type="file"
@@ -532,7 +530,6 @@ exports[`App Route /signup authenticated 1`] = `
<input <input
accept="" accept=""
autocomplete="off" autocomplete="off"
multiple=""
style="display: none;" style="display: none;"
tabindex="-1" tabindex="-1"
type="file" type="file"

View File

@@ -51,6 +51,13 @@ export const FileUploadProvider = ({ children }) => {
} }
}; };
const rejectedFile = (files) => {
if (files.length === 0) return;
setUploading(true);
setStatus(`Rejected ${files[0].name} too large`);
setProgress(-1);
};
return ( return (
<FileUploadContext.Provider value={{ <FileUploadContext.Provider value={{
uploading, uploading,
@@ -59,6 +66,7 @@ export const FileUploadProvider = ({ children }) => {
linkURL, linkURL,
upload, upload,
cancel, cancel,
rejectedFile,
}}> }}>
{children} {children}
</FileUploadContext.Provider> </FileUploadContext.Provider>

View File

@@ -9,7 +9,7 @@ describe("FileUploadContext", () => {
beforeEach(() => { beforeEach(() => {
const TestComp = () => { const TestComp = () => {
const { progress, uploading, status, linkURL, upload, cancel } = useFileUploadContext(); const { progress, uploading, status, linkURL, upload, cancel } = useFileUploadContext();
const TEST_FILE = [{ file: { name: "test.jpg" }}]; const TEST_FILE = [{ file: { name: "test.jpg", size: 0 }}];
const TEST_ACCESSTOKEN = "ACCESSTOKEN"; const TEST_ACCESSTOKEN = "ACCESSTOKEN";
return ( return (
<> <>

View File

@@ -1,5 +1,6 @@
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from "react";
import auth from '../../services/auth'; import auth from "../../services/auth";
import getTimerWorker from "../../services/timer";
const UserContext = React.createContext(); const UserContext = React.createContext();
@@ -7,23 +8,45 @@ export const UserProvider = ({ children }) => {
const [fetching, setFetching] = useState(false); const [fetching, setFetching] = useState(false);
const [token, setToken] = useState(null); const [token, setToken] = useState(null);
const [error, setError] = useState(null); const [error, setError] = useState(null);
let timer;
useEffect(() => { useEffect(() => {
if (!localStorage) return; if (!localStorage) return;
const token = JSON.parse(localStorage.getItem("token")); const t = JSON.parse(localStorage.getItem("token"));
if (!t || !t.idToken || !t.idToken.jwtToken) return;
if (!t.idToken.payload || !t.idToken.payload.exp) return;
setToken(t);
}, []);
useEffect(() => {
if (!token) return; if (!token) return;
const { idToken: { jwtToken }} = token; const { idToken: { jwtToken }} = token;
const verifyToken = async (accessToken) => {
const result = await auth.verify(accessToken);
if (result.authenticated) {
setToken(token);
} else {
await signOut();
}
};
verifyToken(jwtToken); verifyToken(jwtToken);
return () => {}; return () => {
}, []); if (timer) timer.terminate();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token])
const verifyToken = async (accessToken) => {
const result = await auth.verify(accessToken);
if (!result.authenticated || !token.idToken.payload || !token.idToken.payload.exp) {
signOut();
return;
}
const duration = (1000 * token.idToken.payload.exp) - (new Date()).getTime();
if (!timer) {
timer = getTimerWorker();
timer.onMessage((e) => {
if (e.data === "timeout") {
signOut();
}
})
}
timer.start(duration);
};
const signIn = async (username, password) => { const signIn = async (username, password) => {
let result = null; let result = null;
@@ -92,7 +115,7 @@ export const UserProvider = ({ children }) => {
return result; return result;
}; };
const signOut = async () => { const signOut = () => {
setToken(null); setToken(null);
if (!localStorage) return; if (!localStorage) return;
localStorage.removeItem("token"); localStorage.removeItem("token");

View File

@@ -1,10 +1,17 @@
jest.mock("../../services/auth"); jest.mock("../../services/auth");
jest.mock("../../services/timer");
import {render, cleanup, screen, fireEvent, waitFor} from "@testing-library/react" import {render, cleanup, screen, fireEvent, waitFor} from "@testing-library/react"
import { UserProvider, useUserContext } from "../Contexts/UserContext"; import { UserProvider, useUserContext } from "../Contexts/UserContext";
import auth from "../../services/auth"; import auth from "../../services/auth";
import getTimerWorker from "../../services/timer";
const TEST_TOKEN = { idToken: { jwtToken: "TEST" }}; const TEST_TOKEN = { idToken: {
jwtToken: "TEST",
payload: {
exp: (new Date().getTime() / 1000)
}
}};
describe("UseContext", () => { describe("UseContext", () => {
@@ -114,14 +121,22 @@ describe("UseContext", () => {
it("No error sign in", async () => { it("No error sign in", async () => {
const TOKEN_STRING = JSON.stringify(TEST_TOKEN); const TOKEN_STRING = JSON.stringify(TEST_TOKEN);
const timer = getTimerWorker();
auth.setSignInResponse(TEST_TOKEN); auth.setSignInResponse(TEST_TOKEN);
auth.setVerifyResponse({ authenticated: true })
fireEvent.click(screen.getByTestId("signIn")); fireEvent.click(screen.getByTestId("signIn"));
await waitFor(() => expect(screen.getByTestId("fetching").innerHTML).toEqual("false")); await waitFor(() => expect(screen.getByTestId("fetching").innerHTML).toEqual("false"));
expect(screen.getByTestId("error").innerHTML).toEqual(""); expect(screen.getByTestId("error").innerHTML).toEqual("");
expect(screen.getByTestId("token").innerHTML).toEqual(TOKEN_STRING); expect(screen.getByTestId("token").innerHTML).toEqual(TOKEN_STRING);
if (!localStorage) return; expect(timer.start.mock.calls.length).toEqual(1);
expect(localStorage.getItem("token")).toEqual(TOKEN_STRING); expect(timer.onMessage.mock.calls.length).toEqual(1);
localStorage.removeItem("token"); expect(timer.stop.mock.calls.length).toEqual(0);
expect(timer.terminate.mock.calls.length).toEqual(0);
if (!localStorage) {
expect(localStorage.getItem("token")).toEqual(TOKEN_STRING);
localStorage.removeItem("token");
}
}); });
it("Handle server error", async () => { it("Handle server error", async () => {

View File

@@ -27,7 +27,6 @@ exports[`File Upload Form Should render 1`] = `
<input <input
accept="" accept=""
autocomplete="off" autocomplete="off"
multiple=""
style="display: none;" style="display: none;"
tabindex="-1" tabindex="-1"
type="file" type="file"

View File

@@ -7,15 +7,18 @@ import ModalProgressBar from "../ModalProgressBar";
import useStyles from "../Styles"; import useStyles from "../Styles";
const FileUploadZone = ({ classes, token }) => { const FileUploadZone = ({ classes, token }) => {
const { upload } = useFileUploadContext(); const { upload, rejectedFile } = useFileUploadContext();
const { token: { idToken: { jwtToken : authToken } } } = useUserContext(); const { token: { idToken: { jwtToken : authToken } } } = useUserContext();
return ( return (
<form className={classes.form} noValidate> <form className={classes.form} noValidate>
<DropzoneAreaBase <DropzoneAreaBase
maxFileSize={5e+7} id="dropzone"
maxFileSize={1e+9}
filesLimit={1}
showAlerts={false} showAlerts={false}
onAdd={(files) => upload(files, authToken)} onAdd={(files) => upload(files, authToken)}
onDropRejected={(files) => { rejectedFile(files) }}
/> />
<ModalProgressBar /> <ModalProgressBar />
</form> </form>

View File

@@ -0,0 +1,12 @@
const timer = {
start: jest.fn(),
stop: jest.fn(),
onMessage: jest.fn(),
terminate: jest.fn()
}
const getTimerWorker = () => {
return timer;
}
export default getTimerWorker;

View File

@@ -11,7 +11,7 @@ export const getCancelToken = () => {
return issuedCancelToken; return issuedCancelToken;
} }
export const uploadFile = async (file, token, onProgress, cancelToken) => { export const uploadFile = async (file, token, onProgress, cancelToken, hash) => {
if (!uploadFileDelay) return uploadFileResponse; if (!uploadFileDelay) return uploadFileResponse;
onProgress(50); onProgress(50);
await delay(10000); await delay(10000);

View File

@@ -0,0 +1,41 @@
const workerscript = () => {
var INTERVAL = 60000;
// eslint-disable-next-line no-restricted-globals
var me = self;
var timerID = 0;
var deadline;
function startTimer(duration) {
deadline = new Date(Date.now() + duration - INTERVAL);
stopTimer();
timerID = setInterval(function() {
var now = new Date();
if (now > deadline) {
me.postMessage("timeout");
stopTimer();
}
}, INTERVAL);
}
function stopTimer() {
if (timerID > 0) clearInterval(timerID);
timerID = 0;
}
me.onmessage = function(e) {
if (e.data.action === "start") {
startTimer(e.data.duration);
}
else if (e.data.action === "stop") {
stopTimer();
}
}
};
let code = workerscript.toString();
code = code.substring(code.indexOf("{")+1, code.lastIndexOf("}"));
const blob = new Blob([code], {type: "application/javascript"});
const timeout_script = URL.createObjectURL(blob);
module.exports = timeout_script;

40
src/services/timer.js Normal file
View File

@@ -0,0 +1,40 @@
import worker_script from "./timeoutScript";
const getTimerWorker = () => {
const worker = new Worker(worker_script);
let messageHandler = null;
const workerHandler = (e) => {
if (messageHandler === null) return;
messageHandler(e);
}
worker.addEventListener("message", workerHandler);
return {
start: (duration) => {
if (!worker) return;
worker.postMessage({
action: "start",
duration,
});
},
stop: () => {
worker.postMessage({
action: "stop",
});
},
onMessage: (handler) => {
messageHandler = handler;
},
terminate: () => {
worker.removeEventListener("message", workerHandler);
worker.terminate();
messageHandler = null;
}
}
}
export default getTimerWorker;

View File

@@ -1,6 +1,6 @@
import axios from 'axios'; import axios from 'axios';
const UPLOAD_ENDPOINT = process.env.REACT_APP_UPLOAD_SERVICE_URL || "https://gw-dev.fiskerdps.com"; const UPLOAD_ENDPOINT = process.env.REACT_APP_UPLOAD_SERVICE_URL || "https://gw-dev.fiskerdps.com/ota_update";
export const getCancelToken = () => { export const getCancelToken = () => {
const token = axios.CancelToken; const token = axios.CancelToken;
@@ -10,10 +10,10 @@ export const getCancelToken = () => {
export const uploadFile = (file, token, onProgress, cancelToken) => { export const uploadFile = (file, token, onProgress, cancelToken) => {
const form = new FormData(); const form = new FormData();
let options = { let options = {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'multipart/form-data', "Content-Type": "multipart/form-data",
'Authorization': `Bearer ${token}`, "Authorization": `Bearer ${token}`,
}, },
cancelToken, cancelToken,
}; };