users.js 37 KB

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