api.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  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) {
  146. // when profile image
  147. return user.id === ownerId.toString();
  148. }
  149. // eslint-disable-next-line no-return-await
  150. return await Page.isAccessiblePageByViewer(attachment.page, user);
  151. }
  152. const actions = {};
  153. const api = {};
  154. actions.api = api;
  155. // api.download = async function(req, res) {
  156. // const id = req.params.id;
  157. // const attachment = await Attachment.findById(id);
  158. // return responseForAttachment(req, res, attachment, true);
  159. // };
  160. /**
  161. * @swagger
  162. *
  163. * /attachments.uploadProfileImage:
  164. * post:
  165. * tags: [Attachments]
  166. * operationId: uploadProfileImage
  167. * summary: /attachments.uploadProfileImage
  168. * description: Upload profile image
  169. * requestBody:
  170. * content:
  171. * "multipart/form-data":
  172. * schema:
  173. * properties:
  174. * file:
  175. * type: string
  176. * format: binary
  177. * description: attachment data
  178. * user:
  179. * type: string
  180. * description: user to set profile image
  181. * encoding:
  182. * path:
  183. * contentType: application/x-www-form-urlencoded
  184. * "*\/*":
  185. * schema:
  186. * properties:
  187. * file:
  188. * type: string
  189. * format: binary
  190. * description: attachment data
  191. * user:
  192. * type: string
  193. * description: user to set profile
  194. * encoding:
  195. * path:
  196. * contentType: application/x-www-form-urlencoded
  197. * responses:
  198. * 200:
  199. * description: Succeeded to add attachment.
  200. * content:
  201. * application/json:
  202. * schema:
  203. * allOf:
  204. * - $ref: '#/components/schemas/ApiResponseSuccess'
  205. * - type: object
  206. * properties:
  207. * attachment:
  208. * $ref: '#/components/schemas/AttachmentProfile'
  209. * description: The uploaded profile image attachment
  210. * 403:
  211. * $ref: '#/components/responses/Forbidden'
  212. * 500:
  213. * $ref: '#/components/responses/InternalServerError'
  214. */
  215. /**
  216. * @api {post} /attachments.uploadProfileImage Add attachment for profile image
  217. * @apiName UploadProfileImage
  218. * @apiGroup Attachment
  219. *
  220. * @apiParam {File} file
  221. */
  222. api.uploadProfileImage = async (req, res) => {
  223. // check params
  224. if (req.file == null) {
  225. return res.json(ApiResponse.error('File error.'));
  226. }
  227. if (!req.user) {
  228. return res.json(ApiResponse.error('param "user" must be set.'));
  229. }
  230. const file = req.file;
  231. // Validate file type
  232. const { isValid, error } = validateImageContentType(file.mimetype);
  233. if (!isValid) {
  234. return res.json(ApiResponse.error(error));
  235. }
  236. let attachment;
  237. try {
  238. const user = await User.findById(req.user._id);
  239. await user.deleteImage();
  240. attachment = await attachmentService.createAttachment(
  241. file,
  242. req.user,
  243. null,
  244. AttachmentType.PROFILE_IMAGE,
  245. );
  246. await user.updateImage(attachment);
  247. } catch (err) {
  248. logger.error(err);
  249. return res.json(ApiResponse.error(err.message));
  250. }
  251. const result = {
  252. attachment: attachment.toObject({ virtuals: true }),
  253. };
  254. return res.json(ApiResponse.success(result));
  255. };
  256. /**
  257. * @swagger
  258. *
  259. * /attachments.remove:
  260. * post:
  261. * tags: [Attachments]
  262. * operationId: removeAttachment
  263. * summary: /attachments.remove
  264. * description: Remove attachment
  265. * requestBody:
  266. * content:
  267. * application/json:
  268. * schema:
  269. * properties:
  270. * attachment_id:
  271. * $ref: '#/components/schemas/ObjectId'
  272. * required:
  273. * - attachment_id
  274. * responses:
  275. * 200:
  276. * description: Succeeded to remove attachment.
  277. * content:
  278. * application/json:
  279. * schema:
  280. * $ref: '#/components/schemas/ApiResponseSuccess'
  281. * 403:
  282. * $ref: '#/components/responses/Forbidden'
  283. * 500:
  284. * $ref: '#/components/responses/InternalServerError'
  285. */
  286. /**
  287. * @api {post} /attachments.remove Remove attachments
  288. * @apiName RemoveAttachments
  289. * @apiGroup Attachment
  290. *
  291. * @apiParam {String} attachment_id
  292. */
  293. api.remove = async (req, res) => {
  294. const id = req.body.attachment_id;
  295. const attachment = await Attachment.findOne({ _id: { $eq: id } });
  296. if (attachment == null) {
  297. return res.json(ApiResponse.error('attachment not found'));
  298. }
  299. const isDeletable = await isDeletableByUser(req.user, attachment);
  300. if (!isDeletable) {
  301. return res.json(
  302. ApiResponse.error(
  303. `Forbidden to remove the attachment '${attachment.id}'`,
  304. ),
  305. );
  306. }
  307. try {
  308. await attachmentService.removeAttachment(attachment);
  309. } catch (err) {
  310. logger.error(err);
  311. return res
  312. .status(500)
  313. .json(ApiResponse.error('Error while deleting file'));
  314. }
  315. activityEvent.emit('update', res.locals.activity._id, {
  316. action: SupportedAction.ACTION_ATTACHMENT_REMOVE,
  317. });
  318. return res.json(ApiResponse.success({}));
  319. };
  320. /**
  321. * @swagger
  322. *
  323. * /attachments.removeProfileImage:
  324. * post:
  325. * tags: [Attachments]
  326. * operationId: removeProfileImage
  327. * summary: /attachments.removeProfileImage
  328. * description: Remove profile image
  329. * requestBody:
  330. * content:
  331. * application/json:
  332. * schema:
  333. * properties:
  334. * user:
  335. * type: string
  336. * description: user to remove profile image
  337. * responses:
  338. * 200:
  339. * description: Succeeded to add attachment.
  340. * content:
  341. * application/json:
  342. * schema:
  343. * $ref: '#/components/schemas/ApiResponseSuccess'
  344. * 403:
  345. * $ref: '#/components/responses/Forbidden'
  346. * 500:
  347. * $ref: '#/components/responses/InternalServerError'
  348. */
  349. /**
  350. * @api {post} /attachments.removeProfileImage Remove profile image attachments
  351. * @apiGroup Attachment
  352. * @apiParam {String} attachment_id
  353. */
  354. api.removeProfileImage = async (req, res) => {
  355. const user = req.user;
  356. const attachment = await Attachment.findById(user.imageAttachment);
  357. if (attachment == null) {
  358. return res.json(ApiResponse.error('attachment not found'));
  359. }
  360. const isDeletable = await isDeletableByUser(user, attachment);
  361. if (!isDeletable) {
  362. return res.json(
  363. ApiResponse.error(
  364. `Forbidden to remove the attachment '${attachment.id}'`,
  365. ),
  366. );
  367. }
  368. try {
  369. await user.deleteImage();
  370. } catch (err) {
  371. logger.error(err);
  372. return res
  373. .status(500)
  374. .json(ApiResponse.error('Error while deleting image'));
  375. }
  376. return res.json(ApiResponse.success({}));
  377. };
  378. return actions;
  379. };