diff --git a/extra/api-spec.json5 b/extra/api-spec.json5 new file mode 100644 index 00000000..3be0d389 --- /dev/null +++ b/extra/api-spec.json5 @@ -0,0 +1,68 @@ +[ + { + "name": "getPushExample", + "description": "Get a push example.", + "params": [ + { + "name": "language", + "type": "string", + "description": "The programming language such as `javascript-fetch` or `python`. See the directory ./extra/push-examples for a list of available languages." + } + ], + "returnType": "response-json", + "okReturn": [ + { + "name": "code", + "type": "string", + "description": "The push example." + } + ], + "possibleErrorReasons": [ + "The parameter `language` is not available" + ], + }, + { + "name": "checkApprise", + "description": "Check if the apprise library is installed.", + "params": [], + "returnType": "boolean", + }, + { + "name": "getSettings", + "description": "", + "params": [], + "returnType": "response-json", + "okReturn": [ + { + "name": "data", + "type": "object", + "description": "The setting object. It does not contain default values." + } + ], + "possibleErrorReasons": [], + }, + { + "name": "changePassword", + "description": "", + "params": [ + { + "name": "password", + "type": "object", + "description": "The password object with the following properties: `currentPassword` and `newPassword`" + } + ], + "returnType": "response-json", + "okReturn": [ + { + "name": "data", + "type": "object", + "description": "The setting object. It does not contain default values." + } + ], + "possibleErrorReasons": [ + "Incorrect current password", + "Invalid new password", + "Password is too weak" + ], + } +] diff --git a/package-lock.json b/package-lock.json index 5e638947..4a70a7b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "iconv-lite": "~0.6.3", "isomorphic-ws": "^5.0.0", "jsesc": "~3.0.2", + "json5": "~2.2.3", "jsonata": "^2.0.3", "jsonwebtoken": "~9.0.0", "jwt-decode": "~3.1.2", @@ -13206,7 +13207,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, "bin": { "json5": "lib/cli.js" }, diff --git a/package.json b/package.json index 9cd26bd0..fa3caf0f 100644 --- a/package.json +++ b/package.json @@ -106,6 +106,7 @@ "iconv-lite": "~0.6.3", "isomorphic-ws": "^5.0.0", "jsesc": "~3.0.2", + "json5": "~2.2.3", "jsonata": "^2.0.3", "jsonwebtoken": "~9.0.0", "jwt-decode": "~3.1.2", diff --git a/server/routers/api-router.js b/server/routers/api-router.js index 605fe72e..04da8a95 100644 --- a/server/routers/api-router.js +++ b/server/routers/api-router.js @@ -16,6 +16,9 @@ const ioClient = require("socket.io-client").io; const Socket = require("socket.io-client").Socket; const { headerAuthMiddleware } = require("../auth"); const jwt = require("jsonwebtoken"); +const fs = require("fs"); +const JSON5 = require("json5"); +const apiSpec = JSON5.parse(fs.readFileSync("./extra/api-spec.json5", "utf8")); let router = express.Router(); @@ -142,11 +145,11 @@ router.post("/api", headerAuthMiddleware, async (request, response) => { try { let result = await socketClientHandler(socket, token, requestData); - let status = 404; + let status = 200; if (result.status) { status = result.status; - } else if (result.ok) { - status = 200; + } else if (typeof result === "object" && result.ok === false) { + status = 404; } response.status(status).json(result); } catch (e) { @@ -170,6 +173,50 @@ function socketClientHandler(socket, token, requestData) { socket.on("connect", () => { socket.emit("loginByToken", token, (res) => { if (res.ok) { + let matched = false; + + // Find the action in the API spec + for (let actionObj of apiSpec) { + + // Find it + if (action === actionObj.name) { + matched = true; + let flatParams = []; + + // Check if required parameters are provided + if (actionObj.params.length > 0 && !params) { + reject({ + status: 400, + ok: false, + msg: "Missing \"params\" property in request body", + }); + return; + } + + // Check if required parameters are valid + for (let paramObj of actionObj.params) { + let value = params[paramObj.name]; + + // Check if required parameter is in a correct data type + if (typeof value !== paramObj.type) { + reject({ + status: 400, + ok: false, + msg: `Parameter "${paramObj.name}" should be "${paramObj.type}". Got "${typeof value}" instead.` + }); + return; + } + + flatParams.push(value); + } + + socket.emit(actionObj.name, ...flatParams, (res) => { + resolve(res); + }); + + break; + } + } if (action === "getPushExample") { if (params.length <= 0) { @@ -183,8 +230,9 @@ function socketClientHandler(socket, token, requestData) { resolve(res); }); } + } - } else { + if (!matched) { reject({ status: 404, ok: false, diff --git a/server/server.js b/server/server.js index 3cb9f64a..e46a45fc 100644 --- a/server/server.js +++ b/server/server.js @@ -1255,6 +1255,10 @@ let needSetup = false; try { checkLogin(socket); + if (typeof password.currentPassword === "undefined") { + throw new Error("Incorrect current password"); + } + if (!password.newPassword) { throw new Error("Invalid new password"); } diff --git a/server/util-server.js b/server/util-server.js index 1540f689..b96984a8 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -833,7 +833,7 @@ exports.checkLogin = (socket) => { */ exports.doubleCheckPassword = async (socket, currentPassword) => { if (typeof currentPassword !== "string") { - throw new Error("Wrong data type?"); + throw new Error("Wrong data type of current password"); } let user = await R.findOne("user", " id = ? AND active = 1 ", [ diff --git a/test/api.http b/test/api.http index d76cee08..8c6820c1 100644 --- a/test/api.http +++ b/test/api.http @@ -4,8 +4,40 @@ Content-Type: application/json { "action": "getPushExample", - "params": [ - "javascript-fetch" - ] + "params": { + "language": "javascript-fetch" + } } + +### +POST http://localhost:3001/api +Authorization: Bearer uk1_1HaQRETls-E5KlhB6yCtf8WJRW57KwFMuKkya-Tj +Content-Type: application/json + +{ + "action": "checkApprise" +} + ### +POST http://localhost:3001/api +Authorization: Bearer uk1_1HaQRETls-E5KlhB6yCtf8WJRW57KwFMuKkya-Tj +Content-Type: application/json + +{ + "action": "getSettings" +} + +### +POST http://localhost:3001/api +Authorization: Bearer uk1_1HaQRETls-E5KlhB6yCtf8WJRW57KwFMuKkya-Tj +Content-Type: application/json + +{ + "action": "changePassword", + "params": { + "password": { + "currentPassword": "123456", + "newPassword": "1sfdsf234567" + } + } +}