import smartHutStore from "./store"; import actions from "./storeActions"; import axios from "axios"; class Endpoint { axiosInstance = axios.create({ baseURL: this.URL, validateStatus: (status) => status >= 200 && status < 300, }); /** * Returns the endpoint URL (SmartHut backend URL) * @returns {String} endpoint URL */ static get URL() { return window.BACKEND_URL !== "__BACKEND_URL__" ? window.BACKEND_URL : "http://localhost:8080"; } /** * Returns token for current session, null if logged out * @returns {String|null} the token */ static get token() { return smartHutStore.getState().login.token; } /** * Performs an authenticated request * @param {get|post|put|delete} the desired method * @param {String} route the desired route (e.g. "/rooms/1/devices") * @param {[String]String} query query ('?') parameters (no params by default) * @param {any} body the JSON request body */ static send(method, route, query = {}, body = null) { if (!this.token) { throw new Error("No token while performing authenticated request"); } if (method !== "get") return this.axiosInstance(route, { method: method, params: query, data: ["put", "post"].indexOf(method) !== -1 ? body : null, headers: { Authorization: `Bearer ${this.token}`, }, }).then((res) => { if (!res.data) { console.error("Response body is empty"); return null; } else { return res; } }); } /** * Performs an authenticated GET request * @param {String} route the desired route (e.g. "/rooms/1/devices") * @param {[String]String} query query ('?') parameters (no params by default) */ static get(route, query) { return this.send("get", route, query); } /** * Performs an authenticated POST request * @param {String} route the desired route (e.g. "/rooms/1/devices") * @param {[String]String} query query ('?') parameters (no params by default) * @param {any} body the JSON request body */ static post(route, query, body) { return this.send("post", route, query, body); } /** * Performs an authenticated PUT request * @param {String} route the desired route (e.g. "/rooms/1/devices") * @param {[String]String} query query ('?') parameters (no params by default) * @param {any} body the JSON request body */ static put(route, query, body) { return this.send("put", route, query, body); } /** * Performs an authenticated DELETE request * @param {get|post|put|delete} the desired method * @param {String} route the desired route (e.g. "/rooms/1/devices") * @param {[String]String} query query ('?') parameters (no params by default) */ static delete(route, query) { return this.send("delete", route, query); } } /** * Given an error response, returns an array of user * friendly messages to display to the user * @param {*} err the Axios error reponse object * @returns {String[]} user friendly error messages */ function parseValidationErrors(err) { if ( err.response && err.response.status === 400 && err.response.data && Array.isArray(err.response.data.errors) ) { return [...new Set(err.response.data.errors.map((e) => e.defaultMessage))]; } else { console.warn("Non validation error", err); return ["Network error"]; } } export class RemoteService { /** * 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(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)); }) .catch((err) => { console.warn("login error", err); return [ err.response.status === 401 ? "Wrong credentials" : "An error occurred while logging in", ]; }); }; } /** * Performs logout * @param {String} usernameOrEmail * @param {String} password */ static logout() { return (dispatch) => void dispatch(actions.logout()); } /** * Fetches user information via REST calls, if it is logged in * @returns {Promise} promise that resolves to void and rejects * with user-fiendly errors as a String array */ static fetchUserInfo() { return (dispatch) => { return Endpoint.get("/auth/profile") .then((res) => void dispatch(actions.userInfoUpdate(res.data))) .catch((err) => { console.warn("Fetch user info error", err); return ["Network error"]; }); }; } /** * Fetches all rooms that belong to this user. This call does not * populate the devices attribute in rooms. * @returns {Promise} promise that resolves to void and rejects * with user-fiendly errors as a String array */ static fetchAllRooms() { return (dispatch) => { return Endpoint.get("/room") .then((res) => void dispatch(actions.roomsUpdate(res.data))) .catch((err) => { console.error("Fetch all rooms error", err); return ["Network error"]; }); }; } /** * Fetches all devices in a particular room, or fetches all devices. * This also updates the devices attribute on values in the map rooms. * @param {Number|null} roomId the room to which fetch devices * from, null to fetch from all rooms * @returns {Promise} promise that resolves to void and rejects * with user-fiendly errors as a String array */ static fetchDevices(roomId = null) { return (dispatch) => { return Endpoint.get(roomId ? `/room/${roomId}/device` : "/device") .then((res) => void dispatch(actions.devicesUpdate(roomId, res.data))) .catch((err) => { console.error(`Fetch devices roomId=${roomId} error`, err); return ["Network error"]; }); }; } /** * Creates/Updates a room with the given data * @param {String} data.name the room's name, * @param {String} data.icon the room's icon name in SemanticUI icons * @param {String} data.image ths room's image, as base64 * @param {Number|null} roomId the room's id if update, null for creation * @returns {Promise} promise that resolves to void and rejects * with user-fiendly errors as a String array */ static saveRoom(data, roomId = null) { return (dispatch) => { data = { name: data.name, icon: data.icon, image: data.image, }; return (roomId ? Endpoint.put(`/room/${roomId}`, {}, data) : Endpoint.post(`/room`, {}, data) ) .then((res) => void dispatch(actions.roomSave(res.data))) .catch(parseValidationErrors); }; } /** * Creates/Updates a device with the given data. If * data.id is truthy, then a update call is performed, * otherwise a create call is performed. The update URL * is computed based on data.kind when data.flowType = * 'OUTPUT', otherwise the PUT "/device" endpoint * is used for updates and the POST "/" * endpoints are used for creation. * @param {Device} data the device to update. * @returns {Promise} promise that resolves to void and rejects * with user-fiendly errors as a String array */ static updateDevice(data) { return (dispatch) => { let url = "/device"; if ((data.id && data.flowType === "OUTPUT") || !data.id) { url = "/" + data.kind; } return Endpoint[data.id ? "put" : "post"](url, {}, data) .then((res) => void dispatch(actions.deviceUpdate(res.data))) .catch((err) => { console.warn("Update device: ", data, "error: ", err); return ["Network error"]; }); }; } static _operateInput(url, getUrl, action) { return (dispatch) => { return Endpoint.put(url, {}, action) .then(async (res) => { const inputDevice = Endpoint.get(getUrl); delete inputDevice.outputs; dispatch(actions.deviceOperationUpdate([...res.data, inputDevice])); }) .catch((err) => { console.warn(`${url} error`, err); return ["Network error"]; }); }; } /** * Changes the state of a switch, by turning it on, off or toggling it. * * @typedef {"ON" | "OFF" | "TOGGLE"} SwitchOperation * * @param {Number} switchId the switch device id * @param {SwitchOperation} type the operation to perform on the switch * @returns {Promise} promise that resolves to void and rejects * with user-fiendly errors as a String array */ static switchOperate(switchId, type) { return this._operateInput("/switch/operate", `/switch/${switchId}`, { type: type.toUpperCase(), id: switchId, }); } /** * Turns a knob dimmer to a specific amount * * @param {Number} dimmerId the knob dimmer id * @param {number} intensity the absolute intensity to dim to. Must be >=0 and <= 100. * @returns {Promise} promise that resolves to void and rejects * with user-fiendly errors as a String array */ static knobDimmerDimTo(dimmerId, intensity) { return this._operateInput("/knobDimmer/dimTo", `/knobDimmer/${dimmerId}`, { intensity, id: dimmerId, }); } /** * Turns a button dimmer up or down * * @typedef {"UP" | "DOWN"} ButtonDimmerDimType * * @param {Number} dimmerId the button dimmer id * @param {ButtonDimmerDimType} dimType the type of dim to perform * @returns {Promise} promise that resolves to void and rejects * with user-fiendly errors as a String array */ static buttonDimmerDim(dimmerId, dimType) { return this._operateInput( "/buttonDimmer/dim", `/buttonDimmer/${dimmerId}`, { dimType, id: dimmerId, } ); } /** * Resets the meter on a smart plug * * @param {Number} smartPlugId the smart plug to reset * @returns {Promise} promise that resolves to void and rejects * with user-fiendly errors as a String array */ static smartPlugReset(smartPlugId) { return (dispatch) => { return Endpoint.delete(`/smartPlug/${smartPlugId}/meter`) .then((res) => dispatch(actions.deviceOperationUpdate([res.data]))) .catch((err) => { console.warn(`Smartplug reset error`, err); return ["Network error"]; }); }; } /** * Deletes a room * @param {Number} roomId the id of the room to delete * @returns {Promise} promise that resolves to void and rejects * with user-fiendly errors as a String array */ static deleteRoom(roomId) { return (dispatch) => { Endpoint.delete(`/room/${roomId}`) .then((_) => dispatch(actions.roomDelete(roomId))) .catch((err) => { console.warn("Room deletion error", err); return ["Network error"]; }); }; } /** * Deletes a device * @param {Number} deviceId the id of the device to delete * @returns {Promise} promise that resolves to void and rejects * with user-fiendly errors as a String array */ static deleteDevice(deviceId) { return (dispatch) => { Endpoint.delete(`/device/${deviceId}`) .then((_) => dispatch(actions.deviceDelete(deviceId))) .catch((err) => { console.warn("Device deletion error", err); return ["Network error"]; }); }; } } export class Forms { /** * Attempts to create a new user from the given data. * This method does not update the global state, * please check its return value. * @param {String} data.username the chosen username * @param {String} data.password the chosen password * @param {String} data.email the chosen email * @param {String} data.name the chosen full name * @returns {Promise} promise that resolves to void and rejects * with validation errors as a String array */ static submitRegistration(data) { return Endpoint.post( "/register", {}, { username: data.username, password: data.password, name: data.name, email: data.email, } ) .then((_) => void 0) .catch(parseValidationErrors); } /** * Sends a request to perform a password reset. * This method does not update the global state, * please check its return value. * @param {String} email the email to which perform the reset * @returns {Promise} promise that resolves to void and rejects * with validation errors as a String array */ static submitInitResetPassword(email) { return Endpoint.post( "/register/init-reset-password", {}, { usernameOrEmail: email, } ) .then((_) => void 0) .catch((err) => { console.warn("Init reset password failed", err); return ["Network error"]; }); } /** * Sends the password for the actual password reset, haviug already * performed email verification * This method does not update the global state, * please check its return value. * @param {String} password the new password * @returns {Promise} promise that resolves to void and rejects * with validation errors as a String array */ static submitResetPassword(password) { return Endpoint.post( "/register/reset-password", {}, { password, } ) .then((_) => void 0) .catch(parseValidationErrors); } }