api.js 12 KB

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