diff --git a/smart-hut/src/remote.js b/smart-hut/src/remote.js index 1115f69..e170bd5 100644 --- a/smart-hut/src/remote.js +++ b/smart-hut/src/remote.js @@ -3,6 +3,7 @@ import actions from "./storeActions"; import axios from "axios"; class Endpoint { + socket = null; axiosInstance = axios.create({ baseURL: this.URL, validateStatus: (status) => status >= 200 && status < 300, @@ -55,6 +56,31 @@ class Endpoint { } }); } + /** + * Performs login + * @param {String} usernameOrEmail + * @param {String} password + * @returns {Promise} promise that resolves to void and rejects + * with user-fiendly errors as a String array + */ + static login(dispatch, usernameOrEmail, password) { + return Endpoint.axiosInstance + .post(`${Endpoint.URL}/auth/login`, { + usernameOrEmail, + password, + }) + .then((res) => { + localStorage.setItem("token", res.token); + localStorage.setItem("exp", new Date().getTime() + 5 * 60 * 60 * 1000); + this.socket = new ServiceSocket(res.data.token); + return res.data.token; + }); + } + + static logout() { + this.socket.close(); + this.socket = null; + } /** * Performs an authenticated GET request @@ -126,19 +152,8 @@ export class RemoteService { */ static login(usernameOrEmail, password) { return (dispatch) => { - return Endpoint.axiosInstance - .post(`${Endpoint.URL}/auth/login`, { - usernameOrEmail, - password, - }) - .then((res) => { - localStorage.setItem("token", res.token); - localStorage.setItem( - "exp", - new Date().getTime() + 5 * 60 * 60 * 1000 - ); - dispatch(actions.loginSuccess(res.token)); - }) + return Endpoint.login(dispatch, usernameOrEmail, password) + .then((token) => dispatch(actions.loginSuccess(token))) .catch((err) => { console.warn("login error", err); return [ @@ -156,7 +171,7 @@ export class RemoteService { * @param {String} password */ static logout() { - return (dispatch) => void dispatch(actions.logout()); + return (dispatch) => Endpoint.logout.then(void dispatch(actions.logout())); } /** @@ -386,6 +401,71 @@ export class RemoteService { } } +/** Class to handle connection with the sensor socket */ +class ServiceSocket { + token; + authenticated = false; + retries = 0; + connection; + + static get URL() { + const httpURL = new URL(Endpoint.URL); + const isSecure = httpURL.protocol === "https:"; + const protocol = isSecure ? "wss:" : "ws:"; + const port = httpURL.port || (isSecure ? 443 : 80); + return `${protocol}//${httpURL.host}:${port}/sensor-socket`; + } + + /** + * Create a new sensor socket connection + * @param {string} token - The JWT token (needed for authentication) + */ + constructor(token) { + this.token = token; + this.authenticated = false; + } + + _init() { + this.connection = new WebSocket(ServiceSocket.URL); + + this.connection.onopen = (_) => { + this.connection.send(JSON.stringify({ token: this.token })); + }; + + this.connection.onmessage = (evt) => { + let data = JSON.parse(evt.data); + + if (!this.authenticated) { + if (data.authenticated) { + this.authenticated = true; + this.retries = 0; + } else { + console.error("socket authentication failed", data); + } + } else { + this.invokeCallbacks(data); + } + }; + + this.connection.onerror = (evt) => { + console.warn("socket error", evt); + if (this.retries >= 5) { + console.error("too many socket connection retries"); + return; + } + this.retries++; + this._init(); + }; + } + + /** + * Closes the underlying websocket connection + */ + close() { + this.connection.close(); + } +} + export class Forms { /** * Attempts to create a new user from the given data. diff --git a/smart-hut/src/store.js b/smart-hut/src/store.js index 47c64cc..06290c0 100644 --- a/smart-hut/src/store.js +++ b/smart-hut/src/store.js @@ -1,22 +1,7 @@ -import { createStore } from "redux"; -import { RemoteService } from "./remote"; -import { createDispatchHook } from "react-redux"; +import { createStore, applyMiddleware } from "redux"; +import thunk from "redux-thunk"; import actions from "./storeActions"; -const initialToken = localStorage.getItem("token"); - -const initialState = { - login: { - loggedIn: false, - token: initialToken ? initialToken : null, - }, - userInfo: null, - /** @type {[integer]Room} */ - rooms: {}, - /** @type {[integer]Device} */ - devices: {}, -}; - function reducer(previousState, action) { let newState = Object.assign({}, previousState); const createOrUpdateRoom = (room) => { @@ -129,9 +114,34 @@ function reducer(previousState, action) { console.warn(`Action type ${action.type} unknown`, action); } + console.log("new state: ", newState); return newState; } -const smartHutStore = createStore(reducer, initialState); +function createSmartHutStore() { + const token = localStorage.getItem("token"); + const exp = localStorage.getItem("exp"); + const initialState = { + login: { + token: token, + }, + userInfo: null, + /** @type {[integer]Room} */ + rooms: {}, + /** @type {[integer]Device} */ + devices: {}, + }; + + initialState.login.loggedIn = token && exp > new Date().getTime(); + if (!initialState.login.loggedIn) { + localStorage.removeItem("token"); + localStorage.removeItem("exp"); + initialState.login.token = null; + } + + return createStore(reducer, initialState, applyMiddleware(thunk)); +} + +const smartHutStore = createSmartHutStore(); export default smartHutStore;