Works
parent
1f8f686cf5
commit
deb0e13593
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -24,7 +24,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) => {
|
||||
|
|
|
@ -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,13 +1,32 @@
|
|||
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 { goHome, 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 were 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), goHome);
|
||||
} else {
|
||||
goHome();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
|
|
@ -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(); }
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as React from "react";
|
|||
import axios from "axios";
|
||||
import { t } from "i18next";
|
||||
import { error as log, init as logInit } from "farmbot-toastr";
|
||||
import { prettyPrintApiErrors } from "../util";
|
||||
import { prettyPrintApiErrors, goHome } from "../util";
|
||||
import { API } from "../api";
|
||||
import { State, Props } from "./interfaces";
|
||||
import { Widget, WidgetHeader, WidgetBody, Row, Col } from "../ui/index";
|
||||
|
@ -42,7 +42,7 @@ export class PasswordReset extends React.Component<Props, State> {
|
|||
password,
|
||||
password_confirmation: passwordConfirmation,
|
||||
}).then(() => {
|
||||
window.location.href = "/";
|
||||
goHome();
|
||||
}).catch((error: string) => {
|
||||
log(prettyPrintApiErrors(error as {}));
|
||||
});
|
||||
|
|
|
@ -2,9 +2,19 @@ import axios from "axios";
|
|||
import { API } from "./api/index";
|
||||
import { AuthState } from "./auth/interfaces";
|
||||
import { HttpData } from "./util";
|
||||
import { setToken } from "./auth/actions";
|
||||
|
||||
/** 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> => {
|
||||
return axios
|
||||
.get(API.current.tokensPath)
|
||||
.then((x: HttpData<AuthState>) => x.data, () => (old));
|
||||
API.setBaseUrl(old.token.unencoded.iss);
|
||||
setToken(old); // The Axios interceptors might not be set yet.
|
||||
type Resp = HttpData<AuthState>;
|
||||
|
||||
return axios.get(API.current.tokensPath).then((x: Resp) => {
|
||||
setToken(old);
|
||||
return x.data;
|
||||
}, (x) => {
|
||||
return Promise.reject("X");
|
||||
});
|
||||
};
|
||||
|
|
|
@ -9,7 +9,7 @@ import { history } from "./history";
|
|||
import { Store } from "./redux/interfaces";
|
||||
import { ready } from "./config/actions";
|
||||
import { Session } from "./session";
|
||||
import { isMobile, attachToRoot } from "./util";
|
||||
import { isMobile, attachToRoot, goHome } from "./util";
|
||||
import { Callback } from "i18next";
|
||||
|
||||
interface RootComponentProps {
|
||||
|
@ -77,7 +77,7 @@ 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).
|
||||
|
@ -289,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 = "/";
|
||||
goHome();
|
||||
}
|
||||
// ==== 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") {
|
||||
|
@ -74,9 +74,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 => {
|
||||
|
|
|
@ -9,6 +9,7 @@ import { box } from "boxed_value";
|
|||
import { TaggedResource } from "./resources/tagged_resources";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { history } from "./history";
|
||||
import { Session } from "./session";
|
||||
|
||||
// http://stackoverflow.com/a/901144/1064917
|
||||
// Grab a query string param by name, because react-router-redux doesn't
|
||||
|
@ -447,3 +448,25 @@ export function bitArray(...values: boolean[]) {
|
|||
export function bail(message: string): never {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
export const goHome = (): never => {
|
||||
Session.clear();
|
||||
window.location.href = "/";
|
||||
return bail("Going to home page.");
|
||||
};
|
||||
|
||||
// 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(`
|
||||
|
|
Loading…
Reference in New Issue