frontend/smart-hut/src/remote.js

540 lines
16 KiB
JavaScript
Raw Normal View History

2020-04-07 10:46:55 +00:00
import smartHutStore from "./store";
import actions from "./storeActions";
import axios from "axios";
class Endpoint {
socket = null;
2020-04-07 10:46:55 +00:00
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 login
* @param {String} usernameOrEmail
* @param {String} password
* @returns {Promise<Undefined, String[]>} 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;
}
2020-04-07 10:46:55 +00:00
/**
* 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<Undefined, String[]>} promise that resolves to void and rejects
* with user-fiendly errors as a String array
*/
static login(usernameOrEmail, password) {
return (dispatch) => {
return Endpoint.login(dispatch, usernameOrEmail, password)
.then((token) => dispatch(actions.loginSuccess(token)))
2020-04-07 10:46:55 +00:00
.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) => Endpoint.logout.then(void dispatch(actions.logout()));
2020-04-07 10:46:55 +00:00
}
/**
* Fetches user information via REST calls, if it is logged in
* @returns {Promise<Undefined, String[]>} 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<Undefined, String[]>} 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<Undefined, String[]>} 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<Undefined, String[]>} 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 "/<device.kind>"
* endpoints are used for creation.
* @param {Device} data the device to update.
* @returns {Promise<Undefined, String[]>} 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<Undefined, String[]>} 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<Undefined, String[]>} 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<Undefined, String[]>} 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<Undefined, String[]>} 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<Undefined, String[]>} 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<Undefined, String[]>} 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"];
});
};
}
}
/** 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();
}
}
2020-04-07 10:46:55 +00:00
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<Undefined, String[]>} 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<Undefined, String[]>} 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<Undefined, String[]>} 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);
}
}