users.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047
  1. import { ErrorV3 } from '@growi/core';
  2. import { SupportedAction } from '~/interfaces/activity';
  3. import Activity from '~/server/models/activity';
  4. import loggerFactory from '~/utils/logger';
  5. import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
  6. import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
  7. const logger = loggerFactory('growi:routes:apiv3:users');
  8. const express = require('express');
  9. const router = express.Router();
  10. const path = require('path');
  11. const { body, query } = require('express-validator');
  12. const { isEmail } = require('validator');
  13. const { serializePageSecurely } = require('../../models/serializers/page-serializer');
  14. const { serializeUserSecurely } = require('../../models/serializers/user-serializer');
  15. const PAGE_ITEMS = 50;
  16. const validator = {};
  17. /**
  18. * @swagger
  19. * tags:
  20. * name: Users
  21. */
  22. /**
  23. * @swagger
  24. *
  25. * components:
  26. * schemas:
  27. * User:
  28. * description: User
  29. * type: object
  30. * properties:
  31. * _id:
  32. * type: string
  33. * description: user ID
  34. * example: 5ae5fccfc5577b0004dbd8ab
  35. * lang:
  36. * type: string
  37. * description: language
  38. * example: 'en_US'
  39. * status:
  40. * type: integer
  41. * description: status
  42. * example: 0
  43. * admin:
  44. * type: boolean
  45. * description: whether the admin
  46. * example: false
  47. * email:
  48. * type: string
  49. * description: E-Mail address
  50. * example: alice@aaa.aaa
  51. * username:
  52. * type: string
  53. * description: username
  54. * example: alice
  55. * name:
  56. * type: string
  57. * description: full name
  58. * example: Alice
  59. * createdAt:
  60. * type: string
  61. * description: date created at
  62. * example: 2010-01-01T00:00:00.000Z
  63. */
  64. module.exports = (crowi) => {
  65. const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
  66. const loginRequired = require('../../middlewares/login-required')(crowi, true);
  67. const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
  68. const adminRequired = require('../../middlewares/admin-required')(crowi);
  69. const addActivity = generateAddActivityMiddleware(crowi);
  70. const activityEvent = crowi.event('activity');
  71. const {
  72. User,
  73. Page,
  74. ExternalAccount,
  75. UserGroupRelation,
  76. } = crowi.models;
  77. const statusNo = {
  78. registered: User.STATUS_REGISTERED,
  79. active: User.STATUS_ACTIVE,
  80. suspended: User.STATUS_SUSPENDED,
  81. invited: User.STATUS_INVITED,
  82. };
  83. validator.statusList = [
  84. query('selectedStatusList').if(value => value != null).custom((value, { req }) => {
  85. const { user } = req;
  86. if (user != null && user.admin) {
  87. return value;
  88. }
  89. throw new Error('the param \'selectedStatusList\' is not allowed to use by the users except administrators');
  90. }),
  91. // validate sortOrder : asc or desc
  92. query('sortOrder').isIn(['asc', 'desc']),
  93. // validate sort : what column you will sort
  94. query('sort').isIn(['id', 'status', 'username', 'name', 'email', 'createdAt', 'lastLoginAt']),
  95. query('page').isInt({ min: 1 }),
  96. query('forceIncludeAttributes').toArray().custom((value, { req }) => {
  97. // only the admin user can specify forceIncludeAttributes
  98. if (value.length === 0) {
  99. return true;
  100. }
  101. return req.user.admin;
  102. }),
  103. ];
  104. validator.recentCreatedByUser = [
  105. query('limit').if(value => value != null).isInt({ max: 300 }).withMessage('You should set less than 300 or not to set limit.'),
  106. ];
  107. validator.usernames = [
  108. query('q').isString().withMessage('q is required'),
  109. query('offset').optional().isInt().withMessage('offset must be a number'),
  110. query('limit').optional().isInt({ max: 20 }).withMessage('You should set less than 20 or not to set limit.'),
  111. query('options').optional().isString().withMessage('options must be string'),
  112. ];
  113. // express middleware
  114. const certifyUserOperationOtherThenYourOwn = (req, res, next) => {
  115. const { id } = req.params;
  116. if (req.user._id.toString() === id) {
  117. const msg = 'This API is not available for your own users';
  118. logger.error(msg);
  119. return res.apiv3Err(new ErrorV3(msg), 400);
  120. }
  121. next();
  122. };
  123. const sendEmailByUserList = async(userList) => {
  124. const { appService, mailService } = crowi;
  125. const appTitle = appService.getAppTitle();
  126. const failedToSendEmailList = [];
  127. for (const user of userList) {
  128. try {
  129. // eslint-disable-next-line no-await-in-loop
  130. await mailService.send({
  131. to: user.email,
  132. subject: `Invitation to ${appTitle}`,
  133. template: path.join(crowi.localeDir, 'en_US/admin/userInvitation.txt'),
  134. vars: {
  135. email: user.email,
  136. password: user.password,
  137. url: crowi.appService.getSiteUrl(),
  138. appTitle,
  139. },
  140. });
  141. // eslint-disable-next-line no-await-in-loop
  142. await User.updateIsInvitationEmailSended(user.user.id);
  143. }
  144. catch (err) {
  145. logger.error(err);
  146. failedToSendEmailList.push({
  147. email: user.email,
  148. reason: err.message,
  149. });
  150. }
  151. }
  152. return { failedToSendEmailList };
  153. };
  154. /**
  155. * @swagger
  156. *
  157. * paths:
  158. * /users:
  159. * get:
  160. * tags: [Users]
  161. * operationId: listUsers
  162. * summary: /users
  163. * description: Select selected columns from users order by asc or desc
  164. * parameters:
  165. * - name: page
  166. * in: query
  167. * description: page number
  168. * schema:
  169. * type: number
  170. * - name: selectedStatusList
  171. * in: query
  172. * description: status list
  173. * schema:
  174. * type: string
  175. * - name: searchText
  176. * in: query
  177. * description: For incremental search value from input box
  178. * schema:
  179. * type: string
  180. * - name: sortOrder
  181. * in: query
  182. * description: asc or desc
  183. * schema:
  184. * type: string
  185. * - name: sort
  186. * in: query
  187. * description: sorting column
  188. * schema:
  189. * type: string
  190. * responses:
  191. * 200:
  192. * description: users are fetched
  193. * content:
  194. * application/json:
  195. * schema:
  196. * properties:
  197. * paginateResult:
  198. * $ref: '#/components/schemas/PaginateResult'
  199. */
  200. router.get('/', accessTokenParser, loginRequired, validator.statusList, apiV3FormValidator, async(req, res) => {
  201. const page = parseInt(req.query.page) || 1;
  202. // status
  203. const { forceIncludeAttributes } = req.query;
  204. const selectedStatusList = req.query.selectedStatusList || ['active'];
  205. const statusNoList = (selectedStatusList.includes('all')) ? Object.values(statusNo) : selectedStatusList.map(element => statusNo[element]);
  206. // Search from input
  207. const searchText = req.query.searchText || '';
  208. const searchWord = new RegExp(`${searchText}`);
  209. // Sort
  210. const { sort, sortOrder } = req.query;
  211. const sortOutput = {
  212. [sort]: (sortOrder === 'desc') ? -1 : 1,
  213. };
  214. // For more information about the external specification of the User API, see here (https://dev.growi.org/5fd7466a31d89500488248e3)
  215. const orConditions = [
  216. { name: { $in: searchWord } },
  217. { username: { $in: searchWord } },
  218. ];
  219. const query = {
  220. $and: [
  221. { status: { $in: statusNoList } },
  222. {
  223. $or: orConditions,
  224. },
  225. ],
  226. };
  227. try {
  228. if (req.user != null) {
  229. orConditions.push(
  230. {
  231. $and: [
  232. { isEmailPublished: true },
  233. { email: { $in: searchWord } },
  234. ],
  235. },
  236. );
  237. }
  238. if (forceIncludeAttributes.includes('email')) {
  239. orConditions.push({ email: { $in: searchWord } });
  240. }
  241. const paginateResult = await User.paginate(
  242. query,
  243. {
  244. sort: sortOutput,
  245. page,
  246. limit: PAGE_ITEMS,
  247. },
  248. );
  249. paginateResult.docs = paginateResult.docs.map((doc) => {
  250. // return email only when specified by query
  251. const { email } = doc;
  252. const user = serializeUserSecurely(doc);
  253. if (forceIncludeAttributes.includes('email')) {
  254. user.email = email;
  255. }
  256. return user;
  257. });
  258. return res.apiv3({ paginateResult });
  259. }
  260. catch (err) {
  261. const msg = 'Error occurred in fetching user group list';
  262. logger.error('Error', err);
  263. return res.apiv3Err(new ErrorV3(msg, 'user-group-list-fetch-failed'), 500);
  264. }
  265. });
  266. /**
  267. * @swagger
  268. *
  269. * paths:
  270. * /{id}/recent:
  271. * get:
  272. * tags: [Users]
  273. * operationId: recent created page of user id
  274. * summary: /usersIdReacent
  275. * parameters:
  276. * - name: id
  277. * in: path
  278. * required: true
  279. * description: id of user
  280. * schema:
  281. * type: string
  282. * responses:
  283. * 200:
  284. * description: users recent created pages are fetched
  285. * content:
  286. * application/json:
  287. * schema:
  288. * properties:
  289. * paginateResult:
  290. * $ref: '#/components/schemas/PaginateResult'
  291. */
  292. router.get('/:id/recent', accessTokenParser, loginRequired, validator.recentCreatedByUser, apiV3FormValidator, async(req, res) => {
  293. const { id } = req.params;
  294. let user;
  295. try {
  296. user = await User.findById(id);
  297. }
  298. catch (err) {
  299. const msg = 'Error occurred in find user';
  300. logger.error('Error', err);
  301. return res.apiv3Err(new ErrorV3(msg, 'retrieve-recent-created-pages-failed'), 500);
  302. }
  303. if (user == null) {
  304. return res.apiv3Err(new ErrorV3('find-user-is-not-found'));
  305. }
  306. const limit = parseInt(req.query.limit) || await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationM') || 30;
  307. const page = req.query.page;
  308. const offset = (page - 1) * limit;
  309. const queryOptions = { offset, limit };
  310. try {
  311. const result = await Page.findListByCreator(user, req.user, queryOptions);
  312. result.pages = result.pages.map(page => serializePageSecurely(page));
  313. return res.apiv3(result);
  314. }
  315. catch (err) {
  316. const msg = 'Error occurred in retrieve recent created pages for user';
  317. logger.error('Error', err);
  318. return res.apiv3Err(new ErrorV3(msg, 'retrieve-recent-created-pages-failed'), 500);
  319. }
  320. });
  321. validator.inviteEmail = [
  322. // isEmail prevents line breaks, so use isString
  323. body('shapedEmailList').custom((value) => {
  324. const array = value.filter((value) => { return isEmail(value) });
  325. if (array.length === 0) {
  326. throw new Error('At least one valid email address is required');
  327. }
  328. return array;
  329. }),
  330. ];
  331. /**
  332. * @swagger
  333. *
  334. * paths:
  335. * /users/invite:
  336. * post:
  337. * tags: [Users]
  338. * operationId: inviteUser
  339. * summary: /users/invite
  340. * description: Create new users and send Emails
  341. * parameters:
  342. * - name: shapedEmailList
  343. * in: query
  344. * description: Invitation emailList
  345. * schema:
  346. * type: object
  347. * - name: sendEmail
  348. * in: query
  349. * description: Whether to send mail
  350. * schema:
  351. * type: boolean
  352. * responses:
  353. * 200:
  354. * description: Inviting user success
  355. * content:
  356. * application/json:
  357. * schema:
  358. * properties:
  359. * createdUserList:
  360. * type: object
  361. * description: Users successfully created
  362. * existingEmailList:
  363. * type: object
  364. * description: Users email that already exists
  365. * failedEmailList:
  366. * type: object
  367. * description: Users email that failed to create or send email
  368. */
  369. router.post('/invite', loginRequiredStrictly, adminRequired, addActivity, validator.inviteEmail, apiV3FormValidator, async(req, res) => {
  370. // Delete duplicate email addresses
  371. const emailList = Array.from(new Set(req.body.shapedEmailList));
  372. let failedEmailList = [];
  373. // Create users
  374. const createUser = await User.createUsersByEmailList(emailList);
  375. if (createUser.failedToCreateUserEmailList.length > 0) {
  376. failedEmailList = failedEmailList.concat(createUser.failedToCreateUserEmailList);
  377. }
  378. // Send email
  379. if (req.body.sendEmail) {
  380. const sendEmail = await sendEmailByUserList(createUser.createdUserList);
  381. if (sendEmail.failedToSendEmailList.length > 0) {
  382. failedEmailList = failedEmailList.concat(sendEmail.failedToSendEmailList);
  383. }
  384. }
  385. const parameters = { action: SupportedAction.ACTION_ADMIN_USERS_INVITE };
  386. activityEvent.emit('update', res.locals.activity._id, parameters);
  387. return res.apiv3({
  388. createdUserList: createUser.createdUserList,
  389. existingEmailList: createUser.existingEmailList,
  390. failedEmailList,
  391. }, 201);
  392. });
  393. /**
  394. * @swagger
  395. *
  396. * paths:
  397. * /users/{id}/giveAdmin:
  398. * put:
  399. * tags: [Users]
  400. * operationId: giveAdminUser
  401. * summary: /users/{id}/giveAdmin
  402. * description: Give user admin
  403. * parameters:
  404. * - name: id
  405. * in: path
  406. * required: true
  407. * description: id of user for admin
  408. * schema:
  409. * type: string
  410. * responses:
  411. * 200:
  412. * description: Give user admin success
  413. * content:
  414. * application/json:
  415. * schema:
  416. * properties:
  417. * userData:
  418. * type: object
  419. * description: data of admin user
  420. */
  421. router.put('/:id/giveAdmin', loginRequiredStrictly, adminRequired, addActivity, async(req, res) => {
  422. const { id } = req.params;
  423. try {
  424. const userData = await User.findById(id);
  425. await userData.makeAdmin();
  426. const serializedUserData = serializeUserSecurely(userData);
  427. activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_USERS_GIVE_ADMIN });
  428. return res.apiv3({ userData: serializedUserData });
  429. }
  430. catch (err) {
  431. logger.error('Error', err);
  432. return res.apiv3Err(new ErrorV3(err));
  433. }
  434. });
  435. /**
  436. * @swagger
  437. *
  438. * paths:
  439. * /users/{id}/removeAdmin:
  440. * put:
  441. * tags: [Users]
  442. * operationId: removeAdminUser
  443. * summary: /users/{id}/removeAdmin
  444. * description: Remove user admin
  445. * parameters:
  446. * - name: id
  447. * in: path
  448. * required: true
  449. * description: id of user for removing admin
  450. * schema:
  451. * type: string
  452. * responses:
  453. * 200:
  454. * description: Remove user admin success
  455. * content:
  456. * application/json:
  457. * schema:
  458. * properties:
  459. * userData:
  460. * type: object
  461. * description: data of removed admin user
  462. */
  463. router.put('/:id/removeAdmin', loginRequiredStrictly, adminRequired, certifyUserOperationOtherThenYourOwn, addActivity, async(req, res) => {
  464. const { id } = req.params;
  465. try {
  466. const userData = await User.findById(id);
  467. await userData.removeFromAdmin();
  468. const serializedUserData = serializeUserSecurely(userData);
  469. activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_USERS_REMOVE_ADMIN });
  470. return res.apiv3({ userData: serializedUserData });
  471. }
  472. catch (err) {
  473. logger.error('Error', err);
  474. return res.apiv3Err(new ErrorV3(err));
  475. }
  476. });
  477. /**
  478. * @swagger
  479. *
  480. * paths:
  481. * /users/{id}/activate:
  482. * put:
  483. * tags: [Users]
  484. * operationId: activateUser
  485. * summary: /users/{id}/activate
  486. * description: Activate user
  487. * parameters:
  488. * - name: id
  489. * in: path
  490. * required: true
  491. * description: id of activate user
  492. * schema:
  493. * type: string
  494. * responses:
  495. * 200:
  496. * description: Activationg user success
  497. * content:
  498. * application/json:
  499. * schema:
  500. * properties:
  501. * userData:
  502. * type: object
  503. * description: data of activate user
  504. */
  505. router.put('/:id/activate', loginRequiredStrictly, adminRequired, addActivity, async(req, res) => {
  506. // check user upper limit
  507. const isUserCountExceedsUpperLimit = await User.isUserCountExceedsUpperLimit();
  508. if (isUserCountExceedsUpperLimit) {
  509. const msg = 'Unable to activate because user has reached limit';
  510. logger.error('Error', msg);
  511. return res.apiv3Err(new ErrorV3(msg));
  512. }
  513. const { id } = req.params;
  514. try {
  515. const userData = await User.findById(id);
  516. await userData.statusActivate();
  517. const serializedUserData = serializeUserSecurely(userData);
  518. activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_USERS_ACTIVATE });
  519. return res.apiv3({ userData: serializedUserData });
  520. }
  521. catch (err) {
  522. logger.error('Error', err);
  523. return res.apiv3Err(new ErrorV3(err));
  524. }
  525. });
  526. /**
  527. * @swagger
  528. *
  529. * paths:
  530. * /users/{id}/deactivate:
  531. * put:
  532. * tags: [Users]
  533. * operationId: deactivateUser
  534. * summary: /users/{id}/deactivate
  535. * description: Deactivate user
  536. * parameters:
  537. * - name: id
  538. * in: path
  539. * required: true
  540. * description: id of deactivate user
  541. * schema:
  542. * type: string
  543. * responses:
  544. * 200:
  545. * description: Deactivationg user success
  546. * content:
  547. * application/json:
  548. * schema:
  549. * properties:
  550. * userData:
  551. * type: object
  552. * description: data of deactivate user
  553. */
  554. router.put('/:id/deactivate', loginRequiredStrictly, adminRequired, certifyUserOperationOtherThenYourOwn, addActivity, async(req, res) => {
  555. const { id } = req.params;
  556. try {
  557. const userData = await User.findById(id);
  558. await userData.statusSuspend();
  559. const serializedUserData = serializeUserSecurely(userData);
  560. activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_USERS_DEACTIVATE });
  561. return res.apiv3({ userData: serializedUserData });
  562. }
  563. catch (err) {
  564. logger.error('Error', err);
  565. return res.apiv3Err(new ErrorV3(err));
  566. }
  567. });
  568. /**
  569. * @swagger
  570. *
  571. * paths:
  572. * /users/{id}/remove:
  573. * delete:
  574. * tags: [Users]
  575. * operationId: removeUser
  576. * summary: /users/{id}/remove
  577. * description: Delete user
  578. * parameters:
  579. * - name: id
  580. * in: path
  581. * required: true
  582. * description: id of delete user
  583. * schema:
  584. * type: string
  585. * responses:
  586. * 200:
  587. * description: Deleting user success
  588. * content:
  589. * application/json:
  590. * schema:
  591. * properties:
  592. * userData:
  593. * type: object
  594. * description: data of delete user
  595. */
  596. router.delete('/:id/remove', loginRequiredStrictly, adminRequired, certifyUserOperationOtherThenYourOwn, addActivity, async(req, res) => {
  597. const { id } = req.params;
  598. try {
  599. const userData = await User.findById(id);
  600. await UserGroupRelation.remove({ relatedUser: userData });
  601. await userData.statusDelete();
  602. await ExternalAccount.remove({ user: userData });
  603. await Page.removeByPath(`/user/${userData.username}`);
  604. const serializedUserData = serializeUserSecurely(userData);
  605. activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_USERS_REMOVE });
  606. return res.apiv3({ userData: serializedUserData });
  607. }
  608. catch (err) {
  609. logger.error('Error', err);
  610. return res.apiv3Err(new ErrorV3(err));
  611. }
  612. });
  613. /**
  614. * @swagger
  615. *
  616. * paths:
  617. * /users/external-accounts:
  618. * get:
  619. * tags: [Users]
  620. * operationId: listExternalAccountsUsers
  621. * summary: /users/external-accounts
  622. * description: Get external-account
  623. * responses:
  624. * 200:
  625. * description: external-account are fetched
  626. * content:
  627. * application/json:
  628. * schema:
  629. * properties:
  630. * paginateResult:
  631. * $ref: '#/components/schemas/PaginateResult'
  632. */
  633. router.get('/external-accounts/', loginRequiredStrictly, adminRequired, async(req, res) => {
  634. const page = parseInt(req.query.page) || 1;
  635. try {
  636. const paginateResult = await ExternalAccount.findAllWithPagination({ page });
  637. return res.apiv3({ paginateResult });
  638. }
  639. catch (err) {
  640. const msg = 'Error occurred in fetching external-account list ';
  641. logger.error(msg, err);
  642. return res.apiv3Err(new ErrorV3(msg + err.message, 'external-account-list-fetch-failed'), 500);
  643. }
  644. });
  645. /**
  646. * @swagger
  647. *
  648. * paths:
  649. * /users/external-accounts/{id}/remove:
  650. * delete:
  651. * tags: [Users]
  652. * operationId: removeExternalAccountUser
  653. * summary: /users/external-accounts/{id}/remove
  654. * description: Delete ExternalAccount
  655. * parameters:
  656. * - name: id
  657. * in: path
  658. * required: true
  659. * description: id of ExternalAccount
  660. * schema:
  661. * type: string
  662. * responses:
  663. * 200:
  664. * description: External Account is removed
  665. * content:
  666. * application/json:
  667. * schema:
  668. * properties:
  669. * externalAccount:
  670. * type: object
  671. * description: A result of `ExtenralAccount.findByIdAndRemove`
  672. */
  673. router.delete('/external-accounts/:id/remove', loginRequiredStrictly, adminRequired, apiV3FormValidator, async(req, res) => {
  674. const { id } = req.params;
  675. try {
  676. const externalAccount = await ExternalAccount.findByIdAndRemove(id);
  677. return res.apiv3({ externalAccount });
  678. }
  679. catch (err) {
  680. const msg = 'Error occurred in deleting a external account ';
  681. logger.error(msg, err);
  682. return res.apiv3Err(new ErrorV3(msg + err.message, 'extenral-account-delete-failed'));
  683. }
  684. });
  685. /**
  686. * @swagger
  687. *
  688. * paths:
  689. * /users/update.imageUrlCache:
  690. * put:
  691. * tags: [Users]
  692. * operationId: update.imageUrlCache
  693. * summary: /users/update.imageUrlCache
  694. * description: update imageUrlCache
  695. * parameters:
  696. * - name: userIds
  697. * in: query
  698. * description: user id list
  699. * schema:
  700. * type: string
  701. * responses:
  702. * 200:
  703. * description: success creating imageUrlCached
  704. * content:
  705. * application/json:
  706. * schema:
  707. * properties:
  708. * userData:
  709. * type: object
  710. * description: users updated with imageUrlCached
  711. */
  712. router.put('/update.imageUrlCache', loginRequiredStrictly, adminRequired, async(req, res) => {
  713. try {
  714. const userIds = req.body.userIds;
  715. const users = await User.find({ _id: { $in: userIds }, imageUrlCached: null });
  716. const requests = await Promise.all(users.map(async(user) => {
  717. return {
  718. updateOne: {
  719. filter: { _id: user._id },
  720. update: { $set: { imageUrlCached: await user.generateImageUrlCached() } },
  721. },
  722. };
  723. }));
  724. if (requests.length > 0) {
  725. await User.bulkWrite(requests);
  726. }
  727. return res.apiv3({});
  728. }
  729. catch (err) {
  730. logger.error('Error', err);
  731. return res.apiv3Err(new ErrorV3(err));
  732. }
  733. });
  734. /**
  735. * @swagger
  736. *
  737. * paths:
  738. * /users/reset-password:
  739. * put:
  740. * tags: [Users]
  741. * operationId: resetPassword
  742. * summary: /users/reset-password
  743. * description: update imageUrlCache
  744. * requestBody:
  745. * content:
  746. * application/json:
  747. * schema:
  748. * properties:
  749. * id:
  750. * type: string
  751. * description: user id for reset password
  752. * responses:
  753. * 200:
  754. * description: success resrt password
  755. */
  756. router.put('/reset-password', loginRequiredStrictly, adminRequired, addActivity, async(req, res) => {
  757. const { appService, mailService } = crowi;
  758. const { id } = req.body;
  759. let newPassword;
  760. let user;
  761. try {
  762. [newPassword, user] = await Promise.all([
  763. await User.resetPasswordByRandomString(id),
  764. await User.findById(id)]);
  765. activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_USERS_PASSWORD_RESET });
  766. }
  767. catch (err) {
  768. const msg = 'Error occurred during password reset request procedure.';
  769. logger.error(err);
  770. return res.apiv3Err(`${msg} Cause: ${err}`);
  771. }
  772. try {
  773. await mailService.send({
  774. to: user.email,
  775. subject: 'Your password has been reset by the administrator',
  776. template: path.join(crowi.localeDir, 'en_US/admin/userResetPassword.txt'),
  777. vars: {
  778. email: user.email,
  779. password: newPassword,
  780. url: crowi.appService.getSiteUrl(),
  781. appTitle: appService.getAppTitle(),
  782. },
  783. });
  784. return res.apiv3({});
  785. }
  786. catch (err) {
  787. const msg = 'Error occurred during password reset send e-mail.';
  788. logger.error(err);
  789. return res.apiv3Err(`${msg} Cause: ${err}`);
  790. }
  791. });
  792. /**
  793. * @swagger
  794. *
  795. * paths:
  796. * /users/send-invitation-email:
  797. * put:
  798. * tags: [Users]
  799. * operationId: sendInvitationEmail
  800. * summary: /users/send-invitation-email
  801. * description: send invitation email
  802. * requestBody:
  803. * content:
  804. * application/json:
  805. * schema:
  806. * properties:
  807. * id:
  808. * type: string
  809. * description: user id for send invitation email
  810. * responses:
  811. * 200:
  812. * description: success send invitation email
  813. * content:
  814. * application/json:
  815. * schema:
  816. * properties:
  817. * failedToSendEmail:
  818. * type: object
  819. * description: email and reasons for email sending failure
  820. */
  821. router.put('/send-invitation-email', loginRequiredStrictly, adminRequired, addActivity, async(req, res) => {
  822. const { id } = req.body;
  823. try {
  824. const user = await User.findById(id);
  825. const newPassword = await User.resetPasswordByRandomString(id);
  826. const userList = [{
  827. email: user.email,
  828. password: newPassword,
  829. user: { id },
  830. }];
  831. const sendEmail = await sendEmailByUserList(userList);
  832. // return null if absent
  833. activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_USERS_SEND_INVITATION_EMAIL });
  834. return res.apiv3({ failedToSendEmail: sendEmail.failedToSendEmailList[0] });
  835. }
  836. catch (err) {
  837. logger.error('Error', err);
  838. return res.apiv3Err(new ErrorV3(err));
  839. }
  840. });
  841. /**
  842. * @swagger
  843. *
  844. * paths:
  845. * /users/list:
  846. * get:
  847. * tags: [Users]
  848. * summary: /users/list
  849. * operationId: getUsersList
  850. * description: Get list of users
  851. * parameters:
  852. * - in: query
  853. * name: userIds
  854. * schema:
  855. * type: string
  856. * description: user IDs
  857. * example: 5e06fcc7516d64004dbf4da6,5e098d53baa2ac004e7d24ad
  858. * responses:
  859. * 200:
  860. * description: Succeeded to get list of users.
  861. * content:
  862. * application/json:
  863. * schema:
  864. * properties:
  865. * users:
  866. * type: array
  867. * items:
  868. * $ref: '#/components/schemas/User'
  869. * description: user list
  870. * 403:
  871. * $ref: '#/components/responses/403'
  872. * 500:
  873. * $ref: '#/components/responses/500'
  874. */
  875. router.get('/list', accessTokenParser, loginRequired, async(req, res) => {
  876. const userIds = req.query.userIds || null;
  877. let userFetcher;
  878. if (userIds !== null && userIds.split(',').length > 0) {
  879. userFetcher = User.findUsersByIds(userIds.split(','));
  880. }
  881. else {
  882. userFetcher = User.findAllUsers();
  883. }
  884. const data = {};
  885. try {
  886. const users = await userFetcher;
  887. data.users = users.map((user) => {
  888. // omit email
  889. if (user.isEmailPublished !== true) { // compare to 'true' because Crowi original data doesn't have 'isEmailPublished'
  890. user.email = undefined;
  891. }
  892. return user.toObject({ virtuals: true });
  893. });
  894. }
  895. catch (err) {
  896. return res.apiv3Err(new ErrorV3(err));
  897. }
  898. return res.apiv3(data);
  899. });
  900. router.get('/usernames', accessTokenParser, loginRequired, validator.usernames, apiV3FormValidator, async(req, res) => {
  901. const q = req.query.q;
  902. const offset = +req.query.offset || 0;
  903. const limit = +req.query.limit || 10;
  904. try {
  905. const options = JSON.parse(req.query.options || '{}');
  906. const data = {};
  907. if (options.isIncludeActiveUser == null || options.isIncludeActiveUser) {
  908. const activeUserData = await User.findUserByUsernameRegexWithTotalCount(q, [User.STATUS_ACTIVE], { offset, limit });
  909. const activeUsernames = activeUserData.users.map(user => user.username);
  910. Object.assign(data, { activeUser: { usernames: activeUsernames, totalCount: activeUserData.totalCount } });
  911. }
  912. if (options.isIncludeInactiveUser) {
  913. const inactiveUserStates = [User.STATUS_REGISTERED, User.STATUS_SUSPENDED, User.STATUS_INVITED];
  914. const inactiveUserData = await User.findUserByUsernameRegexWithTotalCount(q, inactiveUserStates, { offset, limit });
  915. const inactiveUsernames = inactiveUserData.users.map(user => user.username);
  916. Object.assign(data, { inactiveUser: { usernames: inactiveUsernames, totalCount: inactiveUserData.totalCount } });
  917. }
  918. if (options.isIncludeActivitySnapshotUser && req.user.admin) {
  919. const activitySnapshotUserData = await Activity.findSnapshotUsernamesByUsernameRegexWithTotalCount(q, { offset, limit });
  920. Object.assign(data, { activitySnapshotUser: activitySnapshotUserData });
  921. }
  922. // eslint-disable-next-line max-len
  923. const canIncludeMixedUsernames = (options.isIncludeMixedUsernames && req.user.admin) || (options.isIncludeMixedUsernames && !options.isIncludeActivitySnapshotUser);
  924. if (canIncludeMixedUsernames) {
  925. const allUsernames = [...data.activeUser?.usernames || [], ...data.inactiveUser?.usernames || [], ...data?.activitySnapshotUser?.usernames || []];
  926. const distinctUsernames = Array.from(new Set(allUsernames));
  927. Object.assign(data, { mixedUsernames: distinctUsernames });
  928. }
  929. return res.apiv3(data);
  930. }
  931. catch (err) {
  932. logger.error('Failed to get usernames', err);
  933. return res.apiv3Err(err);
  934. }
  935. });
  936. return router;
  937. };