personal-setting.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651
  1. import { body } from 'express-validator';
  2. import { listLocaleIds } from '~/utils/locale-utils';
  3. import loggerFactory from '~/utils/logger';
  4. import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
  5. import EditorSettings from '../../models/editor-settings';
  6. import InAppNotificationSettings from '../../models/in-app-notification-settings';
  7. const logger = loggerFactory('growi:routes:apiv3:personal-setting');
  8. const express = require('express');
  9. const passport = require('passport');
  10. const router = express.Router();
  11. /**
  12. * @swagger
  13. * tags:
  14. * name: PersonalSetting
  15. */
  16. /**
  17. * @swagger
  18. *
  19. * components:
  20. * schemas:
  21. * PersonalSettings:
  22. * description: personal settings
  23. * type: object
  24. * properties:
  25. * name:
  26. * type: string
  27. * email:
  28. * type: string
  29. * lang:
  30. * type: string
  31. * isEmailPublished:
  32. * type: boolean
  33. * Passwords:
  34. * description: passwords for update
  35. * type: object
  36. * properties:
  37. * oldPassword:
  38. * type: string
  39. * newPassword:
  40. * type: string
  41. * newPasswordConfirm:
  42. * type: string
  43. * AssociateUser:
  44. * description: Ldap account for associate
  45. * type: object
  46. * properties:
  47. * username:
  48. * type: string
  49. * password:
  50. * type: string
  51. * DisassociateUser:
  52. * description: Ldap account for disassociate
  53. * type: object
  54. * properties:
  55. * providerType:
  56. * type: string
  57. * accountId:
  58. * type: string
  59. */
  60. module.exports = (crowi) => {
  61. const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
  62. const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
  63. const csrf = require('../../middlewares/csrf')(crowi);
  64. const { User, ExternalAccount } = crowi.models;
  65. const minPasswordLength = crowi.configManager.getConfig('crowi', 'app:minPasswordLength');
  66. const validator = {
  67. personal: [
  68. body('name').isString().not().isEmpty(),
  69. body('email')
  70. .isEmail()
  71. .custom((email) => {
  72. if (!User.isEmailValid(email)) throw new Error('email is not included in whitelist');
  73. return true;
  74. }),
  75. body('lang').isString().isIn(listLocaleIds()),
  76. body('isEmailPublished').isBoolean(),
  77. body('slackMemberId').optional().isString(),
  78. ],
  79. imageType: [
  80. body('isGravatarEnabled').isBoolean(),
  81. ],
  82. password: [
  83. body('oldPassword').isString(),
  84. body('newPassword').isString().not().isEmpty()
  85. .isLength({ min: minPasswordLength })
  86. .withMessage(`password must be at least ${minPasswordLength} characters long`),
  87. body('newPasswordConfirm').isString().not().isEmpty()
  88. .custom((value, { req }) => {
  89. return (value === req.body.newPassword);
  90. }),
  91. ],
  92. associateLdap: [
  93. body('username').isString().not().isEmpty(),
  94. body('password').isString().not().isEmpty(),
  95. ],
  96. disassociateLdap: [
  97. body('providerType').isString().not().isEmpty(),
  98. body('accountId').isString().not().isEmpty(),
  99. ],
  100. editorSettings: [
  101. body('theme').optional().isString(),
  102. body('keymapMode').optional().isString(),
  103. body('styleActiveLine').optional().isBoolean(),
  104. body('renderMathJaxInRealtime').optional().isBoolean(),
  105. body('renderDrawioInRealtime').optional().isBoolean(),
  106. body('autoFormatMarkdownTable').optional().isBoolean(),
  107. body('textlintSettings.neverAskBeforeDownloadLargeFiles').optional().isBoolean(),
  108. body('textlintSettings.textlintRules.*.name').optional().isString(),
  109. body('textlintSettings.textlintRules.*.options').optional(),
  110. body('textlintSettings.textlintRules.*.isEnabled').optional().isBoolean(),
  111. ],
  112. inAppNotificationSettings: [
  113. body('defaultSubscribeRules.*.name').isString(),
  114. body('defaultSubscribeRules.*.isEnabled').optional().isBoolean(),
  115. ],
  116. };
  117. /**
  118. * @swagger
  119. *
  120. * /personal-setting:
  121. * get:
  122. * tags: [PersonalSetting]
  123. * operationId: getPersonalSetting
  124. * summary: /personal-setting
  125. * description: Get personal parameters
  126. * responses:
  127. * 200:
  128. * description: params of personal
  129. * content:
  130. * application/json:
  131. * schema:
  132. * properties:
  133. * currentUser:
  134. * type: object
  135. * description: personal params
  136. */
  137. router.get('/', accessTokenParser, loginRequiredStrictly, async(req, res) => {
  138. const { username } = req.user;
  139. try {
  140. const user = await User.findUserByUsername(username);
  141. // return email and apiToken
  142. const { email, apiToken } = user;
  143. const currentUser = user.toObject();
  144. currentUser.email = email;
  145. currentUser.apiToken = apiToken;
  146. return res.apiv3({ currentUser });
  147. }
  148. catch (err) {
  149. logger.error(err);
  150. return res.apiv3Err('update-personal-settings-failed');
  151. }
  152. });
  153. /**
  154. * @swagger
  155. *
  156. * /personal-setting/is-password-set:
  157. * get:
  158. * tags: [PersonalSetting]
  159. * operationId: getIsPasswordSet
  160. * summary: /personal-setting
  161. * description: Get whether a password has been set
  162. * responses:
  163. * 200:
  164. * description: Whether a password has been set
  165. * content:
  166. * application/json:
  167. * schema:
  168. * properties:
  169. * isPasswordSet:
  170. * type: boolean
  171. */
  172. router.get('/is-password-set', accessTokenParser, loginRequiredStrictly, async(req, res) => {
  173. const { username } = req.user;
  174. try {
  175. const user = await User.findUserByUsername(username);
  176. const isPasswordSet = user.isPasswordSet();
  177. const minPasswordLength = crowi.configManager.getConfig('crowi', 'app:minPasswordLength');
  178. return res.apiv3({ isPasswordSet, minPasswordLength });
  179. }
  180. catch (err) {
  181. logger.error(err);
  182. return res.apiv3Err('fail-to-get-whether-password-is-set');
  183. }
  184. });
  185. /**
  186. * @swagger
  187. *
  188. * /personal-setting:
  189. * put:
  190. * tags: [PersonalSetting]
  191. * operationId: updatePersonalSetting
  192. * summary: /personal-setting
  193. * description: Update personal setting
  194. * requestBody:
  195. * required: true
  196. * content:
  197. * application/json:
  198. * schema:
  199. * $ref: '#/components/schemas/PersonalSettings'
  200. * responses:
  201. * 200:
  202. * description: params of personal
  203. * content:
  204. * application/json:
  205. * schema:
  206. * properties:
  207. * currentUser:
  208. * type: object
  209. * description: personal params
  210. */
  211. router.put('/', accessTokenParser, loginRequiredStrictly, csrf, validator.personal, apiV3FormValidator, async(req, res) => {
  212. try {
  213. const user = await User.findOne({ _id: req.user.id });
  214. user.name = req.body.name;
  215. user.email = req.body.email;
  216. user.lang = req.body.lang;
  217. user.isEmailPublished = req.body.isEmailPublished;
  218. user.slackMemberId = req.body.slackMemberId;
  219. const updatedUser = await user.save();
  220. req.i18n.changeLanguage(req.body.lang);
  221. return res.apiv3({ updatedUser });
  222. }
  223. catch (err) {
  224. logger.error(err);
  225. return res.apiv3Err('update-personal-settings-failed');
  226. }
  227. });
  228. /**
  229. * @swagger
  230. *
  231. * /personal-setting/image-type:
  232. * put:
  233. * tags: [PersonalSetting]
  234. * operationId: putUserImageType
  235. * summary: /personal-setting/image-type
  236. * description: Update user image type
  237. * responses:
  238. * 200:
  239. * description: succeded to update user image type
  240. * content:
  241. * application/json:
  242. * schema:
  243. * properties:
  244. * userData:
  245. * type: object
  246. * description: user data
  247. */
  248. router.put('/image-type', accessTokenParser, loginRequiredStrictly, csrf, validator.imageType, apiV3FormValidator, async(req, res) => {
  249. const { isGravatarEnabled } = req.body;
  250. try {
  251. const userData = await req.user.updateIsGravatarEnabled(isGravatarEnabled);
  252. return res.apiv3({ userData });
  253. }
  254. catch (err) {
  255. logger.error(err);
  256. return res.apiv3Err('update-personal-settings-failed');
  257. }
  258. });
  259. /**
  260. * @swagger
  261. *
  262. * /personal-setting/external-accounts:
  263. * get:
  264. * tags: [PersonalSetting]
  265. * operationId: getExternalAccounts
  266. * summary: /personal-setting/external-accounts
  267. * description: Get external accounts that linked current user
  268. * responses:
  269. * 200:
  270. * description: external accounts
  271. * content:
  272. * application/json:
  273. * schema:
  274. * properties:
  275. * externalAccounts:
  276. * type: object
  277. * description: array of external accounts
  278. */
  279. router.get('/external-accounts', accessTokenParser, loginRequiredStrictly, async(req, res) => {
  280. const userData = req.user;
  281. try {
  282. const externalAccounts = await ExternalAccount.find({ user: userData });
  283. return res.apiv3({ externalAccounts });
  284. }
  285. catch (err) {
  286. logger.error(err);
  287. return res.apiv3Err('get-external-accounts-failed');
  288. }
  289. });
  290. /**
  291. * @swagger
  292. *
  293. * /personal-setting/password:
  294. * put:
  295. * tags: [PersonalSetting]
  296. * operationId: putUserPassword
  297. * summary: /personal-setting/password
  298. * description: Update user password
  299. * requestBody:
  300. * required: true
  301. * content:
  302. * application/json:
  303. * schema:
  304. * $ref: '#/components/schemas/Passwords'
  305. * responses:
  306. * 200:
  307. * description: user password
  308. * content:
  309. * application/json:
  310. * schema:
  311. * properties:
  312. * userData:
  313. * type: object
  314. * description: user data updated
  315. */
  316. router.put('/password', accessTokenParser, loginRequiredStrictly, csrf, validator.password, apiV3FormValidator, async(req, res) => {
  317. const { body, user } = req;
  318. const { oldPassword, newPassword } = body;
  319. if (user.isPasswordSet() && !user.isPasswordValid(oldPassword)) {
  320. return res.apiv3Err('wrong-current-password', 400);
  321. }
  322. try {
  323. const userData = await user.updatePassword(newPassword);
  324. return res.apiv3({ userData });
  325. }
  326. catch (err) {
  327. logger.error(err);
  328. return res.apiv3Err('update-password-failed');
  329. }
  330. });
  331. /**
  332. * @swagger
  333. *
  334. * /personal-setting/api-token:
  335. * put:
  336. * tags: [PersonalSetting]
  337. * operationId: putUserApiToken
  338. * summary: /personal-setting/api-token
  339. * description: Update user api token
  340. * responses:
  341. * 200:
  342. * description: succeded to update user api token
  343. * content:
  344. * application/json:
  345. * schema:
  346. * properties:
  347. * userData:
  348. * type: object
  349. * description: user data
  350. */
  351. router.put('/api-token', loginRequiredStrictly, csrf, async(req, res) => {
  352. const { user } = req;
  353. try {
  354. const userData = await user.updateApiToken();
  355. return res.apiv3({ userData });
  356. }
  357. catch (err) {
  358. logger.error(err);
  359. return res.apiv3Err('update-api-token-failed');
  360. }
  361. });
  362. /**
  363. * @swagger
  364. *
  365. * /personal-setting/associate-ldap:
  366. * put:
  367. * tags: [PersonalSetting]
  368. * operationId: associateLdapAccount
  369. * summary: /personal-setting/associate-ldap
  370. * description: associate Ldap account
  371. * requestBody:
  372. * required: true
  373. * content:
  374. * application/json:
  375. * schema:
  376. * $ref: '#/components/schemas/AssociateUser'
  377. * responses:
  378. * 200:
  379. * description: succeded to associate Ldap account
  380. * content:
  381. * application/json:
  382. * schema:
  383. * properties:
  384. * associateUser:
  385. * type: object
  386. * description: Ldap account associate to me
  387. */
  388. router.put('/associate-ldap', accessTokenParser, loginRequiredStrictly, csrf, validator.associateLdap, apiV3FormValidator, async(req, res) => {
  389. const { passportService } = crowi;
  390. const { user, body } = req;
  391. const { username } = body;
  392. if (!passportService.isLdapStrategySetup) {
  393. logger.error('LdapStrategy has not been set up');
  394. return res.apiv3Err('associate-ldap-account-failed', 405);
  395. }
  396. try {
  397. await passport.authenticate('ldapauth');
  398. const associateUser = await ExternalAccount.associate('ldap', username, user);
  399. return res.apiv3({ associateUser });
  400. }
  401. catch (err) {
  402. logger.error(err);
  403. return res.apiv3Err('associate-ldap-account-failed');
  404. }
  405. });
  406. /**
  407. * @swagger
  408. *
  409. * /personal-setting/disassociate-ldap:
  410. * put:
  411. * tags: [PersonalSetting]
  412. * operationId: disassociateLdapAccount
  413. * summary: /personal-setting/disassociate-ldap
  414. * description: disassociate Ldap account
  415. * requestBody:
  416. * required: true
  417. * content:
  418. * application/json:
  419. * schema:
  420. * $ref: '#/components/schemas/DisassociateUser'
  421. * responses:
  422. * 200:
  423. * description: succeded to disassociate Ldap account
  424. * content:
  425. * application/json:
  426. * schema:
  427. * properties:
  428. * disassociateUser:
  429. * type: object
  430. * description: Ldap account disassociate to me
  431. */
  432. router.put('/disassociate-ldap', accessTokenParser, loginRequiredStrictly, csrf, validator.disassociateLdap, apiV3FormValidator, async(req, res) => {
  433. const { user, body } = req;
  434. const { providerType, accountId } = body;
  435. try {
  436. const count = await ExternalAccount.count({ user });
  437. // make sure password set or this user has two or more ExternalAccounts
  438. if (user.password == null && count <= 1) {
  439. return res.apiv3Err('disassociate-ldap-account-failed');
  440. }
  441. const disassociateUser = await ExternalAccount.findOneAndRemove({ providerType, accountId, user });
  442. return res.apiv3({ disassociateUser });
  443. }
  444. catch (err) {
  445. logger.error(err);
  446. return res.apiv3Err('disassociate-ldap-account-failed');
  447. }
  448. });
  449. /**
  450. * @swagger
  451. *
  452. * /personal-setting/editor-settings:
  453. * put:
  454. * tags: [EditorSetting]
  455. * operationId: putEditorSettings
  456. * summary: /editor-setting
  457. * description: Put editor preferences
  458. * responses:
  459. * 200:
  460. * description: params of editor settings
  461. * content:
  462. * application/json:
  463. * schema:
  464. * properties:
  465. * currentUser:
  466. * type: object
  467. * description: editor settings
  468. */
  469. router.put('/editor-settings', accessTokenParser, loginRequiredStrictly, csrf, validator.editorSettings, apiV3FormValidator, async(req, res) => {
  470. const query = { userId: req.user.id };
  471. const { body } = req;
  472. const {
  473. theme, keymapMode, styleActiveLine, renderMathJaxInRealtime, renderDrawioInRealtime, autoFormatMarkdownTable,
  474. textlintSettings,
  475. } = body;
  476. const document = {
  477. theme, keymapMode, styleActiveLine, renderMathJaxInRealtime, renderDrawioInRealtime, autoFormatMarkdownTable,
  478. };
  479. if (textlintSettings != null) {
  480. if (textlintSettings.neverAskBeforeDownloadLargeFiles != null) {
  481. Object.assign(document, { 'textlintSettings.neverAskBeforeDownloadLargeFiles': textlintSettings.neverAskBeforeDownloadLargeFiles });
  482. }
  483. if (textlintSettings.textlintRules != null) {
  484. Object.assign(document, { 'textlintSettings.textlintRules': textlintSettings.textlintRules });
  485. }
  486. }
  487. // Insert if document does not exist, and return new values
  488. // See: https://mongoosejs.com/docs/api.html#model_Model.findOneAndUpdate
  489. const options = { upsert: true, new: true };
  490. try {
  491. const response = await EditorSettings.findOneAndUpdate(query, { $set: document }, options);
  492. return res.apiv3(response);
  493. }
  494. catch (err) {
  495. logger.error(err);
  496. return res.apiv3Err('updating-editor-settings-failed');
  497. }
  498. });
  499. /**
  500. * @swagger
  501. *
  502. * /personal-setting/editor-settings:
  503. * get:
  504. * tags: [EditorSetting]
  505. * operationId: getEditorSettings
  506. * summary: /editor-setting
  507. * description: Get editor preferences
  508. * responses:
  509. * 200:
  510. * description: params of editor settings
  511. * content:
  512. * application/json:
  513. * schema:
  514. * properties:
  515. * currentUser:
  516. * type: object
  517. * description: editor settings
  518. */
  519. router.get('/editor-settings', accessTokenParser, loginRequiredStrictly, async(req, res) => {
  520. try {
  521. const query = { userId: req.user.id };
  522. const editorSettings = await EditorSettings.findOne(query) ?? new EditorSettings();
  523. return res.apiv3(editorSettings);
  524. }
  525. catch (err) {
  526. logger.error(err);
  527. return res.apiv3Err('getting-editor-settings-failed');
  528. }
  529. });
  530. /**
  531. * @swagger
  532. *
  533. * /personal-setting/in-app-notification-settings:
  534. * put:
  535. * tags: [in-app-notification-settings]
  536. * operationId: putInAppNotificationSettings
  537. * summary: personal-setting/in-app-notification-settings
  538. * description: Put InAppNotificationSettings
  539. * responses:
  540. * 200:
  541. * description: params of InAppNotificationSettings
  542. * content:
  543. * application/json:
  544. * schema:
  545. * properties:
  546. * currentUser:
  547. * type: object
  548. * description: in-app-notification-settings
  549. */
  550. // eslint-disable-next-line max-len
  551. router.put('/in-app-notification-settings', accessTokenParser, loginRequiredStrictly, csrf, validator.inAppNotificationSettings, apiV3FormValidator, async(req, res) => {
  552. const query = { userId: req.user.id };
  553. const subscribeRules = req.body.subscribeRules;
  554. if (subscribeRules == null) {
  555. return res.apiv3Err('no-rules-found');
  556. }
  557. const options = { upsert: true, new: true, runValidators: true };
  558. try {
  559. const response = await InAppNotificationSettings.findOneAndUpdate(query, { $set: { subscribeRules } }, options);
  560. return res.apiv3(response);
  561. }
  562. catch (err) {
  563. logger.error(err);
  564. return res.apiv3Err('updating-in-app-notification-settings-failed');
  565. }
  566. });
  567. /**
  568. * @swagger
  569. *
  570. * /personal-setting/in-app-notification-settings:
  571. * get:
  572. * tags: [in-app-notification-settings]
  573. * operationId: getInAppNotificationSettings
  574. * summary: personal-setting/in-app-notification-settings
  575. * description: Get InAppNotificationSettings
  576. * responses:
  577. * 200:
  578. * description: params of InAppNotificationSettings
  579. * content:
  580. * application/json:
  581. * schema:
  582. * properties:
  583. * currentUser:
  584. * type: object
  585. * description: InAppNotificationSettings
  586. */
  587. router.get('/in-app-notification-settings', accessTokenParser, loginRequiredStrictly, async(req, res) => {
  588. const query = { userId: req.user.id };
  589. try {
  590. const response = await InAppNotificationSettings.findOne(query);
  591. return res.apiv3(response);
  592. }
  593. catch (err) {
  594. logger.error(err);
  595. return res.apiv3Err('getting-in-app-notification-settings-failed');
  596. }
  597. });
  598. return router;
  599. };