From 0be490bf39d7bcfc3e5d0f07d43f4d6bd763bb07 Mon Sep 17 00:00:00 2001 From: lukew3 Date: Fri, 14 May 2021 21:09:22 -0400 Subject: [PATCH] created axiosInstance with refresh tokens --- gulpfile.js | 11 ++---- models.js | 2 +- server.js | 61 +++++++++++++++++++++++++++++++-- src/js/account-controller.js | 12 ++++--- src/js/axios-instance.js | 65 ++++++++++++++++++++++++++++++++++++ src/js/cloud-functions.js | 2 +- src/js/commandline.js | 6 ++-- src/js/config.js | 4 +-- src/js/db.js | 14 ++++---- src/js/misc.js | 4 +-- src/js/test/test-logic.js | 23 ++++++++++--- src/js/theme-controller.js | 4 +-- 12 files changed, 167 insertions(+), 41 deletions(-) create mode 100644 src/js/axios-instance.js diff --git a/gulpfile.js b/gulpfile.js index 15c254ae1..d2ae8726d 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -14,15 +14,7 @@ sass.compiler = require("dart-sass"); let eslintConfig = { parser: "babel-eslint", - globals: [ - "jQuery", - "$", - "axios", - "Cookies", - "moment", - "html2canvas", - "ClipboardItem", - ], + globals: ["jQuery", "$", "Cookies", "moment", "html2canvas", "ClipboardItem"], envs: ["es6", "browser", "node"], rules: { "constructor-super": "error", @@ -89,6 +81,7 @@ let eslintConfig = { //refactored files, which should be es6 modules //once all files are moved here, then can we use a bundler to its full potential const refactoredSrc = [ + "./src/js/axios-instance.js", "./src/js/db.js", "./src/js/cloud-functions.js", "./src/js/misc.js", diff --git a/models.js b/models.js index 4d6426809..1dad92f95 100644 --- a/models.js +++ b/models.js @@ -36,6 +36,7 @@ const userSchema = new Schema( }, email: { type: String, required: true }, password: { type: String, required: true }, + refreshTokens: [{ type: String, required: true }], }, { timestamps: true, @@ -46,4 +47,3 @@ const userSchema = new Schema( const User = mongoose.model("User", userSchema); module.exports = { User }; -//export User; diff --git a/server.js b/server.js index 200e92b38..eeec8e4c0 100644 --- a/server.js +++ b/server.js @@ -66,6 +66,12 @@ app.post("/api/signIn", (req, res) => { { name: user.name }, process.env.ACCESS_TOKEN_SECRET ); + const refreshToken = jwt.sign( + { name: user.name }, + process.env.REFRESH_TOKEN_SECRET + ); + user.refreshTokens.push(refreshToken); + user.save(); const retUser = { uid: user._id, displayName: user.name, @@ -73,7 +79,11 @@ app.post("/api/signIn", (req, res) => { emailVerified: user.emailVerified, metadata: { creationTime: user.createdAt }, }; - res.json({ accessToken: accessToken, user: retUser }); + res.json({ + accessToken: accessToken, + refreshToken: refreshToken, + user: retUser, + }); } else { //if password doesn't match hash res.status(500).send({ error: "Password invalid" }); @@ -111,6 +121,12 @@ app.post("/api/signUp", (req, res) => { { name: req.body.name }, process.env.ACCESS_TOKEN_SECRET ); + const refreshToken = jwt.sign( + { name: user.name }, + process.env.REFRESH_TOKEN_SECRET + ); + user.refreshTokens.push(refreshToken); + user.save(); const retUser = { uid: user._id, displayName: user.name, @@ -118,7 +134,11 @@ app.post("/api/signUp", (req, res) => { emailVerified: user.emailVerified, metadata: { creationTime: user.createdAt }, }; - res.json({ accessToken: accessToken, user: retUser }); + res.json({ + accessToken: accessToken, + refreshToken: refreshToken, + user: retUser, + }); }) .catch((e) => { console.log(e); @@ -128,6 +148,23 @@ app.post("/api/signUp", (req, res) => { }); }); +app.post("/api/refreshToken", (req, res) => { + const authHeader = req.headers["authorization"]; + const token = authHeader && authHeader.split(" ")[1]; + if (token == null) return res.sendStatus(401); + jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, identity) => { + if (err) return res.sendStatus(403); + User.findOne({ name: identity.name }, (err, user) => { + if (!user.refreshTokens.includes(token)) return res.sendStatus(403); + const accessToken = jwt.sign( + { name: identity.name }, + process.env.ACCESS_TOKEN_SECRET + ); + res.json({ accessToken: accessToken }); + }); + }); +}); + app.post("/api/passwordReset", (req, res) => { const email = req.body.email; //send email to the passed email requesting password reset @@ -136,7 +173,6 @@ app.post("/api/passwordReset", (req, res) => { app.get("/api/fetchSnapshot", authenticateToken, (req, res) => { /* Takes token and returns snap */ - //this is called in init snapshot User.findOne({ name: req.name }, (err, user) => { if (err) res.status(500).send({ error: err }); //populate snap object with data from user document @@ -147,6 +183,25 @@ app.get("/api/fetchSnapshot", authenticateToken, (req, res) => { }); }); +app.post("/api/testCompleted", authenticateToken, (req, res) => { + User.findOne({ name: req.name }, (err, user) => { + if (err) res.status(500).send({ error: err }); + + //Codes from legacy + //1 Saved: No personal best + //2 Saved: Personal best + //-1 Could not save result + //-2 Possible bot detected. Result not saved. + //-3 Could not verify keypress stats. Result not saved. + //-4 Result data does not make sense. Result not saved. + //-5 Test too short. Result not saved. + //-999 Internal error. Result might not be saved. + //return createdId + //return user data + res.json({ snap: snap }); + }); +}); + app.get("/api/userResults", authenticateToken, (req, res) => { User.findOne({ name: req.name }, (err, user) => { if (err) res.status(500).send({ error: err }); diff --git a/src/js/account-controller.js b/src/js/account-controller.js index c8a741c7a..5593b3918 100644 --- a/src/js/account-controller.js +++ b/src/js/account-controller.js @@ -13,16 +13,15 @@ import * as AllTimeStats from "./all-time-stats"; import * as DB from "./db"; import * as TestLogic from "./test-logic"; import * as UI from "./ui"; -import axios from "axios"; import Cookies from "js-cookie"; - +import axiosInstance from "./axios-instance"; //var gmailProvider = new firebase.auth.GoogleAuthProvider(); export function signIn() { $(".pageLogin .preloader").removeClass("hidden"); let email = $(".pageLogin .login input")[0].value; let password = $(".pageLogin .login input")[1].value; - axios + axiosInstance .post("/api/signIn", { email: email, password: password, @@ -32,12 +31,14 @@ export function signIn() { if ($(".pageLogin .login #rememberMe input").prop("checked")) { // TODO: set user login cookie that persists after session Cookies.set("accessToken", response.data.accessToken); + Cookies.set("refreshToken", response.data.refreshToken); Cookies.set("uid", response.data.user._id); Cookies.set("displayName", response.data.user.name); Cookies.set("email", response.data.user.email); } else { //set user login cookie to persist only as long as the session lives Cookies.set("accessToken", response.data.accessToken); + Cookies.set("refreshToken", response.data.refreshToken); Cookies.set("uid", response.data.user._id); Cookies.set("displayName", response.data.user.name); Cookies.set("email", response.data.user.email); @@ -133,7 +134,7 @@ function signUp() { return; } - axios + axiosInstance .post("/api/signUp", { name: nname, email: email, @@ -142,6 +143,7 @@ function signUp() { .then((response) => { let usr = response.data.user; Cookies.set("accessToken", response.data.accessToken); + Cookies.set("refreshToken", response.data.accessToken); Cookies.set("uid", usr._id); Cookies.set("displayName", usr.name); Cookies.set("email", usr.email); @@ -184,7 +186,7 @@ function signUp() { $(".pageLogin #forgotPasswordButton").click((e) => { let email = prompt("Email address"); if (email) { - axios + axiosInstance .post("/api/passwordReset", { email: email, }) diff --git a/src/js/axios-instance.js b/src/js/axios-instance.js new file mode 100644 index 000000000..c81435463 --- /dev/null +++ b/src/js/axios-instance.js @@ -0,0 +1,65 @@ +import Cookies from "js-cookie"; +import axios from "axios"; + +const axiosInstance = axios.create(); + +// Request interceptor for API calls +axiosInstance.interceptors.request.use( + async (config) => { + const accessToken = Cookies.get("accessToken") + ? Cookies.get("accessToken") + : null; + if (accessToken) { + config.headers = { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + "Content-Type": "application/json", + }; + } else { + config.headers = { + Accept: "application/json", + "Content-Type": "application/json", + }; + } + return config; + }, + (error) => { + Promise.reject(error); + } +); + +// Response interceptor for API calls +axiosInstance.interceptors.response.use( + (response) => { + return response; + }, + async function (error) { + const originalRequest = error.config; + if (error.response.status === 401 && !originalRequest._retry) { + originalRequest._retry = true; + //console.log("Refreshing access token"); + const refreshToken = Cookies.get("refreshToken") + ? Cookies.get("refreshToken") + : null; + await axios + .post( + `/api/refreshToken`, + {}, + { headers: { Authorization: `Bearer ${refreshToken}` } } + ) + .then((response) => { + Cookies.set("accessToken", response.data.accessToken); + axios.defaults.headers.common["Authorization"] = + "Bearer " + response.data.accessToken; + }) + .catch((error) => { + console.log(error); + axios.defaults.headers.common["Authorization"] = "Bearer failed"; + }); + return axiosInstance(originalRequest); + } + return Promise.reject(error); + } +); + +export default axiosInstance; diff --git a/src/js/cloud-functions.js b/src/js/cloud-functions.js index bd617f662..7677c08dd 100644 --- a/src/js/cloud-functions.js +++ b/src/js/cloud-functions.js @@ -2,7 +2,7 @@ //export function testCompleted = axios.post('/api/testCompleted', )firebase // .functions() // .httpsCallable("testCompleted"); -import axios from "axios"; +import axiosInstance from "./axios-instance"; export function testCompleted(input) { console.log("testCompleted"); diff --git a/src/js/commandline.js b/src/js/commandline.js index 27757d032..ecb67541c 100644 --- a/src/js/commandline.js +++ b/src/js/commandline.js @@ -4,7 +4,7 @@ import Config, * as UpdateConfig from "./config"; import * as Focus from "./focus"; import * as CommandlineLists from "./commandline-lists"; import * as TestUI from "./test-ui"; -import axios from "axios"; +import axiosInstance from "./axios-instance"; let commandLineMouseMode = false; @@ -164,7 +164,7 @@ function trigger(command) { } }); if (!subgroup && !input && !sticky) { - axios + axiosInstance .post("/api/analytics/usedCommandLine", { command: command }) .catch(() => { console.log("Analytics unavailable"); @@ -324,7 +324,7 @@ $("#commandInput input").keydown((e) => { } } }); - axios + axiosInstance .post("/api/analytics/usedCommandLine", { command: command }) .catch(() => { console.log("Analytics unavailable"); diff --git a/src/js/config.js b/src/js/config.js index 94c394a78..43c2e7413 100644 --- a/src/js/config.js +++ b/src/js/config.js @@ -17,7 +17,7 @@ import * as UI from "./ui"; import * as CommandlineLists from "./commandline-lists"; import * as BackgroundFilter from "./custom-background-filter"; import LayoutList from "./layouts"; -import axios from "axios"; +import axiosInstance from "./axios-instance"; export let localStorageConfig = null; export let dbConfigLoaded = false; @@ -1218,7 +1218,7 @@ export function setLanguage(language, nosave) { language = "english"; } config.language = language; - axios + axiosInstance .post("/api/analytics/changedLanguage", { language: language }) .catch(() => { console.log("Analytics unavailable"); diff --git a/src/js/db.js b/src/js/db.js index a85c3c421..038363f06 100644 --- a/src/js/db.js +++ b/src/js/db.js @@ -2,7 +2,7 @@ import { loadTags } from "./result-filters"; import * as AccountButton from "./account-button"; import * as CloudFunctions from "./cloud-functions"; import * as Notifications from "./notifications"; -import axios from "axios"; +import axiosInstance from "./axios-instance"; import Cookies from "js-cookie"; //const db = firebase.firestore(); @@ -14,7 +14,7 @@ let dbSnapshot = null; export function updateName(uid, name) { //db.collection(`users`).doc(uid).set({ name: name }, { merge: true }); - axios.post("/api/updateName", { + axiosInstance.post("/api/updateName", { uid: uid, name: name, }); @@ -52,10 +52,8 @@ export async function initSnapshot() { //send api request with token that returns tags, presets, and data needed for snap if (currentUser() == null) return false; const token = Cookies.get("accessToken"); - await axios - .get("/api/fetchSnapshot", { - headers: { Authorization: `Bearer ${token}` }, - }) + await axiosInstance + .get("/api/fetchSnapshot") .then((response) => { dbSnapshot = response.data.snap; loadTags(dbSnapshot.tags); @@ -73,7 +71,7 @@ export async function getUserResults() { if (dbSnapshot.results !== undefined) { return true; } else { - axios + axiosInstance .get("/api/userResults", { uid: user.uid, }) @@ -435,7 +433,7 @@ export function updateLbMemory(mode, mode2, type, value) { export async function saveConfig(config) { if (currentUser() !== null) { AccountButton.loading(true); - axios + axiosInstance .post("/api/saveConfig", { uid: currentUser().uid, }) diff --git a/src/js/misc.js b/src/js/misc.js index dde501b1b..d108b31b9 100644 --- a/src/js/misc.js +++ b/src/js/misc.js @@ -1,6 +1,6 @@ import * as Loader from "./loader"; import * as DB from "./db"; -import axios from "axios"; +import axiosInstance from "./axios-instance"; export function getuid() { console.error("Only share this uid with Miodec and nobody else!"); @@ -316,7 +316,7 @@ export function migrateFromCookies() { export function sendVerificationEmail() { Loader.show(); let cu = DB.currentUser(); - axios + axiosInstance .post("/api/sendEmailVerification", { uid: cu.uid, }) diff --git a/src/js/test/test-logic.js b/src/js/test/test-logic.js index ae5efe193..faaef12c9 100644 --- a/src/js/test/test-logic.js +++ b/src/js/test/test-logic.js @@ -28,6 +28,7 @@ import * as ThemeColors from "./theme-colors"; import * as CloudFunctions from "./cloud-functions"; import * as TestLeaderboards from "./test-leaderboards"; import * as Replay from "./replay.js"; +import axiosInstance from "./axios-instance"; export let notSignedInLastResult = null; @@ -311,9 +312,9 @@ export function startTest() { } try { if (DB.currentUser() != null) { - axios.post("/api/analytics/testStarted"); + axiosInstance.post("/api/analytics/testStarted"); } else { - axios.post("/api/analytics/testStartedNoLogin"); + axiosInstance.post("/api/analytics/testStartedNoLogin"); } } catch (e) { console.log("Analytics unavailable"); @@ -1541,6 +1542,18 @@ export function finish(difficultyFailed = false) { `checking ` ); } + const token = Cookies.get("accessToken"); + axiosInstance + .post("/api/testCompleted", { + obj: completedEvent, + }) + .then((response) => { + //return a result message that will be shown if there was an error + }) + .catch((error) => { + Notifications.add(error, -1); + }); + CloudFunctions.testCompleted({ uid: DB.currentUser().uid, obj: completedEvent, @@ -1616,7 +1629,7 @@ export function finish(difficultyFailed = false) { } } - axios + axiosInstance .post("/api/analytics/testCompleted", { completedEvent: completedEvent, }) @@ -1657,7 +1670,7 @@ export function finish(difficultyFailed = false) { }); }); } else { - axios + axiosInstance .post("/api/analytics/testCompletedNoLogin", { completedEvent: completedEvent, }) @@ -1669,7 +1682,7 @@ export function finish(difficultyFailed = false) { } else { Notifications.add("Test invalid", 0); TestStats.setInvalid(); - axios + axiosInstance .post("/api/analytics/testCompletedInvalid", { completedEvent: completedEvent, }) diff --git a/src/js/theme-controller.js b/src/js/theme-controller.js index 6e2feeb7d..41d44cf22 100644 --- a/src/js/theme-controller.js +++ b/src/js/theme-controller.js @@ -5,7 +5,7 @@ import * as Notifications from "./notifications"; import Config from "./config"; import * as UI from "./ui"; import tinycolor from "tinycolor2"; -import axios from "axios"; +import axiosInstance from "./axios-instance"; let isPreviewingTheme = false; export let randomTheme = null; @@ -96,7 +96,7 @@ export function apply(themeName) { }); } - axios + axiosInstance .post("/api/analytics/changedTheme", { theme: themeName, })