Merge pull request #498 from RickCarlino/fix_auth_errors_II
(WIP) Fix more auth issues, refactorings.
This commit is contained in:
commit
34799c954e
|
@ -12,7 +12,8 @@ module CeleryScriptSettingsBag
|
|||
move_relative write_pin read_pin send_message
|
||||
factory_reset execute_script set_user_env wait
|
||||
add_point install_farmware update_farmware zero
|
||||
remove_farmware take_photo data_update find_home)
|
||||
remove_farmware take_photo data_update find_home
|
||||
install_first_party_farmware)
|
||||
ALLOWED_PACKAGES = %w(farmbot_os arduino_firmware)
|
||||
ALLOWED_CHAGES = %w(add remove update)
|
||||
RESOURCE_NAME = %w(images plants regimens peripherals
|
||||
|
|
|
@ -8,8 +8,7 @@ conf.output = {
|
|||
// must match config.webpack.output_dir
|
||||
path: path.join(__dirname, '..', 'public', 'webpack'),
|
||||
publicPath: '//localhost:' + devServerPort + '/webpack/',
|
||||
filename: '[name].js',
|
||||
watch: true
|
||||
filename: '[name].js'
|
||||
};
|
||||
|
||||
conf.devServer = {
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
"start": "echo '===We use `rails api:start` now.==='",
|
||||
"heroku-postbuild": "webpack --config=./config/webpack.prod.js",
|
||||
"dev": "echo '===We use `rails api:start` now.==='",
|
||||
"webpack": "./node_modules/.bin/webpack-dev-server --config config/webpack.config.js --host 0.0.0.0",
|
||||
"webpack": "./node_modules/.bin/webpack-dev-server --config config/webpack.dev.js --host 0.0.0.0",
|
||||
"test": "jest --coverage --no-cache",
|
||||
"typecheck": "tsc --noEmit --jsx preserve"
|
||||
},
|
||||
|
|
|
@ -1,38 +1,47 @@
|
|||
jest.mock("axios", () => ({
|
||||
default: {
|
||||
get() {
|
||||
return Promise.reject("NO");
|
||||
}
|
||||
interceptors: {
|
||||
response: { use: jest.fn() },
|
||||
request: { use: jest.fn() }
|
||||
},
|
||||
get() { return Promise.reject("NO"); }
|
||||
}
|
||||
}));
|
||||
|
||||
import { AuthState } from "../auth/interfaces";
|
||||
jest.mock("../session", () => {
|
||||
return {
|
||||
Session: {
|
||||
clear: jest.fn(),
|
||||
getBool: jest.fn(),
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
import { maybeRefreshToken } from "../refresh_token";
|
||||
import { API } from "../api/index";
|
||||
import { Session } from "../session";
|
||||
|
||||
API.setBaseUrl("http://blah.whatever.party");
|
||||
|
||||
const fakeAuth = (jti = "456"): AuthState => ({
|
||||
token: {
|
||||
encoded: "---",
|
||||
unencoded: {
|
||||
iat: 123,
|
||||
jti,
|
||||
iss: "---",
|
||||
exp: 456,
|
||||
mqtt: "---",
|
||||
os_update_server: "---"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe("maybeRefreshToken()", () => {
|
||||
|
||||
it("gives you the old token when it cant find a new one", (done) => {
|
||||
maybeRefreshToken(fakeAuth("111"))
|
||||
.then((nextToken) => {
|
||||
expect(nextToken.token.unencoded.jti).toEqual("111");
|
||||
done();
|
||||
});
|
||||
it("logs you out when a refresh fails", (done) => {
|
||||
const t = {
|
||||
token: {
|
||||
encoded: "---",
|
||||
unencoded: {
|
||||
iat: 123,
|
||||
jti: "111",
|
||||
iss: "---",
|
||||
exp: 456,
|
||||
mqtt: "---",
|
||||
os_update_server: "---"
|
||||
}
|
||||
}
|
||||
};
|
||||
maybeRefreshToken(t).then(() => {
|
||||
expect(Session.clear).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,6 +16,10 @@ jest.mock("axios", () => ({
|
|||
default: {
|
||||
get() {
|
||||
return Promise.resolve({ data: mockAuth("000") });
|
||||
},
|
||||
interceptors: {
|
||||
response: { use: jest.fn() },
|
||||
request: { use: jest.fn() }
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
@ -24,7 +28,7 @@ import { AuthState } from "../auth/interfaces";
|
|||
import { maybeRefreshToken } from "../refresh_token";
|
||||
import { API } from "../api/index";
|
||||
|
||||
API.setBaseUrl("http://whateer.party");
|
||||
API.setBaseUrl("http://whatever.party");
|
||||
|
||||
describe("maybeRefreshToken()", () => {
|
||||
it("gives you back your token when things fail", (done) => {
|
||||
|
|
|
@ -8,7 +8,8 @@ import {
|
|||
semverCompare,
|
||||
SemverResult,
|
||||
trim,
|
||||
bitArray
|
||||
bitArray,
|
||||
withTimeout
|
||||
} from "../util";
|
||||
describe("util", () => {
|
||||
describe("safeStringFetch", () => {
|
||||
|
@ -155,3 +156,21 @@ describe("bitArray", () => {
|
|||
expect(bitArray(true, true)).toBe(0b11);
|
||||
});
|
||||
});
|
||||
|
||||
describe("withTimeout()", () => {
|
||||
it("rejects promises that do not meet a particular deadline", (done) => {
|
||||
const p = new Promise(res => setTimeout(() => res("Done"), 10));
|
||||
withTimeout(1, p).then(fail, (y) => {
|
||||
expect(y).toContain("Timed out");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves promises that meet a particular deadline", (done) => {
|
||||
withTimeout(10, new Promise(res => setTimeout(() => res("Done"), 1)))
|
||||
.then(y => {
|
||||
expect(y).toContain("Done");
|
||||
done();
|
||||
}, fail);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,7 +21,7 @@ import { connectDevice } from "../connectivity/connect_device";
|
|||
export function didLogin(authState: AuthState, dispatch: Function) {
|
||||
API.setBaseUrl(authState.token.unencoded.iss);
|
||||
dispatch(fetchReleases(authState.token.unencoded.os_update_server));
|
||||
dispatch(loginOk(authState));
|
||||
dispatch(setToken(authState));
|
||||
Sync.fetchSyncData(dispatch);
|
||||
dispatch(connectDevice(authState));
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ export function didLogin(authState: AuthState, dispatch: Function) {
|
|||
function onLogin(dispatch: Function) {
|
||||
return (response: HttpData<AuthState>) => {
|
||||
const { data } = response;
|
||||
Session.replace(data);
|
||||
Session.replaceToken(data);
|
||||
didLogin(data, dispatch);
|
||||
push("/app/controls");
|
||||
};
|
||||
|
@ -53,12 +53,12 @@ function loginErr() {
|
|||
/** Very important. Once called, all outbound HTTP requests will
|
||||
* have a JSON Web Token attached to their "Authorization" header,
|
||||
* thereby granting access to the API. */
|
||||
export function loginOk(auth: AuthState): ReduxAction<AuthState> {
|
||||
export function setToken(auth: AuthState): ReduxAction<AuthState> {
|
||||
axios.interceptors.response.use(responseFulfilled, responseRejected);
|
||||
axios.interceptors.request.use(requestFulfilled(auth));
|
||||
|
||||
return {
|
||||
type: Actions.LOGIN_OK,
|
||||
type: Actions.REPLACE_TOKEN,
|
||||
payload: auth
|
||||
};
|
||||
}
|
||||
|
@ -120,7 +120,10 @@ export function logout() {
|
|||
// In those cases, seeing a logout message may confuse the user.
|
||||
// To circumvent this, we must check if the user had a token.
|
||||
// If there was infact a token, we can safely show the message.
|
||||
if (Session.getAll()) { success(t("You have been logged out.")); }
|
||||
if (Session.fetchStoredToken()) {
|
||||
success(t("You have been logged out."));
|
||||
}
|
||||
|
||||
Session.clear();
|
||||
// Technically this is unreachable code:
|
||||
return {
|
||||
|
|
|
@ -3,6 +3,6 @@ import { generateReducer } from "../redux/generate_reducer";
|
|||
import { Actions } from "../constants";
|
||||
|
||||
export let authReducer = generateReducer<AuthState | undefined>(undefined)
|
||||
.add<AuthState>(Actions.LOGIN_OK, (s, { payload }) => {
|
||||
.add<AuthState>(Actions.REPLACE_TOKEN, (s, { payload }) => {
|
||||
return payload;
|
||||
});
|
||||
|
|
|
@ -1,23 +1,45 @@
|
|||
jest.unmock("../../auth/actions");
|
||||
const actions = require("../../auth/actions");
|
||||
const didLogin = jest.fn();
|
||||
const mockState = {
|
||||
auth: {
|
||||
token: {
|
||||
unencoded: { iss: "http://geocities.com" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
jest.mock("axios", () => ({
|
||||
default: {
|
||||
interceptors: {
|
||||
response: { use: jest.fn() },
|
||||
request: { use: jest.fn() }
|
||||
},
|
||||
get() { return Promise.resolve({ data: mockState }); }
|
||||
}
|
||||
}));
|
||||
|
||||
jest.mock("../../session", () => ({
|
||||
Session: {
|
||||
fetchStoredToken: jest.fn(),
|
||||
getNum: () => undefined,
|
||||
getBool: () => undefined,
|
||||
getAll: () => undefined
|
||||
}
|
||||
}));
|
||||
actions.didLogin = didLogin;
|
||||
import { ready } from "../actions";
|
||||
|
||||
const STUB_STATE = { auth: "FOO BAR BAZ" };
|
||||
jest.mock("../../auth/actions", () => ({
|
||||
didLogin: jest.fn(),
|
||||
setToken: jest.fn()
|
||||
}));
|
||||
|
||||
import { ready } from "../actions";
|
||||
import { setToken } from "../../auth/actions";
|
||||
|
||||
describe("Actions", () => {
|
||||
it("fetches configs and calls didLogin()", () => {
|
||||
it("calls didLogin()", () => {
|
||||
jest.resetAllMocks();
|
||||
const dispatch = jest.fn();
|
||||
const getState = jest.fn(() => STUB_STATE);
|
||||
const getState = jest.fn(() => mockState);
|
||||
const thunk = ready();
|
||||
thunk(dispatch, getState);
|
||||
expect(didLogin.mock.calls.length).toBe(1);
|
||||
expect(setToken).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,13 +1,31 @@
|
|||
import { didLogin } from "../auth/actions";
|
||||
import { didLogin, setToken } from "../auth/actions";
|
||||
import { Thunk } from "../redux/interfaces";
|
||||
import { Session } from "../session";
|
||||
import { maybeRefreshToken } from "../refresh_token";
|
||||
import { withTimeout } from "../util";
|
||||
import { AuthState } from "../auth/interfaces";
|
||||
|
||||
/** Lets Redux know that the app is ready to bootstrap. */
|
||||
export const storeToken = (auth: AuthState, dispatch: Function) => () => {
|
||||
dispatch(setToken(auth));
|
||||
didLogin(auth, dispatch);
|
||||
};
|
||||
|
||||
/** Amount of time we're willing to wait before concluding that the token is bad
|
||||
* or the API is down. */
|
||||
const MAX_TOKEN_WAIT_TIME = 10000;
|
||||
|
||||
/** (IMPORTANT) One of the highest level callbacks in the app.
|
||||
* called once DOM is ready and React is attached to the DOM.
|
||||
* => Lets Redux know that the app is ready to bootstrap.
|
||||
* => Checks for token updates since last log in. */
|
||||
export function ready(): Thunk {
|
||||
return (dispatch, getState) => {
|
||||
const state = Session.getAll() || getState().auth;
|
||||
if (state) {
|
||||
didLogin(state, dispatch);
|
||||
const auth = Session.fetchStoredToken() || getState().auth;
|
||||
if (auth) {
|
||||
withTimeout(MAX_TOKEN_WAIT_TIME, maybeRefreshToken(auth))
|
||||
.then(storeToken(auth, dispatch), Session.clear);
|
||||
} else {
|
||||
Session.clear();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -379,7 +379,7 @@ export enum Actions {
|
|||
REFRESH_RESOURCE_NO = "REFRESH_RESOURCE_NO",
|
||||
|
||||
// Auth
|
||||
LOGIN_OK = "LOGIN_OK",
|
||||
REPLACE_TOKEN = "LOGIN_OK",
|
||||
|
||||
// Config
|
||||
CHANGE_API_PORT = "CHANGE_API_PORT",
|
||||
|
|
|
@ -2,7 +2,6 @@ import { Farmbot } from "farmbot";
|
|||
import { bail } from "./util";
|
||||
import { set } from "lodash";
|
||||
import { AuthState } from "./auth/interfaces";
|
||||
import { maybeRefreshToken } from "./refresh_token";
|
||||
|
||||
let device: Farmbot;
|
||||
|
||||
|
@ -10,15 +9,11 @@ const secure = location.protocol === "https:"; // :(
|
|||
|
||||
export const getDevice = (): Farmbot => (device || bail("NO DEVICE SET"));
|
||||
|
||||
export function fetchNewDevice(oldToken: AuthState): Promise<Farmbot> {
|
||||
return maybeRefreshToken(oldToken)
|
||||
.then(({ token }) => {
|
||||
device = new Farmbot({ token: token.encoded, secure });
|
||||
set(window, "current_bot", device);
|
||||
return device
|
||||
.connect()
|
||||
.then(() => {
|
||||
return device;
|
||||
}, () => bail("NO CONNECT"));
|
||||
});
|
||||
export function fetchNewDevice(auth: AuthState): Promise<Farmbot> {
|
||||
device = new Farmbot({ token: auth.token.encoded, secure });
|
||||
set(window, "current_bot", device);
|
||||
|
||||
return device
|
||||
.connect()
|
||||
.then(() => device, () => bail("NO CONNECT"));
|
||||
}
|
||||
|
|
|
@ -1,21 +1,15 @@
|
|||
/// <reference path="../typings/index.d.ts" />
|
||||
/**
|
||||
* THIS IS THE ENTRY POINT FOR THE MAIN PORTION OF THE WEB APP.
|
||||
*/
|
||||
import { RootComponent } from "./routes";
|
||||
import { store } from "./redux/store";
|
||||
import { ready } from "./config/actions";
|
||||
*
|
||||
* Try to keep this file light. */
|
||||
import { detectLanguage } from "./i18n";
|
||||
import * as i18next from "i18next";
|
||||
import { stopIE, attachToRoot, shortRevision } from "./util";
|
||||
import { stopIE, shortRevision } from "./util";
|
||||
import { init } from "i18next";
|
||||
import { attachAppToDom } from "./routes";
|
||||
|
||||
stopIE();
|
||||
|
||||
console.log(shortRevision());
|
||||
|
||||
detectLanguage().then((config) => {
|
||||
i18next.init(config, (err, t) => {
|
||||
attachToRoot(RootComponent, { store });
|
||||
store.dispatch(ready());
|
||||
});
|
||||
});
|
||||
detectLanguage().then(config => init(config, attachAppToDom));
|
||||
|
|
|
@ -33,7 +33,7 @@ export class FrontPage extends React.Component<{}, Partial<FrontPageState>> {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (Session.getAll()) { window.location.href = "/app/controls"; }
|
||||
if (Session.fetchStoredToken()) { window.location.href = "/app/controls"; }
|
||||
logInit();
|
||||
API.setBaseUrl(API.fetchBrowserLocation());
|
||||
this.setState({
|
||||
|
@ -66,7 +66,7 @@ export class FrontPage extends React.Component<{}, Partial<FrontPageState>> {
|
|||
API.setBaseUrl(url);
|
||||
axios.post(API.current.tokensPath, payload)
|
||||
.then((resp: HttpData<AuthState>) => {
|
||||
Session.replace(resp.data);
|
||||
Session.replaceToken(resp.data);
|
||||
window.location.href = "/app/controls";
|
||||
}).catch((error: Error) => {
|
||||
switch (_.get(error, "response.status")) {
|
||||
|
@ -308,5 +308,5 @@ export class FrontPage extends React.Component<{}, Partial<FrontPageState>> {
|
|||
</div>;
|
||||
}
|
||||
|
||||
render() { return Session.getAll() ? <div /> : this.defaultContent(); }
|
||||
render() { return Session.fetchStoredToken() ? <div /> : this.defaultContent(); }
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import axios from "axios";
|
||||
import { Session } from "./session";
|
||||
import { BooleanSetting } from "./session_keys";
|
||||
import { InitOptions } from "i18next";
|
||||
|
||||
function generateUrl(langCode: string) {
|
||||
const lang = langCode.slice(0, 2);
|
||||
|
@ -15,17 +16,18 @@ function getUserLang(langCode = "en_us") {
|
|||
.catch((error) => { return "en"; });
|
||||
}
|
||||
|
||||
export function detectLanguage() {
|
||||
return getUserLang(navigator.language).then(function (lang) {
|
||||
// NOTE: Some international users prefer using the app in English.
|
||||
// This preference is stored in `DISABLE_I18N`.
|
||||
const choice = Session.getBool(BooleanSetting.disableI18n) ? "en" : lang;
|
||||
const langi = require("../public/app-resources/languages/" + choice + ".js");
|
||||
return {
|
||||
nsSeparator: "",
|
||||
keySeparator: "",
|
||||
lng: lang,
|
||||
resources: { [lang]: { translation: langi } }
|
||||
};
|
||||
});
|
||||
function generateI18nConfig(lang: string): InitOptions {
|
||||
// NOTE: Some users prefer English over i18nized version.
|
||||
const choice = Session.getBool(BooleanSetting.disableI18n) ? "en" : lang;
|
||||
const langi = require("../public/app-resources/languages/" + choice + ".js");
|
||||
|
||||
return {
|
||||
nsSeparator: "",
|
||||
keySeparator: "",
|
||||
lng: lang,
|
||||
resources: { [lang]: { translation: langi } }
|
||||
};
|
||||
}
|
||||
|
||||
export const detectLanguage =
|
||||
() => getUserLang(navigator.language).then(generateI18nConfig);
|
||||
|
|
|
@ -6,6 +6,7 @@ import { prettyPrintApiErrors } from "../util";
|
|||
import { API } from "../api";
|
||||
import { State, Props } from "./interfaces";
|
||||
import { Widget, WidgetHeader, WidgetBody, Row, Col } from "../ui/index";
|
||||
import { Session } from "../session";
|
||||
|
||||
export class PasswordReset extends React.Component<Props, State> {
|
||||
constructor() {
|
||||
|
@ -41,11 +42,11 @@ export class PasswordReset extends React.Component<Props, State> {
|
|||
id: token,
|
||||
password,
|
||||
password_confirmation: passwordConfirmation,
|
||||
}).then(() => {
|
||||
window.location.href = "/";
|
||||
}).catch((error: string) => {
|
||||
log(prettyPrintApiErrors(error as {}));
|
||||
});
|
||||
})
|
||||
.then(Session.clear)
|
||||
.catch((error: string) => {
|
||||
log(prettyPrintApiErrors(error as {}));
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -2,9 +2,21 @@ import axios from "axios";
|
|||
import { API } from "./api/index";
|
||||
import { AuthState } from "./auth/interfaces";
|
||||
import { HttpData } from "./util";
|
||||
import { setToken } from "./auth/actions";
|
||||
import { Session } from "./session";
|
||||
|
||||
export let maybeRefreshToken = (old: AuthState): Promise<AuthState> => {
|
||||
return axios
|
||||
.get(API.current.tokensPath)
|
||||
.then((x: HttpData<AuthState>) => x.data, () => (old));
|
||||
type Resp = HttpData<AuthState>;
|
||||
|
||||
/** What to do when the Token refresh request completes. */
|
||||
const ok = (x: Resp) => {
|
||||
setToken(x.data); // Start using new token in HTTP requests.
|
||||
return x.data;
|
||||
};
|
||||
|
||||
/** Grab a new token from the API (won't extend token's exp. date).
|
||||
* Redirect to home page on failure. */
|
||||
export let maybeRefreshToken = (old: AuthState): Promise<AuthState> => {
|
||||
API.setBaseUrl(old.token.unencoded.iss);
|
||||
setToken(old); // Precaution: The Axios interceptors might not be set yet.
|
||||
return axios.get(API.current.tokensPath).then(ok, Session.clear);
|
||||
};
|
||||
|
|
|
@ -9,7 +9,8 @@ import { history } from "./history";
|
|||
import { Store } from "./redux/interfaces";
|
||||
import { ready } from "./config/actions";
|
||||
import { Session } from "./session";
|
||||
import { isMobile } from "./util";
|
||||
import { isMobile, attachToRoot } from "./util";
|
||||
import { Callback } from "i18next";
|
||||
|
||||
interface RootComponentProps {
|
||||
store: Store;
|
||||
|
@ -66,11 +67,17 @@ const controlsRoute = {
|
|||
).catch(errorLoading(cb));
|
||||
}
|
||||
};
|
||||
|
||||
export const attachAppToDom: Callback = (err, t) => {
|
||||
attachToRoot(RootComponent, { store: _store });
|
||||
_store.dispatch(ready());
|
||||
};
|
||||
|
||||
export class RootComponent extends React.Component<RootComponentProps, {}> {
|
||||
|
||||
requireAuth(_discard: RouterState, replace: RedirectFunction) {
|
||||
const { store } = this.props;
|
||||
if (Session.getAll()) { // has a previous session in cache
|
||||
if (Session.fetchStoredToken()) { // has a previous session in cache
|
||||
if (store.getState().auth) { // Has session, logged in.
|
||||
return;
|
||||
} else { // Has session but not logged in (returning visitor).
|
||||
|
@ -282,10 +289,10 @@ export class RootComponent extends React.Component<RootComponentProps, {}> {
|
|||
render() {
|
||||
// ==== TEMPORARY HACK. TODO: Add a before hook, if such a thing exists in
|
||||
// React Router. Or switch routing libs.
|
||||
const notLoggedIn = !Session.getAll();
|
||||
const notLoggedIn = !Session.fetchStoredToken();
|
||||
const restrictedArea = window.location.pathname.includes("/app");
|
||||
if (notLoggedIn && restrictedArea) {
|
||||
window.location.href = "/";
|
||||
Session.clear();
|
||||
}
|
||||
// ==== END HACK ====
|
||||
return <Provider store={_store}>
|
||||
|
|
|
@ -17,12 +17,12 @@ export namespace Session {
|
|||
const KEY = "session";
|
||||
|
||||
/** Replace the contents of session storage. */
|
||||
export function replace(nextState: AuthState) {
|
||||
export function replaceToken(nextState: AuthState) {
|
||||
localStorage[KEY] = JSON.stringify(nextState);
|
||||
}
|
||||
|
||||
/** Fetch the previous session. */
|
||||
export function getAll(): AuthState | undefined {
|
||||
export function fetchStoredToken(): AuthState | undefined {
|
||||
try {
|
||||
const v: AuthState = JSON.parse(localStorage[KEY]);
|
||||
if (box(v).kind === "object") {
|
||||
|
@ -36,10 +36,11 @@ export namespace Session {
|
|||
}
|
||||
|
||||
/** Clear localstorage and sessionstorage. */
|
||||
export function clear() {
|
||||
export function clear(): never {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
window.location.href = window.location.origin;
|
||||
// window.location.href = window.location.origin;
|
||||
throw new Error("session cleared");
|
||||
}
|
||||
|
||||
/** Fetch a *boolean* value from localstorage. Returns `undefined` when
|
||||
|
@ -74,9 +75,7 @@ export namespace Session {
|
|||
|
||||
const isBooleanSetting =
|
||||
// tslint:disable-next-line:no-any
|
||||
(x: any): x is BooleanSetting => {
|
||||
return !!BooleanSetting[x];
|
||||
};
|
||||
(x: any): x is BooleanSetting => !!BooleanSetting[x];
|
||||
|
||||
export function safeBooleanSettting(name: string): BooleanSetting {
|
||||
if (isBooleanSetting(name)) {
|
||||
|
|
|
@ -52,7 +52,7 @@ export class Wow extends React.Component<Props, Partial<State>> {
|
|||
axios
|
||||
.post(API.current.tokensPath, payload)
|
||||
.then((resp: HttpData<AuthState>) => {
|
||||
Session.replace(resp.data);
|
||||
Session.replaceToken(resp.data);
|
||||
window.location.href = "/app/controls";
|
||||
})
|
||||
.catch(error => {
|
||||
|
|
|
@ -235,14 +235,14 @@ export function smoothScrollToBottom() {
|
|||
let timer = 0;
|
||||
if (stopY > startY) {
|
||||
for (let i = startY; i < stopY; i += step) {
|
||||
setTimeout("window.scrollTo(0, " + leapY + ")", timer * speed);
|
||||
setTimeout(() => window.scrollTo(0, leapY), timer * speed);
|
||||
leapY += step;
|
||||
if (leapY > stopY) { leapY = stopY; }
|
||||
timer++;
|
||||
} return;
|
||||
}
|
||||
for (let i = startY; i > stopY; i -= step) {
|
||||
setTimeout("window.scrollTo(0, " + leapY + ")", timer * speed);
|
||||
setTimeout(() => window.scrollTo(0, leapY), timer * speed);
|
||||
leapY -= step; if (leapY < stopY) { leapY = stopY; }
|
||||
timer++;
|
||||
}
|
||||
|
@ -447,3 +447,19 @@ export function bitArray(...values: boolean[]) {
|
|||
export function bail(message: string): never {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
// Thanks,
|
||||
// https://italonascimento.github.io
|
||||
// /applying-a-timeout-to-your-promises/#implementing-the-timeout
|
||||
export function withTimeout<T>(ms: number, promise: Promise<T>) {
|
||||
// Create a promise that rejects in <ms> milliseconds
|
||||
const timeout = new Promise((resolve, reject) => {
|
||||
const id = setTimeout(() => {
|
||||
clearTimeout(id);
|
||||
reject(`Timed out in ${ms} ms.`);
|
||||
}, ms);
|
||||
});
|
||||
|
||||
// Returns a race between our timeout and the passed in promise
|
||||
return Promise.race([promise, timeout]);
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ export async function verify() {
|
|||
const url = API.fetchBrowserLocation();
|
||||
try {
|
||||
let r: HttpData<AuthState> = await axios.put(url + "/api/users/verify/" + token);
|
||||
Session.replace(r.data);
|
||||
Session.replaceToken(r.data);
|
||||
window.location.href = window.location.origin + "/app/controls";
|
||||
} catch (e) {
|
||||
document.write(`
|
||||
|
|
92
yarn.lock
92
yarn.lock
|
@ -117,13 +117,13 @@
|
|||
dependencies:
|
||||
axios "^0.16.1"
|
||||
|
||||
"@types/mqtt@^2.5.0":
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/mqtt/-/mqtt-2.5.0.tgz#bc54c2d53f509282168da4a9af865de95bee5101"
|
||||
"@types/mqtt@^0.0.34":
|
||||
version "0.0.34"
|
||||
resolved "https://registry.yarnpkg.com/@types/mqtt/-/mqtt-0.0.34.tgz#7865790000cc8a312242ead9a0e209ba2aa686d4"
|
||||
dependencies:
|
||||
mqtt "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/node@^8.0.34":
|
||||
"@types/node@*", "@types/node@^8.0.34":
|
||||
version "8.0.34"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.34.tgz#55f801fa2ddb2a40dd6dfc15ecfe1dde9c129fe9"
|
||||
|
||||
|
@ -399,10 +399,6 @@ async-foreach@^0.1.3:
|
|||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542"
|
||||
|
||||
async-limiter@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
|
||||
|
||||
async@^1.4.0, async@^1.5.2:
|
||||
version "1.5.2"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
|
||||
|
@ -645,12 +641,6 @@ bl@^0.9.1:
|
|||
dependencies:
|
||||
readable-stream "~1.0.26"
|
||||
|
||||
bl@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.1.tgz#cac328f7bee45730d404b692203fcb590e172d5e"
|
||||
dependencies:
|
||||
readable-stream "^2.0.5"
|
||||
|
||||
blamer@^0.1.9:
|
||||
version "0.1.13"
|
||||
resolved "https://registry.yarnpkg.com/blamer/-/blamer-0.1.13.tgz#61f215f2361bd054e6258c0c5e0086f04074e670"
|
||||
|
@ -1114,7 +1104,7 @@ concat-map@0.0.1:
|
|||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
|
||||
concat-stream@^1.4.7, concat-stream@^1.6.0:
|
||||
concat-stream@^1.4.7:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7"
|
||||
dependencies:
|
||||
|
@ -2435,7 +2425,7 @@ hawk@3.1.3, hawk@~3.1.3:
|
|||
hoek "2.x.x"
|
||||
sntp "1.x.x"
|
||||
|
||||
help-me@^1.0.0, help-me@^1.0.1:
|
||||
help-me@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/help-me/-/help-me-1.1.0.tgz#8f2d508d0600b4a456da2f086556e7e5c056a3c6"
|
||||
dependencies:
|
||||
|
@ -3827,33 +3817,6 @@ mqtt-packet@^3.0.0, mqtt-packet@^3.4.7:
|
|||
bl "^0.9.1"
|
||||
inherits "^2.0.1"
|
||||
|
||||
mqtt-packet@^5.4.0:
|
||||
version "5.4.0"
|
||||
resolved "https://registry.yarnpkg.com/mqtt-packet/-/mqtt-packet-5.4.0.tgz#387104c06aa68fbb9f8159d0c722dd5c3e45df22"
|
||||
dependencies:
|
||||
bl "^1.2.1"
|
||||
inherits "^2.0.3"
|
||||
process-nextick-args "^1.0.7"
|
||||
safe-buffer "^5.1.0"
|
||||
|
||||
mqtt@*:
|
||||
version "2.13.0"
|
||||
resolved "https://registry.yarnpkg.com/mqtt/-/mqtt-2.13.0.tgz#cbf3fae8f0c48328472b0d5f8e049cc800957d7e"
|
||||
dependencies:
|
||||
commist "^1.0.0"
|
||||
concat-stream "^1.6.0"
|
||||
end-of-stream "^1.1.0"
|
||||
help-me "^1.0.1"
|
||||
inherits "^2.0.3"
|
||||
minimist "^1.2.0"
|
||||
mqtt-packet "^5.4.0"
|
||||
pump "^1.0.2"
|
||||
readable-stream "^2.3.3"
|
||||
reinterval "^1.1.0"
|
||||
split2 "^2.1.1"
|
||||
websocket-stream "^5.0.1"
|
||||
xtend "^4.0.1"
|
||||
|
||||
mqtt@^1.7.4:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/mqtt/-/mqtt-1.14.1.tgz#7e376987153d01793e946d26d46122ebf0c03554"
|
||||
|
@ -4711,7 +4674,7 @@ private@^0.1.6:
|
|||
version "0.1.7"
|
||||
resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1"
|
||||
|
||||
process-nextick-args@^1.0.7, process-nextick-args@~1.0.6:
|
||||
process-nextick-args@~1.0.6:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3"
|
||||
|
||||
|
@ -4757,7 +4720,7 @@ public-encrypt@^4.0.0:
|
|||
parse-asn1 "^5.0.0"
|
||||
randombytes "^2.0.1"
|
||||
|
||||
pump@^1.0.0, pump@^1.0.1, pump@^1.0.2:
|
||||
pump@^1.0.0, pump@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.2.tgz#3b3ee6512f94f0e575538c17995f9f16990a5d51"
|
||||
dependencies:
|
||||
|
@ -4970,7 +4933,7 @@ read-pkg@^2.0.0:
|
|||
normalize-package-data "^2.3.2"
|
||||
path-type "^2.0.0"
|
||||
|
||||
"readable-stream@> 1.0.0 < 3.0.0", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.0, readable-stream@^2.2.2, readable-stream@^2.2.6, readable-stream@^2.2.9, readable-stream@^2.3.3:
|
||||
"readable-stream@> 1.0.0 < 3.0.0", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.6, readable-stream@^2.2.9:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c"
|
||||
dependencies:
|
||||
|
@ -5093,7 +5056,7 @@ regjsparser@^0.1.4:
|
|||
dependencies:
|
||||
jsesc "~0.5.0"
|
||||
|
||||
reinterval@^1.0.1, reinterval@^1.1.0:
|
||||
reinterval@^1.0.1:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/reinterval/-/reinterval-1.1.0.tgz#3361ecfa3ca6c18283380dd0bb9546f390f5ece7"
|
||||
|
||||
|
@ -5209,7 +5172,7 @@ safe-buffer@^5.0.1:
|
|||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7"
|
||||
|
||||
safe-buffer@^5.1.0, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||
safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
|
||||
|
||||
|
@ -5484,12 +5447,6 @@ split2@^2.0.1:
|
|||
dependencies:
|
||||
through2 "^2.0.2"
|
||||
|
||||
split2@^2.1.1:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/split2/-/split2-2.2.0.tgz#186b2575bcf83e85b7d18465756238ee4ee42493"
|
||||
dependencies:
|
||||
through2 "^2.0.2"
|
||||
|
||||
sprintf-js@~1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
||||
|
@ -5948,10 +5905,6 @@ ultron@1.0.x:
|
|||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa"
|
||||
|
||||
ultron@~1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.0.tgz#b07a2e6a541a815fc6a34ccd4533baec307ca864"
|
||||
|
||||
unc-path-regex@^0.1.0:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa"
|
||||
|
@ -6232,17 +6185,6 @@ websocket-stream@^3.0.1:
|
|||
ws "^1.0.1"
|
||||
xtend "^4.0.0"
|
||||
|
||||
websocket-stream@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/websocket-stream/-/websocket-stream-5.0.1.tgz#51cb992988c2eeb4525ccd90eafbac52a5ac6700"
|
||||
dependencies:
|
||||
duplexify "^3.2.0"
|
||||
inherits "^2.0.1"
|
||||
readable-stream "^2.2.0"
|
||||
safe-buffer "^5.0.1"
|
||||
ws "^3.0.0"
|
||||
xtend "^4.0.0"
|
||||
|
||||
weinre@^2.0.0-pre-I0Z7U9OV:
|
||||
version "2.0.0-pre-I0Z7U9OV"
|
||||
resolved "https://registry.yarnpkg.com/weinre/-/weinre-2.0.0-pre-I0Z7U9OV.tgz#fef8aa223921f7b40bbbbd4c3ed4302f6fd0a813"
|
||||
|
@ -6352,14 +6294,6 @@ ws@^1.0.1:
|
|||
options ">=0.0.5"
|
||||
ultron "1.0.x"
|
||||
|
||||
ws@^3.0.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-3.2.0.tgz#d5d3d6b11aff71e73f808f40cc69d52bb6d4a185"
|
||||
dependencies:
|
||||
async-limiter "~1.0.0"
|
||||
safe-buffer "~5.1.0"
|
||||
ultron "~1.1.0"
|
||||
|
||||
xml-name-validator@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635"
|
||||
|
@ -6377,7 +6311,7 @@ xmlbuilder@^4.1.0:
|
|||
dependencies:
|
||||
lodash "^4.0.0"
|
||||
|
||||
"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1:
|
||||
"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@~4.0.0, xtend@~4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
|
||||
|
||||
|
|
Loading…
Reference in a new issue