components/items/routes.js

  1. import { itemAllowedForClient, itemsFilterForClient } from './security.js';
  2. import { requireHeader } from './../middleware.js';
  3. import { backendInfo } from '../../server.js';
  4. import { getAllItems, getItem, getItemState, getItemSemantic, sendItemCommand, sendEventsItems } from './backend.js';
  5. import proxy from 'express-http-proxy';
  6. const itemAccess = () => {
  7. return async function (req, res, next) {
  8. const org = req.headers['x-openhab-org'] || '';
  9. const user = req.headers['x-openhab-user'];
  10. try {
  11. const allowed = await itemAllowedForClient(backendInfo.HOST, req, user, org, req.params.itemname);
  12. if (allowed === true) {
  13. next();
  14. } else {
  15. res.status(403).send();
  16. }
  17. } catch {
  18. res.status(500).send();
  19. }
  20. };
  21. };
  22. /**
  23. * Provides required /items routes.
  24. *
  25. * @memberof routes
  26. * @param {*} app expressjs app
  27. */
  28. const items = (app) => {
  29. /**
  30. * @swagger
  31. * /auth/items:
  32. * get:
  33. * summary: Authorization endpoint for Item access.
  34. * description: Used by NGINX auth_request.
  35. * tags: [Auth]
  36. * parameters:
  37. * - in: header
  38. * name: X-OPENHAB-USER
  39. * required: true
  40. * description: Name of user
  41. * schema:
  42. * type: string
  43. * style: form
  44. * - in: header
  45. * name: X-OPENHAB-ORG
  46. * required: false
  47. * description: Organisations the user is member of
  48. * schema:
  49. * type: string
  50. * style: form
  51. * - in: header
  52. * name: X-ORIGINAL-URI
  53. * required: true
  54. * description: Original request URI
  55. * schema:
  56. * type: string
  57. * style: form
  58. * responses:
  59. * 200:
  60. * description: Allowed
  61. * 403:
  62. * description: Forbidden
  63. */
  64. app.get('/auth/items', requireHeader('X-OPENHAB-USER'), requireHeader('X-ORIGINAL-URI'), async (req, res) => {
  65. const org = req.headers['x-openhab-org'] || '';
  66. const user = req.headers['x-openhab-user'];
  67. const regex1 = /(\?|&)items=([a-zA-Z_0-9]+)[&]?/;
  68. const regex2 = /\/items\/([a-zA-Z_0-9]+)/;
  69. const itemname1 = regex1.exec(req.headers['x-original-uri']);
  70. const itemname2 = regex2.exec(req.headers['x-original-uri']);
  71. const itemname = (itemname1 == null) ? itemname2 : itemname1;
  72. if (itemname == null) return res.status(403).send();
  73. try {
  74. const allowed = await itemAllowedForClient(backendInfo.HOST, req, user, org, itemname[1]);
  75. if (allowed === true) {
  76. res.status(200).send();
  77. } else {
  78. res.status(403).send();
  79. }
  80. } catch {
  81. res.status(500).send();
  82. }
  83. });
  84. /**
  85. * @swagger
  86. * /rest/items:
  87. * get:
  88. * summary: Get all available Items.
  89. * tags: [Items]
  90. * parameters:
  91. * - in: query
  92. * name: parameters
  93. * required: false
  94. * description: Query parameters from API (metadata, recursive, type, tags, fields)
  95. * schema:
  96. * type: string
  97. * style: form
  98. * - in: header
  99. * name: X-OPENHAB-USER
  100. * required: true
  101. * description: Name of user
  102. * schema:
  103. * type: string
  104. * style: form
  105. * - in: header
  106. * name: X-OPENHAB-ORG
  107. * required: false
  108. * description: Organisations the user is member of
  109. * schema:
  110. * type: string
  111. * style: form
  112. * responses:
  113. * 200:
  114. * description: OK
  115. * content:
  116. * application/json:
  117. * schema:
  118. * type: object
  119. */
  120. app.get('/rest/items', requireHeader('X-OPENHAB-USER'), async (req, res) => {
  121. const org = req.headers['x-openhab-org'] || '';
  122. const user = req.headers['x-openhab-user'];
  123. try {
  124. const allItems = await getAllItems(backendInfo.HOST, req);
  125. let filteredItems = [];
  126. for (const i in allItems) {
  127. if (await itemAllowedForClient(backendInfo.HOST, req, user, org, allItems[i].name) === true) {
  128. let tempItem = allItems[i];
  129. const tempItem2 = allItems[i].members;
  130. //recursive filtering of member items
  131. if (Array.isArray(tempItem2)) {
  132. tempItem.members = [];
  133. const tempMembers = await itemsFilterForClient(backendInfo.HOST, req, user, org, tempItem2);
  134. tempItem.members = tempMembers;
  135. }
  136. filteredItems.push(tempItem);
  137. }
  138. }
  139. res.status(200).send(filteredItems);
  140. } catch (e) {
  141. console.info(e);
  142. res.status(500).send();
  143. }
  144. });
  145. /**
  146. * @swagger
  147. * /rest/items/{itemname}:
  148. * get:
  149. * summary: Gets a single Item.
  150. * tags: [Items]
  151. * parameters:
  152. * - in: path
  153. * name: itemname
  154. * required: true
  155. * description: Item name
  156. * schema:
  157. * type: string
  158. * style: form
  159. * - in: query
  160. * name: parameters
  161. * required: false
  162. * description: Query parameters from API (metadata, recursive)
  163. * schema:
  164. * type: string
  165. * style: form
  166. * - in: header
  167. * name: X-OPENHAB-USER
  168. * required: true
  169. * description: Name of user
  170. * schema:
  171. * type: string
  172. * style: form
  173. * - in: header
  174. * name: X-OPENHAB-ORG
  175. * required: false
  176. * description: Organisations the user is member of
  177. * schema:
  178. * type: string
  179. * style: form
  180. * responses:
  181. * 200:
  182. * description: OK
  183. * content:
  184. * application/json:
  185. * schema:
  186. * type: object
  187. * 403:
  188. * description: Item access forbidden
  189. * 404:
  190. * description: Item not found
  191. */
  192. app.get('/rest/items/:itemname', requireHeader('X-OPENHAB-USER'), itemAccess(), async (req, res) => {
  193. const org = req.headers['x-openhab-org'] || '';
  194. const user = req.headers['x-openhab-user'];
  195. try {
  196. let response = await getItem(backendInfo.HOST, req, req.params.itemname);
  197. const tempItem = response.json.members;
  198. //recursive filtering of member items
  199. if (Array.isArray(tempItem)) {
  200. response.json.members = [];
  201. const tempMembers = await itemsFilterForClient(backendInfo.HOST, req, user, org, tempItem);
  202. response.json.members = tempMembers;
  203. }
  204. res.status(response.status).send(response.json);
  205. } catch (e) {
  206. console.info(e);
  207. res.status(500).send();
  208. }
  209. });
  210. /**
  211. * @swagger
  212. * /rest/items/{itemname}/state:
  213. * get:
  214. * summary: Gets the state of an Item.
  215. * tags: [Items]
  216. * parameters:
  217. * - in: path
  218. * name: itemname
  219. * required: true
  220. * description: Item name
  221. * schema:
  222. * type: string
  223. * style: form
  224. * - in: header
  225. * name: X-OPENHAB-USER
  226. * required: true
  227. * description: Name of user
  228. * schema:
  229. * type: string
  230. * style: form
  231. * - in: header
  232. * name: X-OPENHAB-ORG
  233. * required: false
  234. * description: Organisations the user is member of
  235. * schema:
  236. * type: string
  237. * style: form
  238. * responses:
  239. * 200:
  240. * description: OK
  241. * content:
  242. * text/plain:
  243. * schema:
  244. * type: string
  245. * 403:
  246. * description: Item access forbidden
  247. * 404:
  248. * description: Item not found
  249. */
  250. app.get('/rest/items/:itemname/state', requireHeader('X-OPENHAB-USER'), itemAccess(), async (req, res) => {
  251. const response = await getItemState(backendInfo.HOST, req, req.params.itemname);
  252. res.status(response.status).send(response.state);
  253. });
  254. /**
  255. * @swagger
  256. * /rest/items/{itemname}/semantic/{semanticClass}:
  257. * get:
  258. * summary: Gets the item which defines the requested semantics of an Item.
  259. * tags: [Items]
  260. * parameters:
  261. * - in: path
  262. * name: itemname
  263. * required: true
  264. * description: Item name
  265. * schema:
  266. * type: string
  267. * style: form
  268. * - in: path
  269. * name: semanticClass
  270. * required: true
  271. * description: Semantic class
  272. * schema:
  273. * type: string
  274. * style: form
  275. * - in: header
  276. * name: X-OPENHAB-USER
  277. * required: true
  278. * description: Name of user
  279. * schema:
  280. * type: string
  281. * style: form
  282. * - in: header
  283. * name: X-OPENHAB-ORG
  284. * required: false
  285. * description: Organisations the user is member of
  286. * schema:
  287. * type: string
  288. * style: form
  289. * responses:
  290. * 200:
  291. * description: OK
  292. * content:
  293. * application/json:
  294. * schema:
  295. * type: object
  296. * 403:
  297. * description: Item access forbidden
  298. * 404:
  299. * description: Item not found
  300. */
  301. app.get('/rest/items/:itemname/semantic/:semanticClass', requireHeader('X-OPENHAB-USER'), itemAccess(), async (req, res) => {
  302. const response = await getItemSemantic(backendInfo.HOST, req, req.params.itemname, req.params.semanticClass);
  303. res.status(response.status).send(response.json);
  304. });
  305. /**
  306. * @swagger
  307. * /rest/items/{itemname}:
  308. * post:
  309. * summary: Sends a command to an Item.
  310. * tags: [Items]
  311. * parameters:
  312. * - in: path
  313. * name: itemname
  314. * required: true
  315. * description: Item name
  316. * schema:
  317. * type: string
  318. * style: form
  319. * - in: header
  320. * name: X-OPENHAB-USER
  321. * required: true
  322. * description: Name of user
  323. * schema:
  324. * type: string
  325. * style: form
  326. * - in: header
  327. * name: X-OPENHAB-ORG
  328. * required: false
  329. * description: Organisations the user is member of
  330. * schema:
  331. * type: string
  332. * style: form
  333. * requestBody:
  334. * description: valid item command (e.g. ON, OFF, UP, DOWN, REFRESH)
  335. * required: true
  336. * content:
  337. * text/plain:
  338. * schema:
  339. * type: string
  340. * responses:
  341. * 200:
  342. * description: OK
  343. * 400:
  344. * description: Item command null
  345. * 403:
  346. * description: Item access forbidden
  347. * 404:
  348. * description: Item not found
  349. */
  350. app.post('/rest/items/:itemname', requireHeader('X-OPENHAB-USER'), itemAccess(), async (req, res) => {
  351. const status = await sendItemCommand(backendInfo.HOST, req, req.params.itemname, req.body);
  352. res.status(status).send();
  353. });
  354. /**
  355. * @swagger
  356. * /rest/events/states/{connectionId}:
  357. * post:
  358. * summary: Changes list of items a SSE connection will receive state updates to.
  359. * tags: [Items]
  360. * parameters:
  361. * - in: path
  362. * name: connectionId
  363. * required: true
  364. * description: Connection ID
  365. * schema:
  366. * type: string
  367. * style: form
  368. * - in: header
  369. * name: X-OPENHAB-USER
  370. * required: true
  371. * description: Name of user
  372. * schema:
  373. * type: string
  374. * style: form
  375. * - in: header
  376. * name: X-OPENHAB-ORG
  377. * required: false
  378. * description: Organisations the user is member of
  379. * schema:
  380. * type: string
  381. * style: form
  382. * requestBody:
  383. * description: items list
  384. * required: true
  385. * content:
  386. * text/plain:
  387. * schema:
  388. * type: string
  389. * responses:
  390. * 200:
  391. * description: OK
  392. * 404:
  393. * description: Items not found / list is empty
  394. */
  395. app.post('/rest/events/states/:connectionId', requireHeader('X-OPENHAB-USER'), async (req, res) => {
  396. const org = req.headers['x-openhab-org'] || '';
  397. const user = req.headers['x-openhab-user'];
  398. const allItems = req.body;
  399. try {
  400. let filteredItems = [];
  401. for (const i in allItems) {
  402. if (await itemAllowedForClient(backendInfo.HOST, req, user, org, allItems[i]) === true) {
  403. filteredItems.push(allItems[i]);
  404. }
  405. }
  406. const status = await sendEventsItems(backendInfo.HOST, req, req.params.connectionId, filteredItems);
  407. res.status(status).send();
  408. } catch (e) {
  409. console.info(e);
  410. res.status(500).send();
  411. }
  412. });
  413. /**
  414. * @swagger
  415. * /chart?items={itemname}:
  416. * get:
  417. * summary: Gets BasicUI OH chart.
  418. * tags: [Sitemaps]
  419. * parameters:
  420. * - in: query
  421. * name: parameters
  422. * required: true
  423. * description: Query parameters (e.g. items)
  424. * schema:
  425. * type: string
  426. * style: form
  427. * - in: header
  428. * name: X-OPENHAB-USER
  429. * required: true
  430. * description: Name of user
  431. * schema:
  432. * type: string
  433. * style: form
  434. * - in: header
  435. * name: X-OPENHAB-ORG
  436. * required: false
  437. * description: Organisations the user is member of
  438. * schema:
  439. * type: string
  440. * style: form
  441. * responses:
  442. * 200:
  443. * description: OK
  444. * 404:
  445. * description: Chart not found
  446. */
  447. app.get('/chart', requireHeader('X-OPENHAB-USER'), proxy(backendInfo.HOST + '/chart', {
  448. proxyReqPathResolver: async (req) => {
  449. const org = req.headers['x-openhab-org'] || '';
  450. const user = req.headers['x-openhab-user'];
  451. const reqPath = req.path;
  452. let reqQuery = req.query;
  453. //filtering of chart items
  454. if (reqQuery.hasOwnProperty('items')) {
  455. let allItems = reqQuery.items;
  456. if (typeof allItems === 'string') allItems = allItems.toString().split(',');
  457. let filteredItems = [];
  458. for (const i in allItems) {
  459. if (await itemAllowedForClient(backendInfo.HOST, req, user, org, allItems[i]) === true) filteredItems.push(allItems[i]);
  460. }
  461. reqQuery.items = filteredItems.toString();
  462. }
  463. let updatedQuery = Object.entries(reqQuery).reduce((str, [p, val]) => {
  464. return `${str}&${p}=${val}`;
  465. }, '');
  466. updatedQuery = updatedQuery.substring(1);
  467. return reqPath + ((updatedQuery) ? '?' + updatedQuery : '');
  468. },
  469. proxyErrorHandler: function(err, res, next) {
  470. console.info(err);
  471. res.status(500).send();
  472. }
  473. }));
  474. /**
  475. * @swagger
  476. * /analyzer/:
  477. * get:
  478. * summary: Analyze Item(s) using built-in OH analyzer. Requires nginx.
  479. * tags: [Items]
  480. * parameters:
  481. * - in: query
  482. * name: items
  483. * required: true
  484. * description: Item name
  485. * schema:
  486. * type: string
  487. * style: form
  488. * - in: query
  489. * name: parameters
  490. * required: false
  491. * description: Analyzer parameters (e.g. chartType)
  492. * schema:
  493. * type: string
  494. * style: form
  495. * responses:
  496. * 200:
  497. * description: OK
  498. * 404:
  499. * description: Unknown Item or persistence service
  500. */
  501. /**
  502. * @swagger
  503. * /rest/persistence/items/{itemname}:
  504. * get:
  505. * summary: Gets item persistence data from the persistence service. Requires nginx.
  506. * tags: [Items]
  507. * parameters:
  508. * - in: path
  509. * name: itemname
  510. * required: true
  511. * description: Item name
  512. * schema:
  513. * type: string
  514. * style: form
  515. * - in: query
  516. * name: parameters
  517. * required: false
  518. * description: Query parameters (e.g. serviceId, starttime, endtime, page, pagelength, boundary)
  519. * schema:
  520. * type: string
  521. * style: form
  522. * responses:
  523. * 200:
  524. * description: OK
  525. * content:
  526. * application/json:
  527. * schema:
  528. * type: object
  529. * 404:
  530. * description: Unknown Item or persistence service
  531. */
  532. };
  533. export default items;