Login and logout work on the new system. Nasty error on logout

This commit is contained in:
Claudio Maggioni 2020-04-09 13:59:34 +02:00
parent c36d298f10
commit 462cad0e53
7 changed files with 289 additions and 282 deletions

View File

@ -10,6 +10,7 @@
"@testing-library/user-event": "^7.1.2", "@testing-library/user-event": "^7.1.2",
"axios": "^0.19.2", "axios": "^0.19.2",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"immutability-helper": "^3.0.2",
"material-ui-image": "^3.2.3", "material-ui-image": "^3.2.3",
"react": "^16.12.0", "react": "^16.12.0",
"react-axios": "^2.0.3", "react-axios": "^2.0.3",

View File

@ -12,44 +12,17 @@ import ConfirmRegistration from "./views/ConfirmRegistration";
import ConfirmResetPassword from "./views/ConfirmResetPassword"; import ConfirmResetPassword from "./views/ConfirmResetPassword";
import Instruction from "./views/Instruction"; import Instruction from "./views/Instruction";
import queryString from "query-string"; import queryString from "query-string";
import { RemoteService } from "./remote";
import { call } from "./client_server"; import { connect } from "react-redux";
/*let userJsonString = JSON.parse(localStorage.getItem("token"));
let date = new Date().getTime().toString();
let delta = date - userJsonString.timestamp;
if (delta < 5*60*60*1000) {
loggedIn = true;
}*/
class App extends Component { class App extends Component {
constructor(props) { constructor(props, context) {
super(props); super(props, context);
let loggedIn = false;
let token = undefined;
try {
let userJsonString = localStorage.getItem("token");
let exp = localStorage.getItem("exp");
let date = new Date().getTime();
if (userJsonString && exp && date < exp) {
loggedIn = true;
token = userJsonString;
} else {
localStorage.removeItem("token");
localStorage.removeItem("exp");
}
} catch (exception) {}
this.state = { this.state = {
loggedIn: loggedIn, query: "",
token: token,
info: "", info: "",
}; };
this.auth = this.auth.bind(this);
this.logout = this.logout.bind(this);
} }
componentDidMount() { componentDidMount() {
@ -58,70 +31,21 @@ class App extends Component {
this.setState({ this.setState({
query: values, query: values,
}); });
} else {
this.setState({
query: "ciao",
});
} }
} }
auth(data) {
return call
.login(data.params)
.then((res) => {
if (res.data && res.status === 200) {
let expire = new Date().getTime() + 60 * 60 * 5 * 1000;
localStorage.setItem("token", res.data.jwttoken);
localStorage.setItem("exp", expire);
call.setToken(res.data.jwttoken);
this.setState({
user: data.params.user,
token: res.data.jwttoken,
loggedIn: true,
});
this.getInfo();
return res;
//this.props.history.push("/dashboard");
} else {
this.setState({
error: res.data.message,
});
return res.status;
}
})
.catch((err) => {
return err;
});
}
logout() {
this.setState({
loggedIn: false,
});
localStorage.removeItem("token");
localStorage.removeItem("exp");
}
render() { render() {
console.log("rendering root", this.props.loggedIn, this.state.query);
return ( return (
<BrowserRouter> <BrowserRouter>
<Switch> <Switch>
<Route path="/" exact component={Home} /> <Route path="/" exact component={Home} />
<Route path="/login"> <Route path="/login">
{this.state.loggedIn && this.state.token ? ( {this.props.loggedIn ? <Redirect to="/dashboard" /> : <Login />}
<Redirect tkn={this.state.token} to="/dashboard" />
) : (
<Login auth={this.auth} />
)}
</Route> </Route>
<Route path="/signup" exact component={Signup} /> <Route path="/signup" exact component={Signup} />
<Route path="/dashboard"> <Route path="/dashboard">
{this.state.loggedIn ? ( {this.props.loggedIn ? <Dashboard /> : <Redirect to="/login" />}
<Dashboard tkn={this.state.token} logout={this.logout} />
) : (
<Redirect to="/login" />
)}
</Route> </Route>
<Route path="/forgot-password"> <Route path="/forgot-password">
<ForgotPass /> <ForgotPass />
@ -149,4 +73,9 @@ class App extends Component {
} }
} }
export default App; const mapStateToProps = (state, _) => {
console.log("malusae react", state);
return { loggedIn: !!(state.login && state.login.loggedIn) };
};
const AppContainer = connect(mapStateToProps, RemoteService)(App);
export default AppContainer;

View File

@ -1,7 +1,9 @@
import React from "react"; import React from "react";
import { Grid, Divider, Button, Label, Responsive } from "semantic-ui-react"; import { Grid, Divider, Button, Label, Responsive } from "semantic-ui-react";
import { Segment, Image } from "semantic-ui-react"; import { Segment, Image } from "semantic-ui-react";
import { call } from "../client_server"; import { RemoteService } from "../remote";
import { withRouter } from "react-router-dom";
import { connect } from "react-redux";
const IconHomeImage = () => ( const IconHomeImage = () => (
<Image <Image
@ -15,7 +17,7 @@ const IconHomeImage = () => (
const TitleImage = () => <Image src="sm_logo.png" size="medium" centered />; const TitleImage = () => <Image src="sm_logo.png" size="medium" centered />;
export default class MyHeader extends React.Component { export class MyHeader extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
@ -24,15 +26,13 @@ export default class MyHeader extends React.Component {
this.getInfo(); this.getInfo();
} }
getInfo() { getInfo() {
call.getUserInfo(this.state.token).then((res) => { this.props
if (res.status === 200) { .fetchUserInfo()
this.setState({ .catch((err) => console.error("MyHeader fetch user info error", err));
username: res.data.username,
});
}
});
} }
render() { render() {
return ( return (
<div> <div>
@ -86,3 +86,13 @@ export default class MyHeader extends React.Component {
); );
} }
} }
const mapStateToProps = (state, _) => ({
username:
state.userInfo && state.userInfo.username ? state.userInfo.username : "",
});
const LoginContainer = connect(
mapStateToProps,
RemoteService
)(withRouter(MyHeader));
export default LoginContainer;

View File

@ -2,30 +2,43 @@ import smartHutStore from "./store";
import actions from "./storeActions"; import actions from "./storeActions";
import axios from "axios"; import axios from "axios";
class Endpoint { /**
socket = null; * An object returned by promise rejections in remoteservice
axiosInstance = axios.create({ * @typedef {Error} RemoteError
baseURL: this.URL, * @property {String[]} messages a list of user-friendly error messages to show;
validateStatus: (status) => status >= 200 && status < 300, */
}); class RemoteError extends Error {
messages;
/** constructor(messages) {
* Returns the endpoint URL (SmartHut backend URL) super("remote error");
* @returns {String} endpoint URL this.messages = messages;
*/
static get URL() {
return window.BACKEND_URL !== "__BACKEND_URL__"
? window.BACKEND_URL
: "http://localhost:8080";
} }
}
/**
* Returns the endpoint URL (SmartHut backend URL)
* @returns {String} endpoint URL
*/
function endpointURL() {
return window.BACKEND_URL !== "__BACKEND_URL__"
? window.BACKEND_URL
: "http://localhost:8080";
}
const Endpoint = {
axiosInstance: axios.create({
baseURL: endpointURL(),
validateStatus: (status) => status >= 200 && status < 300,
}),
/** /**
* Returns token for current session, null if logged out * Returns token for current session, null if logged out
* @returns {String|null} the token * @returns {String|null} the token
*/ */
static get token() { get token() {
return smartHutStore.getState().login.token; return smartHutStore.getState().login.token;
} },
/** /**
* Performs an authenticated request * Performs an authenticated request
@ -34,99 +47,106 @@ class Endpoint {
* @param {[String]String} query query ('?') parameters (no params by default) * @param {[String]String} query query ('?') parameters (no params by default)
* @param {any} body the JSON request body * @param {any} body the JSON request body
*/ */
static send(method, route, query = {}, body = null) { send: (method, route, query = {}, body = null) => {
if (!this.token) { if (!Endpoint.token) {
throw new Error("No token while performing authenticated request"); throw new Error("No token while performing authenticated request");
} }
if (method !== "get") return Endpoint.axiosInstance(route, {
return this.axiosInstance(route, { method: method,
method: method, params: query,
params: query, data: ["put", "post"].indexOf(method) !== -1 ? body : null,
data: ["put", "post"].indexOf(method) !== -1 ? body : null, headers: {
headers: { Authorization: `Bearer ${Endpoint.token}`,
Authorization: `Bearer ${this.token}`, },
}, }).then((res) => {
}).then((res) => { if (!res.data) {
if (!res.data) { console.error("Response body is empty");
console.error("Response body is empty"); return null;
return null; } else {
} else { return res;
return res; }
} });
}); },
}
/** /**
* Performs login * Performs login
* @param {String} usernameOrEmail * @param {String} usernameOrEmail
* @param {String} password * @param {String} password
* @returns {Promise<Undefined, String[]>} promise that resolves to void and rejects * @returns {Promise<String, *>} promise that resolves to the token string
* with user-fiendly errors as a String array * and rejects to the axios error.
*/ */
static login(dispatch, usernameOrEmail, password) { login: (usernameOrEmail, password) => {
return Endpoint.axiosInstance return Endpoint.axiosInstance
.post(`${Endpoint.URL}/auth/login`, { .post(`/auth/login`, {
usernameOrEmail, usernameOrEmail,
password, password,
}) })
.then((res) => { .then((res) => {
localStorage.setItem("token", res.token); localStorage.setItem("token", res.data.jwttoken);
localStorage.setItem("exp", new Date().getTime() + 5 * 60 * 60 * 1000); localStorage.setItem("exp", new Date().getTime() + 5 * 60 * 60 * 1000);
this.socket = new ServiceSocket(res.data.token); Endpoint.socket = new ServiceSocket(res.data.jwttoken);
return res.data.token; return res.data.jwttoken;
}); });
} },
static logout() { /**
this.socket.close(); * Returns an immediately resolved promise for the socket logouts
this.socket = null; * @return {Promise<Undefined, _>} An always-resolved promise
} */
logout: () => {
return Promise.resolve(void 0);
},
/** /**
* Performs an authenticated GET request * Performs an authenticated GET request
* @param {String} route the desired route (e.g. "/rooms/1/devices") * @param {String} route the desired route (e.g. "/rooms/1/devices")
* @param {[String]String} query query ('?') parameters (no params by default) * @param {[String]String} query query ('?') parameters (no params by default)
* @returns {Promise<*, *>} The Axios-generated promise
*/ */
static get(route, query) { get(route, query = {}) {
return this.send("get", route, query); return this.send("get", route, query);
} },
/** /**
* Performs an authenticated POST request * Performs an authenticated POST request
* @param {String} route the desired route (e.g. "/rooms/1/devices") * @param {String} route the desired route (e.g. "/rooms/1/devices")
* @param {[String]String} query query ('?') parameters (no params by default) * @param {[String]String} query query ('?') parameters (no params by default)
* @param {any} body the JSON request body * @param {any} body the JSON request body
* @returns {Promise<*, *>} The Axios-generated promise
*/ */
static post(route, query, body) { post(route, query, body) {
return this.send("post", route, query, body); return this.send("post", route, query, body);
} },
/** /**
* Performs an authenticated PUT request * Performs an authenticated PUT request
* @param {String} route the desired route (e.g. "/rooms/1/devices") * @param {String} route the desired route (e.g. "/rooms/1/devices")
* @param {[String]String} query query ('?') parameters (no params by default) * @param {[String]String} query query ('?') parameters (no params by default)
* @param {any} body the JSON request body * @param {any} body the JSON request body
* @returns {Promise<*, *>} The Axios-generated promise
*/ */
static put(route, query, body) { put(route, query = {}, body = {}) {
return this.send("put", route, query, body); return this.send("put", route, query, body);
} },
/** /**
* Performs an authenticated DELETE request * Performs an authenticated DELETE request
* @param {get|post|put|delete} the desired method * @param {get|post|put|delete} the desired method
* @param {String} route the desired route (e.g. "/rooms/1/devices") * @param {String} route the desired route (e.g. "/rooms/1/devices")
* @param {[String]String} query query ('?') parameters (no params by default) * @param {[String]String} query query ('?') parameters (no params by default)
* @returns {Promise<*, *>} The Axios-generated promise
*/ */
static delete(route, query) { delete(route, query = {}) {
return this.send("delete", route, query); return this.send("delete", route, query);
} },
} };
/** /**
* Given an error response, returns an array of user * Given an error response, returns an array of user
* friendly messages to display to the user * friendly messages to display to the user
* @param {*} err the Axios error reponse object * @param {*} err the Axios error reponse object
* @returns {String[]} user friendly error messages * @returns {RemoteError} user friendly error messages
*/ */
function parseValidationErrors(err) { function parseValidationErrors(err) {
if ( if (
@ -135,96 +155,97 @@ function parseValidationErrors(err) {
err.response.data && err.response.data &&
Array.isArray(err.response.data.errors) Array.isArray(err.response.data.errors)
) { ) {
return [...new Set(err.response.data.errors.map((e) => e.defaultMessage))]; throw new RemoteError([
...new Set(err.response.data.errors.map((e) => e.defaultMessage)),
]);
} else { } else {
console.warn("Non validation error", err); console.warn("Non validation error", err);
return ["Network error"]; throw new RemoteError(["Network error"]);
} }
} }
export class RemoteService { export const RemoteService = {
/** /**
* Performs login * Performs login
* @param {String} usernameOrEmail * @param {String} usernameOrEmail
* @param {String} password * @param {String} password
* @returns {Promise<Undefined, String[]>} promise that resolves to void and rejects * @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a String array * with user-fiendly errors as a RemoteError
*/ */
static login(usernameOrEmail, password) { login: (usernameOrEmail, password) => {
return (dispatch) => { return (dispatch) => {
return Endpoint.login(dispatch, usernameOrEmail, password) return Endpoint.login(usernameOrEmail, password)
.then((token) => dispatch(actions.loginSuccess(token))) .then((token) => dispatch(actions.loginSuccess(token)))
.catch((err) => { .catch((err) => {
console.warn("login error", err); console.warn("login error", err);
return [ throw new RemoteError([
err.response.status === 401 err.response && err.response.status === 401
? "Wrong credentials" ? "Wrong credentials"
: "An error occurred while logging in", : "An error occurred while logging in",
]; ]);
}); });
}; };
} },
/** /**
* Performs logout * Performs logout
* @param {String} usernameOrEmail
* @param {String} password
*/ */
static logout() { logout: () => {
return (dispatch) => Endpoint.logout.then(void dispatch(actions.logout())); return (dispatch) =>
} Endpoint.logout().then(void dispatch(actions.logout()));
},
/** /**
* Fetches user information via REST calls, if it is logged in * Fetches user information via REST calls, if it is logged in
* @returns {Promise<Undefined, String[]>} promise that resolves to void and rejects * @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a String array * with user-fiendly errors as a RemoteError
*/ */
static fetchUserInfo() { fetchUserInfo: () => {
return (dispatch) => { return (dispatch) => {
return Endpoint.get("/auth/profile") return Endpoint.get("/auth/profile")
.then((res) => void dispatch(actions.userInfoUpdate(res.data))) .then((res) => void dispatch(actions.userInfoUpdate(res.data)))
.catch((err) => { .catch((err) => {
console.warn("Fetch user info error", err); console.warn("Fetch user info error", err);
return ["Network error"]; throw new RemoteError(["Network error"]);
}); });
}; };
} },
/** /**
* Fetches all rooms that belong to this user. This call does not * Fetches all rooms that belong to this user. This call does not
* populate the devices attribute in rooms. * populate the devices attribute in rooms.
* @returns {Promise<Undefined, String[]>} promise that resolves to void and rejects * @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a String array * with user-fiendly errors as a RemoteError
*/ */
static fetchAllRooms() { fetchAllRooms: () => {
return (dispatch) => { return (dispatch) => {
return Endpoint.get("/room") return Endpoint.get("/room")
.then((res) => void dispatch(actions.roomsUpdate(res.data))) .then((res) => void dispatch(actions.roomsUpdate(res.data)))
.catch((err) => { .catch((err) => {
console.error("Fetch all rooms error", err); console.error("Fetch all rooms error", err);
return ["Network error"]; throw new RemoteError(["Network error"]);
}); });
}; };
} },
/** /**
* Fetches all devices in a particular room, or fetches all devices. * Fetches all devices in a particular room, or fetches all devices.
* This also updates the devices attribute on values in the map rooms. * This also updates the devices attribute on values in the map rooms.
* @param {Number|null} roomId the room to which fetch devices * @param {Number|null} roomId the room to which fetch devices
* from, null to fetch from all rooms * from, null to fetch from all rooms
* @returns {Promise<Undefined, String[]>} promise that resolves to void and rejects * @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a String array * with user-fiendly errors as a RemoteError
*/ */
static fetchDevices(roomId = null) { fetchDevices: (roomId = null) => {
return (dispatch) => { return (dispatch) => {
return Endpoint.get(roomId ? `/room/${roomId}/device` : "/device") return Endpoint.get(roomId ? `/room/${roomId}/device` : "/device")
.then((res) => void dispatch(actions.devicesUpdate(roomId, res.data))) .then((res) => void dispatch(actions.devicesUpdate(roomId, res.data)))
.catch((err) => { .catch((err) => {
console.error(`Fetch devices roomId=${roomId} error`, err); console.error(`Fetch devices roomId=${roomId} error`, err);
return ["Network error"]; throw new RemoteError(["Network error"]);
}); });
}; };
} },
/** /**
* Creates/Updates a room with the given data * Creates/Updates a room with the given data
@ -232,10 +253,10 @@ export class RemoteService {
* @param {String} data.icon the room's icon name in SemanticUI icons * @param {String} data.icon the room's icon name in SemanticUI icons
* @param {String} data.image ths room's image, as base64 * @param {String} data.image ths room's image, as base64
* @param {Number|null} roomId the room's id if update, null for creation * @param {Number|null} roomId the room's id if update, null for creation
* @returns {Promise<Undefined, String[]>} promise that resolves to void and rejects * @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a String array * with user-fiendly errors as a RemoteError
*/ */
static saveRoom(data, roomId = null) { saveRoom: (data, roomId = null) => {
return (dispatch) => { return (dispatch) => {
data = { data = {
name: data.name, name: data.name,
@ -250,7 +271,7 @@ export class RemoteService {
.then((res) => void dispatch(actions.roomSave(res.data))) .then((res) => void dispatch(actions.roomSave(res.data)))
.catch(parseValidationErrors); .catch(parseValidationErrors);
}; };
} },
/** /**
* Creates/Updates a device with the given data. If * Creates/Updates a device with the given data. If
@ -261,10 +282,10 @@ export class RemoteService {
* is used for updates and the POST "/<device.kind>" * is used for updates and the POST "/<device.kind>"
* endpoints are used for creation. * endpoints are used for creation.
* @param {Device} data the device to update. * @param {Device} data the device to update.
* @returns {Promise<Undefined, String[]>} promise that resolves to void and rejects * @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a String array * with user-fiendly errors as a RemoteError
*/ */
static updateDevice(data) { updateDevice: (data) => {
return (dispatch) => { return (dispatch) => {
let url = "/device"; let url = "/device";
if ((data.id && data.flowType === "OUTPUT") || !data.id) { if ((data.id && data.flowType === "OUTPUT") || !data.id) {
@ -275,12 +296,12 @@ export class RemoteService {
.then((res) => void dispatch(actions.deviceUpdate(res.data))) .then((res) => void dispatch(actions.deviceUpdate(res.data)))
.catch((err) => { .catch((err) => {
console.warn("Update device: ", data, "error: ", err); console.warn("Update device: ", data, "error: ", err);
return ["Network error"]; throw new RemoteError(["Network error"]);
}); });
}; };
} },
static _operateInput(url, getUrl, action) { _operateInput: (url, getUrl, action) => {
return (dispatch) => { return (dispatch) => {
return Endpoint.put(url, {}, action) return Endpoint.put(url, {}, action)
.then(async (res) => { .then(async (res) => {
@ -290,10 +311,10 @@ export class RemoteService {
}) })
.catch((err) => { .catch((err) => {
console.warn(`${url} error`, err); console.warn(`${url} error`, err);
return ["Network error"]; throw new RemoteError(["Network error"]);
}); });
}; };
} },
/** /**
* Changes the state of a switch, by turning it on, off or toggling it. * Changes the state of a switch, by turning it on, off or toggling it.
@ -302,30 +323,30 @@ export class RemoteService {
* *
* @param {Number} switchId the switch device id * @param {Number} switchId the switch device id
* @param {SwitchOperation} type the operation to perform on the switch * @param {SwitchOperation} type the operation to perform on the switch
* @returns {Promise<Undefined, String[]>} promise that resolves to void and rejects * @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a String array * with user-fiendly errors as a RemoteError
*/ */
static switchOperate(switchId, type) { switchOperate: (switchId, type) => {
return this._operateInput("/switch/operate", `/switch/${switchId}`, { return this._operateInput("/switch/operate", `/switch/${switchId}`, {
type: type.toUpperCase(), type: type.toUpperCase(),
id: switchId, id: switchId,
}); });
} },
/** /**
* Turns a knob dimmer to a specific amount * Turns a knob dimmer to a specific amount
* *
* @param {Number} dimmerId the knob dimmer id * @param {Number} dimmerId the knob dimmer id
* @param {number} intensity the absolute intensity to dim to. Must be >=0 and <= 100. * @param {number} intensity the absolute intensity to dim to. Must be >=0 and <= 100.
* @returns {Promise<Undefined, String[]>} promise that resolves to void and rejects * @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a String array * with user-fiendly errors as a RemoteError
*/ */
static knobDimmerDimTo(dimmerId, intensity) { knobDimmerDimTo: (dimmerId, intensity) => {
return this._operateInput("/knobDimmer/dimTo", `/knobDimmer/${dimmerId}`, { return this._operateInput("/knobDimmer/dimTo", `/knobDimmer/${dimmerId}`, {
intensity, intensity,
id: dimmerId, id: dimmerId,
}); });
} },
/** /**
* Turns a button dimmer up or down * Turns a button dimmer up or down
@ -334,10 +355,10 @@ export class RemoteService {
* *
* @param {Number} dimmerId the button dimmer id * @param {Number} dimmerId the button dimmer id
* @param {ButtonDimmerDimType} dimType the type of dim to perform * @param {ButtonDimmerDimType} dimType the type of dim to perform
* @returns {Promise<Undefined, String[]>} promise that resolves to void and rejects * @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a String array * with user-fiendly errors as a RemoteError
*/ */
static buttonDimmerDim(dimmerId, dimType) { buttonDimmerDim: (dimmerId, dimType) => {
return this._operateInput( return this._operateInput(
"/buttonDimmer/dim", "/buttonDimmer/dim",
`/buttonDimmer/${dimmerId}`, `/buttonDimmer/${dimmerId}`,
@ -346,59 +367,63 @@ export class RemoteService {
id: dimmerId, id: dimmerId,
} }
); );
} },
/** /**
* Resets the meter on a smart plug * Resets the meter on a smart plug
* *
* @param {Number} smartPlugId the smart plug to reset * @param {Number} smartPlugId the smart plug to reset
* @returns {Promise<Undefined, String[]>} promise that resolves to void and rejects * @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a String array * with user-fiendly errors as a RemoteError
*/ */
static smartPlugReset(smartPlugId) { smartPlugReset(smartPlugId) {
return (dispatch) => { return (dispatch) => {
return Endpoint.delete(`/smartPlug/${smartPlugId}/meter`) return Endpoint.delete(`/smartPlug/${smartPlugId}/meter`)
.then((res) => dispatch(actions.deviceOperationUpdate([res.data]))) .then((res) => dispatch(actions.deviceOperationUpdate([res.data])))
.catch((err) => { .catch((err) => {
console.warn(`Smartplug reset error`, err); console.warn(`Smartplug reset error`, err);
return ["Network error"]; throw new RemoteError(["Network error"]);
}); });
}; };
} },
/** /**
* Deletes a room * Deletes a room
* @param {Number} roomId the id of the room to delete * @param {Number} roomId the id of the room to delete
* @returns {Promise<Undefined, String[]>} promise that resolves to void and rejects * @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a String array * with user-fiendly errors as a RemoteError
*/ */
static deleteRoom(roomId) { deleteRoom: (roomId) => {
return (dispatch) => { return (dispatch) => {
Endpoint.delete(`/room/${roomId}`) Endpoint.delete(`/room/${roomId}`)
.then((_) => dispatch(actions.roomDelete(roomId))) .then((_) => dispatch(actions.roomDelete(roomId)))
.catch((err) => { .catch((err) => {
console.warn("Room deletion error", err); console.warn("Room deletion error", err);
return ["Network error"]; throw new RemoteError(["Network error"]);
}); });
}; };
} },
/** /**
* Deletes a device * Deletes a device
* @param {Number} deviceId the id of the device to delete * @param {Number} deviceId the id of the device to delete
* @returns {Promise<Undefined, String[]>} promise that resolves to void and rejects * @returns {Promise<Undefined, RemoteError>} promise that resolves to void and rejects
* with user-fiendly errors as a String array * with user-fiendly errors as a RemoteError
*/ */
static deleteDevice(deviceId) { deleteDevice: (deviceId) => {
return (dispatch) => { return (dispatch) => {
Endpoint.delete(`/device/${deviceId}`) Endpoint.delete(`/device/${deviceId}`)
.then((_) => dispatch(actions.deviceDelete(deviceId))) .then((_) => dispatch(actions.deviceDelete(deviceId)))
.catch((err) => { .catch((err) => {
console.warn("Device deletion error", err); console.warn("Device deletion error", err);
return ["Network error"]; throw new RemoteError(["Network error"]);
}); });
}; };
} },
};
for (const key in RemoteService) {
RemoteService[key] = RemoteService[key].bind(RemoteService);
} }
/** Class to handle connection with the sensor socket */ /** Class to handle connection with the sensor socket */
@ -409,7 +434,7 @@ class ServiceSocket {
connection; connection;
static get URL() { static get URL() {
const httpURL = new URL(Endpoint.URL); const httpURL = new URL(endpointURL());
const isSecure = httpURL.protocol === "https:"; const isSecure = httpURL.protocol === "https:";
const protocol = isSecure ? "wss:" : "ws:"; const protocol = isSecure ? "wss:" : "ws:";
const port = httpURL.port || (isSecure ? 443 : 80); const port = httpURL.port || (isSecure ? 443 : 80);
@ -512,7 +537,7 @@ export class Forms {
.then((_) => void 0) .then((_) => void 0)
.catch((err) => { .catch((err) => {
console.warn("Init reset password failed", err); console.warn("Init reset password failed", err);
return ["Network error"]; throw new RemoteError(["Network error"]);
}); });
} }

View File

@ -1,68 +1,100 @@
import { createStore, applyMiddleware } from "redux"; import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk"; import thunk from "redux-thunk";
import actions from "./storeActions"; import actions from "./storeActions";
import update from "immutability-helper";
function reducer(previousState, action) { function reducer(previousState, action) {
let newState = Object.assign({}, previousState); let newState;
const createOrUpdateRoom = (room) => { const createOrUpdateRoom = (room) => {
if (!(room.id in newState.rooms)) { if (!(room.id in newState.rooms)) {
newState.rooms[room.id] = room; newState = update(newState, {
newState.rooms[room.id].devices = new Set(); rooms: { [room.id]: { devices: { $set: new Set() } } },
});
} else { } else {
newState.rooms[room.id].name = room.name; newState = update(newState, {
newState.rooms[room.id].image = room.image; rooms: {
newState.rooms[room.id].icon = room.icon; [room.id]: {
name: { $set: room.name },
image: { $set: room.image },
icon: { $set: room.icon },
},
},
});
} }
}; };
let change;
switch (action.type) { switch (action.type) {
case "LOGIN_UPDATE": case "LOGIN_UPDATE":
newState.login = action.login; newState = update(previousState, { login: { $set: action.login } });
delete newState.errors.login;
break; break;
case "USER_INFO_UPDATE": case "USER_INFO_UPDATE":
newState.user = action.user; newState = update(previousState, { userInfo: { $set: action.user } });
delete newState.errors.userInfo;
break; break;
case "ROOMS_UPDATE": case "ROOMS_UPDATE":
newState = previousState;
for (const room of action.rooms) { for (const room of action.rooms) {
createOrUpdateRoom(room); createOrUpdateRoom(room);
} }
delete newState.errors.rooms;
break; break;
case "DEVICES_UPDATE": case "DEVICES_UPDATE":
change = null;
// if room is given, delete all devices in that room // if room is given, delete all devices in that room
// and remove any join between that room and deleted // and remove any join between that room and deleted
// devices // devices
if (action.roomId) { if (action.roomId) {
change = {
rooms: { [action.roomId]: { devices: { $set: new Set() } } },
devices: { $unset: [] },
};
const room = newState.rooms[action.roomId]; const room = newState.rooms[action.roomId];
for (const deviceId of room.devices) { for (const deviceId of room.devices) {
delete newState.devices[deviceId]; change.devices.$unset.push(deviceId);
} }
room.devices = [];
} else if (action.partial) { } else if (action.partial) {
// if the update is partial and caused by an operation on an input // if the update is partial and caused by an operation on an input
// device (like /switch/operate), iteratively remove deleted // device (like /switch/operate), iteratively remove deleted
// devices and their join with their corresponding room. // devices and their join with their corresponding room.
change = {
devices: { $unset: [] },
rooms: {},
};
for (const device of action.devices) { for (const device of action.devices) {
const room = newState.rooms[newState.devices[device.id].roomId]; const roomId = previousState.devices[device.id].roomId;
room.devices.delete(device.id); change.rooms[roomId] = change.rooms[roomId] || {
delete newState.devices[device.id]; devices: { $remove: [] },
};
change.rooms[roomId].devices.$remove.push(device.id);
change.devices.$unset.push(device.id);
} }
} else { } else {
// otherwise, just delete all devices and all joins // otherwise, just delete all devices and all joins
// between rooms and devices // between rooms and devices
newState.devices = {}; change = {
devices: { $set: {} },
rooms: {},
};
for (const room of newState.rooms) { for (const room of newState.rooms) {
room.devices = []; change.rooms[room.id].devices = { $set: new Set() };
} }
} }
newState = update(previousState, change);
change = {
devices: {},
rooms: {},
};
for (const device of action.devices) { for (const device of action.devices) {
newState.devices[device.id] = device; change.devices[device.id] = { $set: device };
if (device.roomId in newState.rooms) { if (device.roomId in newState.rooms) {
newState.rooms[device.roomId].devices.add(device.id); const devices = change.rooms[device.roomId].devices;
devices.$add = devices.$add || [];
devices.$add.push(device.id);
} else { } else {
console.warn( console.warn(
"Cannot join device", "Cannot join device",
@ -72,14 +104,13 @@ function reducer(previousState, action) {
); );
} }
} }
newState = update(newState, change);
delete newState.errors.devices;
break; break;
case "ROOM_SAVE": case "ROOM_SAVE":
createOrUpdateRoom(action.room); createOrUpdateRoom(action.room);
break; break;
case "ROOM_DELETE": case "ROOM_DELETE":
if (!(actions.roomId in newState.rooms)) { if (!(actions.roomId in previousState.rooms)) {
console.warn(`Room to delete ${actions.roomId} does not exist`); console.warn(`Room to delete ${actions.roomId} does not exist`);
break; break;
} }
@ -87,31 +118,41 @@ function reducer(previousState, action) {
// This update does not ensure the consistent update of switchId/dimmerId properties // This update does not ensure the consistent update of switchId/dimmerId properties
// on output devices connected to an input device in this room. Please manually request // on output devices connected to an input device in this room. Please manually request
// all devices again if consistent update is desired // all devices again if consistent update is desired
for (const id of newState.rooms[action.roomId].devices) { change = { devices: { $unset: [] } };
delete newState.devices[id];
for (const id of previousState.rooms[action.roomId].devices) {
change.devices.$unset.push(id);
} }
delete newState.rooms[action.roomId]; change.rooms = { $unset: actions.roomId };
newState = update(previousState, change);
break; break;
case "DEVICE_DELETE": case "DEVICE_DELETE":
if (!(actions.deviceId in newState.devices)) { if (!(actions.deviceId in previousState.devices)) {
console.warn(`Device to delete ${actions.deviceId} does not exist`); console.warn(`Device to delete ${actions.deviceId} does not exist`);
break; break;
} }
newState.rooms[newState.devices[actions.deviceId].roomId].devices.delete( newState = update(previousState, {
actions.deviceId devices: { $unset: actions.deviceId },
); rooms: {
delete newState.devices[actions.deviceId]; [previousState.devices[actions.deviceId].roomId]: {
devices: { $remove: actions.deviceId },
},
},
});
break; break;
case "LOGOUT": case "LOGOUT":
newState.login = { token: null, loggedIn: false }; newState = {
newState.rooms = []; login: { loggedIn: false, token: null },
newState.devices = []; rooms: [],
delete newState.errors.login; devices: [],
userInfo: null,
};
break; break;
default: default:
console.warn(`Action type ${action.type} unknown`, action); console.warn(`Action type ${action.type} unknown`, action);
return previousState;
} }
console.log("new state: ", newState); console.log("new state: ", newState);
@ -123,6 +164,7 @@ function createSmartHutStore() {
const exp = localStorage.getItem("exp"); const exp = localStorage.getItem("exp");
const initialState = { const initialState = {
errors: {},
login: { login: {
token: token, token: token,
}, },
@ -133,7 +175,7 @@ function createSmartHutStore() {
devices: {}, devices: {},
}; };
initialState.login.loggedIn = token && exp > new Date().getTime(); initialState.login.loggedIn = !!(token && exp > new Date().getTime());
if (!initialState.login.loggedIn) { if (!initialState.login.loggedIn) {
localStorage.removeItem("token"); localStorage.removeItem("token");
localStorage.removeItem("exp"); localStorage.removeItem("exp");

View File

@ -9,8 +9,11 @@ import {
Icon, Icon,
Input, Input,
} from "semantic-ui-react"; } from "semantic-ui-react";
import { RemoteService } from "../remote";
import { withRouter } from "react-router-dom";
import { connect } from "react-redux";
export default class Login extends Component { class Login extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
@ -23,31 +26,15 @@ export default class Login extends Component {
handleLogin = (e) => { handleLogin = (e) => {
e.preventDefault(); e.preventDefault();
const params = {
usernameOrEmail: this.state.user,
password: this.state.password,
};
this.props this.props
.auth({ .login(this.state.user, this.state.password)
user: this.state.user, .then(() => this.props.history.push("/dashboard"))
params: params,
})
.then((res) => {
if (res.response.status === 200) {
} else if (res.response.status === 401) {
this.setState({
error: { state: true, message: "Wrong credentials" },
});
} else {
console.log(res);
this.setState({
error: { state: true, message: "An error occurred while logging" },
});
}
})
.catch((err) => { .catch((err) => {
console.log(err); console.log(err);
this.setState({
error: { state: true, message: err.messages.join(" - ") },
});
}); });
}; };
@ -108,7 +95,7 @@ export default class Login extends Component {
color="blue" color="blue"
fluid fluid
size="large" size="large"
onClick={this.handleLogin} onClick={this.handleLogin.bind(this)}
> >
Login Login
</Button> </Button>
@ -127,3 +114,9 @@ export default class Login extends Component {
); );
} }
} }
const mapStateToProps = (state, _) => ({ loggedIn: state.login.loggedIn });
const LoginContainer = withRouter(connect(mapStateToProps, RemoteService))(
Login
);
export default LoginContainer;

View File

@ -5228,6 +5228,13 @@ immer@1.10.0:
resolved "https://registry.yarnpkg.com/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d" resolved "https://registry.yarnpkg.com/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d"
integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg== integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg==
immutability-helper@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/immutability-helper/-/immutability-helper-3.0.2.tgz#e9187158b47c93368a92e84c31714c4b3dff30b0"
integrity sha512-fcrJ26wpvUcuGRpoGY4hyQ/JOeR1HAunMmE3C0XYXSe6plAGtgTlB2S4BzueBANCPrDJ7AByL1yrIRLIlVfwpA==
dependencies:
invariant "^2.2.4"
import-cwd@^2.0.0: import-cwd@^2.0.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"