2
0

users.js 38 KB

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