personal-setting.js 21 KB

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