personal-setting.js 19 KB

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