api.js 12 KB

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