personal-setting.js 20 KB

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