api.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. import { SupportedAction } from '~/interfaces/activity';
  2. import { AttachmentType } from '~/server/interfaces/attachment';
  3. import loggerFactory from '~/utils/logger';
  4. import { Attachment } from '../../models/attachment';
  5. /* eslint-disable no-use-before-define */
  6. const logger = loggerFactory('growi:routes:attachment');
  7. const ApiResponse = require('../../util/apiResponse');
  8. /**
  9. * @swagger
  10. * tags:
  11. * name: Attachments
  12. */
  13. /**
  14. * @swagger
  15. *
  16. * components:
  17. * schemas:
  18. * Attachment:
  19. * description: Attachment
  20. * type: object
  21. * properties:
  22. * _id:
  23. * type: string
  24. * description: attachment ID
  25. * example: 5e0734e072560e001761fa67
  26. * __v:
  27. * type: number
  28. * description: attachment version
  29. * example: 0
  30. * fileFormat:
  31. * type: string
  32. * description: file format in MIME
  33. * example: text/plain
  34. * fileName:
  35. * type: string
  36. * description: file name
  37. * example: 601b7c59d43a042c0117e08dd37aad0aimage.txt
  38. * originalName:
  39. * type: string
  40. * description: original file name
  41. * example: file.txt
  42. * creator:
  43. * $ref: '#/components/schemas/User'
  44. * page:
  45. * type: string
  46. * description: page ID attached at
  47. * example: 5e07345972560e001761fa63
  48. * createdAt:
  49. * type: string
  50. * description: date created at
  51. * example: 2010-01-01T00:00:00.000Z
  52. * fileSize:
  53. * type: number
  54. * description: file size
  55. * example: 3494332
  56. * url:
  57. * type: string
  58. * description: attachment URL
  59. * example: http://localhost/files/5e0734e072560e001761fa67
  60. * filePathProxied:
  61. * type: string
  62. * description: file path proxied
  63. * example: "/attachment/5e0734e072560e001761fa67"
  64. * downloadPathProxied:
  65. * type: string
  66. * description: download path proxied
  67. * example: "/download/5e0734e072560e001761fa67"
  68. */
  69. /**
  70. * @swagger
  71. *
  72. * components:
  73. * schemas:
  74. * AttachmentProfile:
  75. * description: Attachment
  76. * type: object
  77. * properties:
  78. * id:
  79. * type: string
  80. * description: attachment ID
  81. * example: 5e0734e072560e001761fa67
  82. * _id:
  83. * type: string
  84. * description: attachment ID
  85. * example: 5e0734e072560e001761fa67
  86. * __v:
  87. * type: number
  88. * description: attachment version
  89. * example: 0
  90. * fileFormat:
  91. * type: string
  92. * description: file format in MIME
  93. * example: image/png
  94. * fileName:
  95. * type: string
  96. * description: file name
  97. * example: 601b7c59d43a042c0117e08dd37aad0a.png
  98. * originalName:
  99. * type: string
  100. * description: original file name
  101. * example: profile.png
  102. * creator:
  103. * $ref: '#/components/schemas/User/properties/_id'
  104. * page:
  105. * type: string
  106. * description: page ID attached at
  107. * example: null
  108. * createdAt:
  109. * type: string
  110. * description: date created at
  111. * example: 2010-01-01T00:00:00.000Z
  112. * fileSize:
  113. * type: number
  114. * description: file size
  115. * example: 3494332
  116. * filePathProxied:
  117. * type: string
  118. * description: file path proxied
  119. * example: "/attachment/5e0734e072560e001761fa67"
  120. * downloadPathProxied:
  121. * type: string
  122. * description: download path proxied
  123. * example: "/download/5e0734e072560e001761fa67"
  124. */
  125. export const routesFactory = (crowi) => {
  126. const Page = crowi.model('Page');
  127. const User = crowi.model('User');
  128. const GlobalNotificationSetting = crowi.model('GlobalNotificationSetting');
  129. const { attachmentService, globalNotificationService } = crowi;
  130. const activityEvent = crowi.event('activity');
  131. /**
  132. * Check the user is accessible to the related page
  133. *
  134. * @param {User} user
  135. * @param {Attachment} attachment
  136. */
  137. async function isDeletableByUser(user, attachment) {
  138. // deletable if creator is null
  139. if (attachment.creator == null) {
  140. return true;
  141. }
  142. const ownerId = attachment.creator._id || attachment.creator;
  143. if (attachment.page == null) { // when profile image
  144. return user.id === ownerId.toString();
  145. }
  146. // eslint-disable-next-line no-return-await
  147. return await Page.isAccessiblePageByViewer(attachment.page, user);
  148. }
  149. const actions = {};
  150. const api = {};
  151. actions.api = api;
  152. // api.download = async function(req, res) {
  153. // const id = req.params.id;
  154. // const attachment = await Attachment.findById(id);
  155. // return responseForAttachment(req, res, attachment, true);
  156. // };
  157. /**
  158. * @swagger
  159. *
  160. * /attachments.uploadProfileImage:
  161. * post:
  162. * tags: [Attachments]
  163. * operationId: uploadProfileImage
  164. * summary: /attachments.uploadProfileImage
  165. * description: Upload profile image
  166. * requestBody:
  167. * content:
  168. * "multipart/form-data":
  169. * schema:
  170. * properties:
  171. * file:
  172. * type: string
  173. * format: binary
  174. * description: attachment data
  175. * user:
  176. * type: string
  177. * description: user to set profile image
  178. * encoding:
  179. * path:
  180. * contentType: application/x-www-form-urlencoded
  181. * "*\/*":
  182. * schema:
  183. * properties:
  184. * file:
  185. * type: string
  186. * format: binary
  187. * description: attachment data
  188. * user:
  189. * type: string
  190. * description: user to set profile
  191. * encoding:
  192. * path:
  193. * contentType: application/x-www-form-urlencoded
  194. * responses:
  195. * 200:
  196. * description: Succeeded to add attachment.
  197. * content:
  198. * application/json:
  199. * schema:
  200. * properties:
  201. * ok:
  202. * $ref: '#/components/schemas/V1Response/properties/ok'
  203. * attachment:
  204. * $ref: '#/components/schemas/AttachmentProfile'
  205. * 403:
  206. * $ref: '#/components/responses/403'
  207. * 500:
  208. * $ref: '#/components/responses/500'
  209. */
  210. /**
  211. * @api {post} /attachments.uploadProfileImage Add attachment for profile image
  212. * @apiName UploadProfileImage
  213. * @apiGroup Attachment
  214. *
  215. * @apiParam {File} file
  216. */
  217. api.uploadProfileImage = async function(req, res) {
  218. // check params
  219. if (req.file == null) {
  220. return res.json(ApiResponse.error('File error.'));
  221. }
  222. if (!req.user) {
  223. return res.json(ApiResponse.error('param "user" must be set.'));
  224. }
  225. const file = req.file;
  226. // check type
  227. const acceptableFileType = /image\/.+/;
  228. if (!file.mimetype.match(acceptableFileType)) {
  229. return res.json(ApiResponse.error('File type error. Only image files is allowed to set as user picture.'));
  230. }
  231. let attachment;
  232. try {
  233. req.user.deleteImage();
  234. attachment = await attachmentService.createAttachment(file, req.user, null, AttachmentType.PROFILE_IMAGE);
  235. await req.user.updateImage(attachment);
  236. }
  237. catch (err) {
  238. logger.error(err);
  239. return res.json(ApiResponse.error(err.message));
  240. }
  241. const result = {
  242. attachment: attachment.toObject({ virtuals: true }),
  243. };
  244. return res.json(ApiResponse.success(result));
  245. };
  246. /**
  247. * @swagger
  248. *
  249. * /attachments.remove:
  250. * post:
  251. * tags: [Attachments, CrowiCompatibles]
  252. * operationId: removeAttachment
  253. * summary: /attachments.remove
  254. * description: Remove attachment
  255. * requestBody:
  256. * content:
  257. * application/json:
  258. * schema:
  259. * properties:
  260. * attachment_id:
  261. * $ref: '#/components/schemas/Attachment/properties/_id'
  262. * required:
  263. * - attachment_id
  264. * responses:
  265. * 200:
  266. * description: Succeeded to remove attachment.
  267. * content:
  268. * application/json:
  269. * schema:
  270. * properties:
  271. * ok:
  272. * $ref: '#/components/schemas/V1Response/properties/ok'
  273. * 403:
  274. * $ref: '#/components/responses/403'
  275. * 500:
  276. * $ref: '#/components/responses/500'
  277. */
  278. /**
  279. * @api {post} /attachments.remove Remove attachments
  280. * @apiName RemoveAttachments
  281. * @apiGroup Attachment
  282. *
  283. * @apiParam {String} attachment_id
  284. */
  285. api.remove = async function(req, res) {
  286. const id = req.body.attachment_id;
  287. const attachment = await Attachment.findById(id);
  288. if (attachment == null) {
  289. return res.json(ApiResponse.error('attachment not found'));
  290. }
  291. const isDeletable = await isDeletableByUser(req.user, attachment);
  292. if (!isDeletable) {
  293. return res.json(ApiResponse.error(`Forbidden to remove the attachment '${attachment.id}'`));
  294. }
  295. try {
  296. await attachmentService.removeAttachment(attachment);
  297. }
  298. catch (err) {
  299. logger.error(err);
  300. return res.status(500).json(ApiResponse.error('Error while deleting file'));
  301. }
  302. activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ATTACHMENT_REMOVE });
  303. return res.json(ApiResponse.success({}));
  304. };
  305. /**
  306. * @swagger
  307. *
  308. * /attachments.removeProfileImage:
  309. * post:
  310. * tags: [Attachments]
  311. * operationId: removeProfileImage
  312. * summary: /attachments.removeProfileImage
  313. * description: Remove profile image
  314. * requestBody:
  315. * content:
  316. * application/json:
  317. * schema:
  318. * properties:
  319. * user:
  320. * type: string
  321. * description: user to remove profile image
  322. * responses:
  323. * 200:
  324. * description: Succeeded to add attachment.
  325. * content:
  326. * application/json:
  327. * schema:
  328. * properties:
  329. * ok:
  330. * $ref: '#/components/schemas/V1Response/properties/ok'
  331. * 403:
  332. * $ref: '#/components/responses/403'
  333. * 500:
  334. * $ref: '#/components/responses/500'
  335. */
  336. /**
  337. * @api {post} /attachments.removeProfileImage Remove profile image attachments
  338. * @apiGroup Attachment
  339. * @apiParam {String} attachment_id
  340. */
  341. api.removeProfileImage = async function(req, res) {
  342. const user = req.user;
  343. const attachment = await Attachment.findById(user.imageAttachment);
  344. if (attachment == null) {
  345. return res.json(ApiResponse.error('attachment not found'));
  346. }
  347. const isDeletable = await isDeletableByUser(user, attachment);
  348. if (!isDeletable) {
  349. return res.json(ApiResponse.error(`Forbidden to remove the attachment '${attachment.id}'`));
  350. }
  351. try {
  352. await user.deleteImage();
  353. }
  354. catch (err) {
  355. logger.error(err);
  356. return res.status(500).json(ApiResponse.error('Error while deleting image'));
  357. }
  358. return res.json(ApiResponse.success({}));
  359. };
  360. return actions;
  361. };