460 lines
14 KiB
JavaScript
460 lines
14 KiB
JavaScript
|
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<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.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<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"];
|
||
|
});
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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);
|
||
|
}
|
||
|
}
|