diff --git a/tnoodle-ui/src/main/api/tnoodle.api.js b/tnoodle-ui/src/main/api/tnoodle.api.js index 79d90918a..fff392da2 100644 --- a/tnoodle-ui/src/main/api/tnoodle.api.js +++ b/tnoodle-ui/src/main/api/tnoodle.api.js @@ -1,5 +1,5 @@ let backendUrl = new URL(window.location.origin); -export const tNoodleBackend = backendUrl.toString().replace(/\/$/g, ''); +export const tNoodleBackend = backendUrl.toString().replace(/\/$/g, ""); let zipEndpoint = "/wcif/zip"; let versionEndpoint = "/version"; @@ -9,7 +9,13 @@ let bestMbldAttemptEndpoint = "/frontend/mbld/best"; let wcaEventsEndpoint = "/frontend/data/events"; let formatsEndpoint = "/frontend/data/formats"; -export const fetchZip = (scrambleClient, wcif, mbld, password, translations) => { +export const fetchZip = ( + scrambleClient, + wcif, + mbld, + password, + translations +) => { let payload = { wcif, multiCubes: { requestedScrambles: mbld }, @@ -20,13 +26,14 @@ export const fetchZip = (scrambleClient, wcif, mbld, password, translations) => payload.zipPassword = password; } - return scrambleClient.loadScrambles(zipEndpoint, payload, wcif.id) + return scrambleClient + .loadScrambles(zipEndpoint, payload, wcif.id) .then((result) => convertToBlob(result)) .catch((error) => console.error(error)); }; const convertToBlob = async (result) => { - let {contentType, payload} = result; + let { contentType, payload } = result; let res = await fetch(`data:${contentType};base64,${payload}`); return await res.blob(); diff --git a/tnoodle-ui/src/main/api/wca.api.js b/tnoodle-ui/src/main/api/wca.api.js index 8c336e3ed..b8e3e1062 100644 --- a/tnoodle-ui/src/main/api/wca.api.js +++ b/tnoodle-ui/src/main/api/wca.api.js @@ -1,12 +1,17 @@ import { BASE_PATH } from "../../App"; +import { getHashParameter, getQueryParameter } from "../util/query.param.util"; // Members of the Software Team can configure this here: https://www.worldcubeassociation.org/oauth/applications/123. const TNOODLE_ACCESS_TOKEN_KEY = "TNoodle.accessToken"; const TNOODLE_LAST_LOGIN_ENV = "TNoodle.lastLoginEnv"; +const TNOODLE_EXPIRATION = "TNoodle.expiration"; const STAGING = "staging"; const PRODUCTION = "production"; +const ACCESS_TOKEN = "access_token"; +const EXPIRES_IN = "expires_in"; + // See https://github.com/thewca/worldcubeassociation.org/wiki/OAuth-documentation-notes#staging-oauth-application let getWcaOrigin = () => { if (isUsingStaging()) { @@ -28,13 +33,21 @@ let getTnoodleAppId = () => { ); }; -let wcaAccessToken = getHashParameter("access_token"); -if (wcaAccessToken) { +let wcaAccessToken = getHashParameter(ACCESS_TOKEN); +let expiresIn = getHashParameter(EXPIRES_IN); +if (!!wcaAccessToken) { window.location.hash = ""; + + let now = new Date(); + let expiration = now.setSeconds(now.getSeconds() + Number(expiresIn)); + localStorage[TNOODLE_ACCESS_TOKEN_KEY] = wcaAccessToken; + localStorage[TNOODLE_EXPIRATION] = expiration; + gotoPreLoginPath(); } else { wcaAccessToken = localStorage[TNOODLE_ACCESS_TOKEN_KEY]; + expiresIn = localStorage[TNOODLE_EXPIRATION]; } export function isUsingStaging() { @@ -59,16 +72,34 @@ export function logIn() { } export function isLogged() { - return ( - localStorage[TNOODLE_ACCESS_TOKEN_KEY] != null && - getCurrentEnv() === getLastLoginEnv() - ); + if (localStorage[TNOODLE_ACCESS_TOKEN_KEY] == null) { + return false; + } + + if (getCurrentEnv() !== getLastLoginEnv()) { + return false; + } + + let expiration = localStorage[TNOODLE_EXPIRATION]; + + if (expiration == null) { + return false; + } + + if (new Date() < new Date(Number(expiration))) { + return true; + } + + logOut(); + return false; } export function logOut() { delete localStorage[TNOODLE_LAST_LOGIN_ENV]; delete localStorage[TNOODLE_ACCESS_TOKEN_KEY]; + delete localStorage[TNOODLE_EXPIRATION]; wcaAccessToken = null; + expiresIn = null; window.location.reload(); } @@ -104,33 +135,6 @@ export function getUpcomingManageableCompetitions() { ).then((response) => response.json()); } -function getHashParameter(name) { - return parseQueryString(window.location.hash)[name]; -} - -export function getQueryParameter(name) { - let urlSplit = window.location.href.split("?"); - let lastElement = urlSplit.slice(-1)[0]; - return parseQueryString(lastElement)[name]; -} - -// Copied from https://stackoverflow.com/a/3855394/1739415 -function parseQueryString(query) { - if (!query) { - return {}; - } - - return (/^[?#]/.test(query) ? query.slice(1) : query) - .split("&") - .reduce((params, param) => { - let [key, value] = param.split("="); - params[key] = value - ? decodeURIComponent(value.replace(/\+/g, " ")) - : ""; - return params; - }, {}); -} - const getLastLoginEnv = () => localStorage[TNOODLE_LAST_LOGIN_ENV]; const getCurrentEnv = () => (isUsingStaging() ? STAGING : PRODUCTION); diff --git a/tnoodle-ui/src/main/components/FmcTranslationsDetail.css b/tnoodle-ui/src/main/components/FmcTranslationsDetail.css new file mode 100644 index 000000000..7a1903573 --- /dev/null +++ b/tnoodle-ui/src/main/components/FmcTranslationsDetail.css @@ -0,0 +1,3 @@ +.fmc-label { + text-transform: capitalize; +} diff --git a/tnoodle-ui/src/main/components/FmcTranslationsDetail.jsx b/tnoodle-ui/src/main/components/FmcTranslationsDetail.jsx index fc43b5e2a..047ef636d 100644 --- a/tnoodle-ui/src/main/components/FmcTranslationsDetail.jsx +++ b/tnoodle-ui/src/main/components/FmcTranslationsDetail.jsx @@ -1,6 +1,5 @@ import React, { Component } from "react"; import { connect } from "react-redux"; -import { capitalize } from "../util/string.util"; import _ from "lodash"; import { updateFileZipBlob, @@ -9,6 +8,7 @@ import { resetTranslations, setSuggestedFmcTranslations, } from "../redux/ActionCreators"; +import "./FmcTranslationsDetail.css"; const TRANSLATIONS_PER_LINE = 3; @@ -30,8 +30,8 @@ const FmcTranslationsDetail = connect( mapDispatchToProps )( class extends Component { - constructor(props) { - super(props); + constructor() { + super(); this.state = { showTranslations: false }; } @@ -123,13 +123,14 @@ const FmcTranslationsDetail = connect( > diff --git a/tnoodle-ui/src/main/components/SideBar.jsx b/tnoodle-ui/src/main/components/SideBar.jsx index 19796a9aa..7c566b3da 100644 --- a/tnoodle-ui/src/main/components/SideBar.jsx +++ b/tnoodle-ui/src/main/components/SideBar.jsx @@ -22,8 +22,8 @@ import { fetchMe, getUpcomingManageableCompetitions, getCompetitionJson, - getQueryParameter, } from "../api/wca.api"; +import { getQueryParameter } from "../util/query.param.util"; import { fetchSuggestedFmcTranslations, fetchBestMbldAttempt, diff --git a/tnoodle-ui/src/main/util/query.param.util.js b/tnoodle-ui/src/main/util/query.param.util.js new file mode 100644 index 000000000..10bc05b64 --- /dev/null +++ b/tnoodle-ui/src/main/util/query.param.util.js @@ -0,0 +1,26 @@ +export function getHashParameter(name) { + return parseQueryString(window.location.hash)[name]; +} + +export function getQueryParameter(name) { + let urlSplit = window.location.href.split("?"); + let lastElement = urlSplit.slice(-1)[0]; + return parseQueryString(lastElement)[name]; +} + +// Copied from https://stackoverflow.com/a/3855394/1739415 +function parseQueryString(query) { + if (!query) { + return {}; + } + + return (/^[?#]/.test(query) ? query.slice(1) : query) + .split("&") + .reduce((params, param) => { + let [key, value] = param.split("="); + params[key] = value + ? decodeURIComponent(value.replace(/\+/g, " ")) + : ""; + return params; + }, {}); +} diff --git a/tnoodle-ui/src/main/util/string.util.js b/tnoodle-ui/src/main/util/string.util.js deleted file mode 100644 index 8205f0b41..000000000 --- a/tnoodle-ui/src/main/util/string.util.js +++ /dev/null @@ -1 +0,0 @@ -export const capitalize = (string) => string[0].toUpperCase() + string.slice(1);