api.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. const mime = require("mime-types");
  2. const fs = require('fs');
  3. const Path = require('path');
  4. const Security = require('../src/security.js');
  5. const MediaService = require('../model/mediaService.js');
  6. const MediaFileMetaModel = require('../model/mediaItemMeta.js').MediaFileMetaModel;
  7. const MediaFileTagModel = require('../model/mediaItemTag.js').MediaFileTagModel;
  8. const { AccessModel, ACCESS_TYPE, ACCESS_GRANT, ACCESS_TO } = require('../model/access.js');
  9. const MailService = require('../src/mailService.js');
  10. function MediaToJson(mediaData) {
  11. if (!mediaData)
  12. return null;
  13. if (mediaData.accessType === ACCESS_GRANT.readNoMeta)
  14. mediaData.meta = {
  15. height: mediaData.meta?.height,
  16. width: mediaData.meta?.width
  17. };
  18. return mediaData;
  19. }
  20. function accessToJson(access) {
  21. const typeStr = [ "unknown", "ldapAccount", "email", "link", "every one" ][access.type];
  22. const accessToStr = [ "unknown", "item", "tag", "meta", "everything", "admin"][access.accessTo];
  23. const grantStr = [ "none", "read", "write", "read without meta"][access.grant];
  24. return {
  25. id: access.id,
  26. type: typeStr,
  27. typeLabel: access.typeLabel,
  28. typeData: access.typeData,
  29. accessTo: accessToStr,
  30. accessToData: access.accessToData,
  31. grant: grantStr
  32. };
  33. }
  34. async function accessListToJson(app, req) {
  35. let result = {
  36. ...(req.sessionObj?.accessList || {})
  37. };
  38. result.isAdmin = await req.sessionObj?.accessList?.isAdmin?.(app, result) || false;
  39. delete result.isAdmin_;
  40. return result;
  41. }
  42. module.exports = { register: app => {
  43. app.router.post("/api/server/reboot", async (req, res) => {
  44. app.routerUtils.onApiRequest(req, res);
  45. if (!await req.sessionObj?.accessList?.isAdmin(app, req.sessionObj?.accessList))
  46. return app.routerUtils.onBadRequest(res);
  47. console.log("Starting reboot process, initiaed from ", res.sessionObj);
  48. app.server.close(() => {
  49. require("child_process").spawn(process.argv.shift(), process.argv, {
  50. cwd: process.cwd(),
  51. detached : true,
  52. stdio: "inherit"
  53. }).unref();
  54. setTimeout(() => process.exit(), 500);
  55. });
  56. app.routerUtils.jsonResponse(res, {});
  57. });
  58. app.router.post("/api/database/reload", async (req, res) => {
  59. app.routerUtils.onApiRequest(req, res);
  60. if (!await req.sessionObj?.accessList?.isAdmin(app, req.sessionObj?.accessList))
  61. return app.routerUtils.onBadRequest(res);
  62. app.libraryManager.forceReload(app);
  63. app.routerUtils.jsonResponse(res, {});
  64. });
  65. app.router.post("/api/database/scan", async (req, res) => {
  66. app.routerUtils.onApiRequest(req, res);
  67. if (!await req.sessionObj?.accessList?.isAdmin(app, req.sessionObj?.accessList))
  68. return app.routerUtils.onBadRequest(res);
  69. app.libraryManager.updateLibraries(app);
  70. app.routerUtils.jsonResponse(res, {});
  71. });
  72. app.router.get("/api/access/list", async (req, res) => {
  73. app.routerUtils.onApiRequest(req, res);
  74. app.routerUtils.jsonResponse(res, await accessListToJson(app, req));
  75. });
  76. app.router.post("/api/access/login", async (req, res) => { // /api/access/login, post: { username: string, password: string }
  77. app.routerUtils.onApiRequest(req, res);
  78. if (!req.post?.username?.length || !req.post?.password?.length)
  79. return app.routerUtils.httpResponse(res, 400, "Missing argument");
  80. try {
  81. const access = await app.databaseHelper.fetch(AccessModel, { type: ACCESS_TYPE.ldapAccount, typeData: req.post.username });
  82. if (access?.length) {
  83. await Security.tryLoginIntoSession(req, access, req.post.username, req.post.password);
  84. if (access.find(x => x.accessTo == ACCESS_TO.admin))
  85. Security.setAdmin(req, true);
  86. } else {
  87. return app.routerUtils.onBadRequest(res, "Invalid Credentials");
  88. }
  89. }
  90. catch (err) {
  91. return app.routerUtils.onBadRequest(res, err);
  92. }
  93. app.routerUtils.jsonResponse(res, await accessListToJson(app, req));
  94. });
  95. app.router.post("/api/access/link", async (req, res) => { // /api/access/link, post: { linkIds: [string] (JSON) }
  96. app.routerUtils.onApiRequest(req, res);
  97. if (!req.post?.linkIds?.length)
  98. return app.routerUtils.httpResponse(res, 400, "Missing argument");
  99. try {
  100. for (let i of JSON.parse(req.post.linkIds)) {
  101. const access = await app.databaseHelper.findOne(AccessModel, { type: ACCESS_TYPE.link, typeData: i });
  102. if (access) {
  103. Security.addLinkToSession(req, access.id, i, access.typeLabel);
  104. if (access.accessTo == ACCESS_TO.admin)
  105. Security.setAdmin(req, true);
  106. }
  107. }
  108. }
  109. catch (err) {
  110. console.error(err);
  111. return app.routerUtils.onBadRequest(res);
  112. }
  113. app.routerUtils.jsonResponse(res, await accessListToJson(app, req));
  114. });
  115. app.router.del("/api/access/:id", async (req, res) => {
  116. app.routerUtils.onApiRequest(req, res);
  117. Security.removeFromSession(req, req.params.id);
  118. const access = await app.databaseHelper.fetch(AccessModel, { id: Object.keys(req.sessionObj.accessList).map(i => req.sessionObj.accessList[i]).filter(x => x.dbId).map(x => x.dbId), accessTo: ACCESS_TO.admin });
  119. const result = Security.setAdmin(req, !!(access?.length || 0));
  120. app.routerUtils.jsonResponse(res, result);
  121. });
  122. app.router.post("/api/access/totp/send", async(req, res) => {
  123. app.routerUtils.onApiRequest(req, res);
  124. if (!req.post?.email)
  125. return app.routerUtils.httpResponse(res, 400, "Missing argument");
  126. const access = await app.databaseHelper.findOne(AccessModel, { type: ACCESS_TYPE.email, typeData: req.post.email.trim() });
  127. if (access) {
  128. const code = Security.GenerateTotp(access.typeData);
  129. const codeHumanReadable = Array.from(code).map((x, idx) => x + (idx %3 === 2? ' ' : '')).join('').trim();
  130. MailService.sendMail(access.typeData, "Photochamber - Your code", require("../templates/authEmailTotp.js")({code: codeHumanReadable}));
  131. }
  132. app.routerUtils.jsonResponse(res, {});
  133. });
  134. app.router.post("/api/access/totp/validate", async(req, res) => {
  135. app.routerUtils.onApiRequest(req, res);
  136. if (!req.post?.email || !req.post?.code)
  137. return app.routerUtils.httpResponse(res, 400, "Missing argument");
  138. const access = await app.databaseHelper.fetch(AccessModel, { type: ACCESS_TYPE.email, typeData: req.post.email.trim() });
  139. if (access.length && Security.ValidateTotp(access[0].typeData, req.post.code.trim())) {
  140. Security.EmailLoginToSession(req, access);
  141. if (access.find(x => x.accessTo == ACCESS_TO.admin))
  142. Security.setAdmin(req, true);
  143. app.routerUtils.jsonResponse(res, await accessListToJson(app, req));
  144. } else {
  145. app.routerUtils.onBadRequest(res, "Invalid Email or Code");
  146. }
  147. });
  148. app.router.post("/api/accessAdmin/create", async (req, res) => {
  149. app.routerUtils.onApiRequest(req, res);
  150. if (!await req.sessionObj?.accessList?.isAdmin(app, req.sessionObj?.accessList) || !req.body)
  151. return app.routerUtils.onBadRequest(res);
  152. let access = new AccessModel();
  153. access.type = parseInt(req.body.typeId);
  154. access.typeData = req.body.typeData;
  155. access.typeLabel = req.body.typeLabel;
  156. access.accessTo = parseInt(req.body.accessToId);
  157. access.accessToData = req.body.accessToData;
  158. access.grant = parseInt(req.body.grant);
  159. try {
  160. await app.databaseHelper.insertOne(access);
  161. }
  162. catch (err) {
  163. console.error(err);
  164. return app.routerUtils.onBadRequest(res);
  165. }
  166. app.routerUtils.jsonResponse(res, accessToJson(access));
  167. });
  168. app.router.del("/api/accessAdmin/:id", async (req, res) => {
  169. app.routerUtils.onApiRequest(req, res);
  170. if (!await req.sessionObj?.accessList?.isAdmin(app, req.sessionObj?.accessList) || !req.params.id)
  171. return app.routerUtils.onBadRequest(res);
  172. app.databaseHelper.remove(AccessModel, { id: parseInt(req.params.id) });
  173. app.routerUtils.jsonResponse(res, {});
  174. });
  175. app.router.post("/api/accessAdmin/:id", async (req, res) => {
  176. app.routerUtils.onApiRequest(req, res);
  177. if (!await req.sessionObj?.accessList?.isAdmin(app, req.sessionObj?.accessList) || !req.params.id || !req.body)
  178. return app.routerUtils.onBadRequest(res);
  179. const access = (await app.databaseHelper.fetch(AccessModel, { id: parseInt(req.params.id) }))?.[0];
  180. if (!access)
  181. return app.routerUtils.onBadRequest(res);
  182. access.typeLabel = req.body.typeLabel;
  183. access.typeData = req.body.typeData;
  184. access.accessTo = parseInt(req.body.accessToId);
  185. access.accessToData = req.body.accessToData;
  186. access.grant = parseInt(req.body.grant);
  187. try {
  188. app.databaseHelper.upsertOne(access);
  189. }
  190. catch (err) {
  191. console.error(err);
  192. return app.routerUtils.onBadRequest(res);
  193. }
  194. app.routerUtils.jsonResponse(res, accessToJson(access));
  195. });
  196. app.router.get("/api/accessAdmin/list", async (req, res) => {
  197. app.routerUtils.onApiRequest(req, res);
  198. if (!await req.sessionObj?.accessList?.isAdmin(app, req.sessionObj?.accessList))
  199. return app.routerUtils.onBadRequest(res);
  200. app.routerUtils.jsonResponse(res, (await app.databaseHelper.fetch(AccessModel)).map(accessToJson));
  201. });
  202. app.router.post("/api/media/:id/tag/del/:tag", async (req, res) => {
  203. app.routerUtils.onApiRequest(req, res);
  204. if (!req.params.id ||!req.params.tag)
  205. return app.routerUtils.onBadRequest(res);
  206. let checksum = [ req.params.id ];
  207. if (req.params.id === "list") {
  208. if (!req.body?.['list[]'])
  209. return app.routerUtils.onBadRequest(res);
  210. checksum = req.body['list[]'];
  211. }
  212. let data = await MediaService.fetchMultiple(app, checksum, req.sessionObj?.accessList, 0);
  213. data = Object.keys(data).map(x => data[x]).filter(x => x.ACCESS_TYPE != ACCESS_GRANT.write);
  214. await Promise.all(data.map(x => MediaService.updateVersionInDb(app, x.fixedSum)));
  215. await app.databaseHelper.remove(MediaFileTagModel, { md5sum: data.map(x => x.fixedSum), tag: decodeURIComponent(req.params.tag), fromMeta: 0 });
  216. const allMedias = await MediaService.fetchMultiple(app, checksum, req.sessionObj?.accessList, 0);
  217. app.routerUtils.jsonResponse(res, Object.keys(allMedias).map(x => allMedias[x]).map(x => MediaToJson(x)));
  218. });
  219. app.router.put("/api/media/:id/tag", async (req, res) => {
  220. app.routerUtils.onApiRequest(req, res);
  221. const requestedTag = req.body?.tag;
  222. if (!req.params.id ||!requestedTag)
  223. return app.routerUtils.onBadRequest(res);
  224. let checksum = [ req.params.id ];
  225. if (req.params.id === "list") {
  226. if (!req.body?.['list[]'])
  227. return app.routerUtils.onBadRequest(res);
  228. checksum = req.body['list[]'];
  229. }
  230. let data = await MediaService.fetchMultiple(app, checksum, req.sessionObj?.accessList, 0);
  231. data = Object.keys(data)
  232. .map(x => data[x])
  233. .filter(x => {
  234. if (x.ACCESS_TYPE != ACCESS_GRANT.write)
  235. return true;
  236. for (let existingTag of [...x.tags, ...x.fixedTags]) {
  237. if (existingTag === requestedTag || existingTag.startsWith(`${requestedTag}/`)) {
  238. return true;
  239. }
  240. }
  241. });
  242. await Promise.all(data.map(x => MediaService.updateVersionInDb(app, x.fixedSum)));
  243. let tag = data.map(x => new MediaFileTagModel(x.fixedSum, requestedTag, false));
  244. try {
  245. await app.databaseHelper.insertMultipleSameTable(tag);
  246. }
  247. catch (err) {
  248. console.error(err);
  249. return app.routerUtils.onBadRequest(res);
  250. }
  251. const allMedias = await MediaService.fetchMultiple(app, checksum, req.sessionObj?.accessList, 0);
  252. app.routerUtils.jsonResponse(res, Object.keys(allMedias).map(x => allMedias[x]).map(x => MediaToJson(x)));
  253. });
  254. app.router.patch("/api/media/:id/meta/:key", async (req, res) => {
  255. app.routerUtils.onApiRequest(req, res);
  256. if (!req.params.id ||!req.params.key || !Number.isInteger(req.body?.value?.length))
  257. return app.routerUtils.onBadRequest(res);
  258. let checksum = [ req.params.id ];
  259. if (req.params.id === "list") {
  260. if (!req.body?.['list[]'])
  261. return app.routerUtils.onBadRequest(res);
  262. checksum = req.body['list[]'];
  263. }
  264. let data = await MediaService.fetchMultiple(app, checksum, req.sessionObj?.accessList, 0);
  265. data = Object.keys(data)
  266. .map(x => data[x])
  267. .filter(x => x.ACCESS_TYPE != ACCESS_GRANT.write);
  268. if (!(await MediaService.updateMeta(app, data.map(x => x.fixedSum), req.params.key, req.body.value)))
  269. return app.routerUtils.onBadRequest(res);
  270. const allMedias = await MediaService.fetchMultiple(app, checksum, req.sessionObj?.accessList, 0);
  271. app.routerUtils.jsonResponse(res, Object.keys(allMedias).map(x => allMedias[x]).map(x => MediaToJson(x)));
  272. });
  273. app.router.get("/api/media/list", async (req, res) => {
  274. app.routerUtils.onApiRequest(req, res);
  275. let first = undefined,
  276. last = undefined,
  277. maxVersion = undefined;
  278. if (req.body?.chronology !== undefined) {
  279. let range = await MediaService.getMediaRange(app);
  280. first = range.min;
  281. last = range.max;
  282. maxVersion = range.maxVersion;
  283. }
  284. let fromDate = parseInt(req.body?.from);
  285. let count = parseInt(req.body?.count);
  286. app.routerUtils.jsonResponse(res, {
  287. data: (await MediaService.fetchMediasWithAccess(
  288. app,
  289. isNaN(fromDate) ? 0 : fromDate,
  290. isNaN(count) ? 25 : Math.min(350, count),
  291. req.sessionObj?.accessList,
  292. req.body?.version || 0)).map(MediaToJson),
  293. first: first,
  294. last: last,
  295. maxVersion: maxVersion
  296. });
  297. });
  298. app.router.get("/api/media/sumlist", async (req, res) => {
  299. app.routerUtils.onApiRequest(req, res);
  300. app.routerUtils.jsonResponse(res, {
  301. data: await MediaService.fetchMediasSumWithAccess(
  302. app,
  303. req.sessionObj?.accessList)
  304. });
  305. });
  306. app.router.del("/api/media/:md5sum", async (req, res) => {
  307. app.routerUtils.onApiRequest(req, res);
  308. let data = MediaToJson(await MediaService.fetchOne(app, req.params.md5sum, req.sessionObj?.accessList, 0));
  309. if (!data)
  310. return app.routerUtils.onPageNotFound(res);
  311. await MediaService.removeMedia(app, data);
  312. app.routerUtils.jsonResponse(res, {});
  313. });
  314. app.router.get("/api/media/:md5sum", async (req, res) => {
  315. app.routerUtils.onApiRequest(req, res);
  316. let data = MediaToJson(await MediaService.fetchOne(app, req.params.md5sum, req.sessionObj?.accessList, 0));
  317. if (!data)
  318. return app.routerUtils.onPageNotFound(res);
  319. app.routerUtils.jsonResponse(res, data);
  320. });
  321. app.router.get("/api/media/thumbnail/:md5sum.jpg", async (req, res) => {
  322. app.routerUtils.onApiRequest(req, res);
  323. let data = await MediaService.fetchOne(app, req.params.md5sum, req.sessionObj?.accessList, 0);
  324. if (!data)
  325. return app.routerUtils.onPageNotFound(res);
  326. try {
  327. let thumbnail = null;
  328. req.body = req.body || {};
  329. req.body.w = parseInt(req.body.w || 0);
  330. req.body.h = parseInt(req.body.h || 0);
  331. req.body.q = parseInt(req.body.q || 6);
  332. try {
  333. thumbnail = await (await app.libraryManager.findMedia(data.path))?.createThumbnail(req.body.w, req.body.h, req.body.q);
  334. } catch (err) {
  335. return app.routerUtils.apiError(res);
  336. }
  337. if (!thumbnail)
  338. return app.routerUtils.onPageNotFound(res);
  339. res.setHeader("Content-Type", "image/jpeg");
  340. res.setHeader("Content-Length", fs.statSync(thumbnail.name)?.size || undefined);
  341. res.setHeader("Cache-Control", "private, max-age=2630000"); // 1 month cache
  342. let rd = fs.createReadStream(thumbnail.name);
  343. rd.once('end', () => thumbnail.removeCallback());
  344. rd.pipe(res);
  345. }
  346. catch (err) {
  347. console.error(err);
  348. app.routerUtils.onPageNotFound(res);
  349. }
  350. });
  351. app.router.get("/api/media/original/:md5sum", async (req, res) => {
  352. app.routerUtils.onApiRequest(req, res);
  353. let data = await MediaService.fetchOne(app, req.params.md5sum, req.sessionObj?.accessList, 0);
  354. if (!data)
  355. return app.routerUtils.onPageNotFound(res);
  356. const fileName = Path.basename(data.path);
  357. res.setHeader("Cache-Control", "private, max-age=2630000"); // 1 month cache
  358. if (data.accessType === ACCESS_GRANT.readNoMeta || req.body?.trim !== undefined) {
  359. console.log("remove meta");//-> trim metadata
  360. }
  361. res.setHeader("Content-Disposition", `attachment; filename="${fileName}"`);
  362. res.setHeader("Content-Type", mime.lookup(data.path));
  363. res.setHeader("Content-Length", fs.statSync(data.path)?.size || undefined);
  364. fs.createReadStream(data.path).pipe(res);
  365. });
  366. }};