user-activation.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. import type { IUser } from '@growi/core';
  2. import { ErrorV3 } from '@growi/core/dist/models';
  3. import { format, subSeconds } from 'date-fns';
  4. import { body, validationResult } from 'express-validator';
  5. import mongoose from 'mongoose';
  6. import path from 'path';
  7. import { SupportedAction } from '~/interfaces/activity';
  8. import { RegistrationMode } from '~/interfaces/registration-mode';
  9. import type Crowi from '~/server/crowi';
  10. import UserRegistrationOrder from '~/server/models/user-registration-order';
  11. import { configManager } from '~/server/service/config-manager';
  12. import { growiInfoService } from '~/server/service/growi-info';
  13. import { getTranslation } from '~/server/service/i18next';
  14. import loggerFactory from '~/utils/logger';
  15. const logger = loggerFactory('growi:routes:apiv3:user-activation');
  16. const PASSOWRD_MINIMUM_NUMBER = 8;
  17. // validation rules for complete registration form
  18. export const completeRegistrationRules = () => {
  19. return [
  20. body('username')
  21. .matches(/^[\da-zA-Z\-_.]+$/)
  22. .withMessage('Username has invalid characters')
  23. .not()
  24. .isEmpty()
  25. .withMessage('Username field is required'),
  26. body('name').not().isEmpty().withMessage('Name field is required'),
  27. body('token').not().isEmpty().withMessage('Token value is required'),
  28. body('password')
  29. .matches(/^[\x20-\x7F]*$/)
  30. .withMessage('Password has invalid character')
  31. .isLength({ min: PASSOWRD_MINIMUM_NUMBER })
  32. .withMessage(
  33. 'Password minimum character should be more than 8 characters',
  34. )
  35. .not()
  36. .isEmpty()
  37. .withMessage('Password field is required'),
  38. ];
  39. };
  40. // middleware to validate complete registration form
  41. export const validateCompleteRegistration = (req, res, next) => {
  42. const errors = validationResult(req);
  43. if (errors.isEmpty()) {
  44. return next();
  45. }
  46. const extractedErrors: string[] = [];
  47. errors.array().map((err) => extractedErrors.push(err.msg));
  48. return res.apiv3Err(extractedErrors);
  49. };
  50. async function sendEmailToAllAdmins(
  51. userData,
  52. admins,
  53. appTitle,
  54. mailService,
  55. template,
  56. url,
  57. ) {
  58. admins.map((admin) => {
  59. return mailService.send({
  60. to: admin.email,
  61. subject: `[${appTitle}:admin] A New User Created and Waiting for Activation`,
  62. template,
  63. vars: {
  64. createdUser: userData,
  65. admin,
  66. url,
  67. appTitle,
  68. },
  69. });
  70. });
  71. }
  72. /**
  73. * @swagger
  74. *
  75. * /complete-registration:
  76. * post:
  77. * summary: /complete-registration
  78. * tags: [Users]
  79. * security: []
  80. * requestBody:
  81. * required: true
  82. * content:
  83. * application/json:
  84. * schema:
  85. * type: object
  86. * properties:
  87. * registerForm:
  88. * type: object
  89. * properties:
  90. * username:
  91. * type: string
  92. * name:
  93. * type: string
  94. * password:
  95. * type: string
  96. * token:
  97. * type: string
  98. * email:
  99. * type: string
  100. * responses:
  101. * 200:
  102. * description: User activation successful
  103. * content:
  104. * application/json:
  105. * schema:
  106. * type: object
  107. * properties:
  108. * redirectTo:
  109. * type: string
  110. */
  111. export const completeRegistrationAction = (crowi: Crowi) => {
  112. const User = mongoose.model<
  113. IUser,
  114. { isEmailValid; isRegisterable; createUserByEmailAndPassword; findAdmins }
  115. >('User');
  116. const activityEvent = crowi.event('activity');
  117. const { aclService, appService, mailService } = crowi;
  118. return async (req, res) => {
  119. const { t } = await getTranslation();
  120. if (req.user != null) {
  121. return res.apiv3Err(
  122. new ErrorV3('You have been logged in', 'registration-failed'),
  123. 403,
  124. );
  125. }
  126. // error when registration is not allowed
  127. if (
  128. configManager.getConfig('security:registrationMode') ===
  129. aclService.labels.SECURITY_REGISTRATION_MODE_CLOSED
  130. ) {
  131. return res.apiv3Err(
  132. new ErrorV3('Registration closed', 'registration-failed'),
  133. 403,
  134. );
  135. }
  136. // error when email authentication is disabled
  137. if (
  138. configManager.getConfig(
  139. 'security:passport-local:isEmailAuthenticationEnabled',
  140. ) !== true
  141. ) {
  142. return res.apiv3Err(
  143. new ErrorV3(
  144. 'Email authentication configuration is disabled',
  145. 'registration-failed',
  146. ),
  147. 403,
  148. );
  149. }
  150. const { userRegistrationOrder } = req;
  151. const registerForm = req.body;
  152. const email = userRegistrationOrder.email;
  153. const name = registerForm.name;
  154. const username = registerForm.username;
  155. const password = registerForm.password;
  156. // email と username の unique チェックする
  157. User.isRegisterable(email, username, (isRegisterable, errOn) => {
  158. let isError = false;
  159. let errorMessage = '';
  160. if (!User.isEmailValid(email)) {
  161. isError = true;
  162. errorMessage += t('message.email_address_could_not_be_used');
  163. }
  164. if (!isRegisterable) {
  165. if (!errOn.username) {
  166. isError = true;
  167. errorMessage += t('message.user_id_is_not_available');
  168. }
  169. if (!errOn.email) {
  170. isError = true;
  171. errorMessage += t('message.email_address_is_already_registered');
  172. }
  173. }
  174. if (isError) {
  175. return res.apiv3Err(
  176. new ErrorV3(errorMessage, 'registration-failed'),
  177. 403,
  178. );
  179. }
  180. User.createUserByEmailAndPassword(
  181. name,
  182. username,
  183. email,
  184. password,
  185. undefined,
  186. async (err, userData) => {
  187. if (err) {
  188. if (err.name === 'UserUpperLimitException') {
  189. errorMessage = t(
  190. 'message.can_not_register_maximum_number_of_users',
  191. );
  192. } else {
  193. errorMessage = t('message.failed_to_register');
  194. }
  195. return res.apiv3Err(
  196. new ErrorV3(errorMessage, 'registration-failed'),
  197. 403,
  198. );
  199. }
  200. const parameters = {
  201. action: SupportedAction.ACTION_USER_REGISTRATION_SUCCESS,
  202. };
  203. activityEvent.emit('update', res.locals.activity._id, parameters);
  204. userRegistrationOrder.revokeOneTimeToken();
  205. if (
  206. configManager.getConfig('security:registrationMode') ===
  207. aclService.labels.SECURITY_REGISTRATION_MODE_RESTRICTED
  208. ) {
  209. const isMailerSetup = mailService.isMailerSetup ?? false;
  210. if (isMailerSetup) {
  211. const admins = await User.findAdmins();
  212. const appTitle = appService.getAppTitle();
  213. const locale = configManager.getConfig('app:globalLang');
  214. const template = path.join(
  215. crowi.localeDir,
  216. `${locale}/admin/userWaitingActivation.ejs`,
  217. );
  218. const url = growiInfoService.getSiteUrl();
  219. sendEmailToAllAdmins(
  220. userData,
  221. admins,
  222. appTitle,
  223. mailService,
  224. template,
  225. url,
  226. );
  227. }
  228. // This 'completeRegistrationAction' should not be able to be called if the email settings is not set up in the first place.
  229. // So this method dows not stop processing as an error, but only displays a warning. -- 2022.11.01 Yuki Takei
  230. else {
  231. logger.warn('E-mail Settings must be set up.');
  232. }
  233. return res.apiv3({});
  234. }
  235. req.login(userData, (err) => {
  236. if (err) {
  237. logger.debug(err);
  238. } else {
  239. // update lastLoginAt
  240. userData.updateLastLoginAt(new Date(), (err) => {
  241. if (err) {
  242. logger.error(`updateLastLoginAt dumps error: ${err}`);
  243. }
  244. });
  245. }
  246. // userData.password can't be empty but, prepare redirect because password property in User Model is optional
  247. // https://github.com/growilabs/growi/pull/6670
  248. const redirectTo =
  249. userData.password != null ? '/' : '/me#password_settings';
  250. return res.apiv3({ redirectTo });
  251. });
  252. },
  253. );
  254. });
  255. };
  256. };
  257. // validation rules for registration form when email authentication enabled
  258. export const registerRules = () => {
  259. return [
  260. body('registerForm.email')
  261. .isEmail()
  262. .withMessage('Email format is invalid.')
  263. .exists()
  264. .withMessage('Email field is required.'),
  265. ];
  266. };
  267. // middleware to validate register form if email authentication enabled
  268. export const validateRegisterForm = (req, res, next) => {
  269. const errors = validationResult(req);
  270. if (errors.isEmpty()) {
  271. return next();
  272. }
  273. const extractedErrors: string[] = [];
  274. errors.array().map((err) => extractedErrors.push(err.msg));
  275. return res.apiv3Err(extractedErrors, 400);
  276. };
  277. async function makeRegistrationEmailToken(email, crowi: Crowi) {
  278. const { mailService, localeDir, appService } = crowi;
  279. const isMailerSetup = mailService.isMailerSetup ?? false;
  280. if (!isMailerSetup) {
  281. throw Error('mailService is not setup');
  282. }
  283. const locale = configManager.getConfig('app:globalLang');
  284. const appUrl = growiInfoService.getSiteUrl();
  285. const userRegistrationOrder =
  286. await UserRegistrationOrder.createUserRegistrationOrder(email);
  287. const grwTzoffsetSec = crowi.appService.getTzoffset() * 60;
  288. const expiredAt = subSeconds(userRegistrationOrder.expiredAt, grwTzoffsetSec);
  289. const formattedExpiredAt = format(expiredAt, 'yyyy/MM/dd HH:mm');
  290. const url = new URL(
  291. `/user-activation/${userRegistrationOrder.token}`,
  292. appUrl,
  293. );
  294. const oneTimeUrl = url.href;
  295. return mailService.send({
  296. to: email,
  297. subject: '[GROWI] User Activation',
  298. template: path.join(
  299. localeDir,
  300. `${locale}/notifications/userActivation.ejs`,
  301. ),
  302. vars: {
  303. appTitle: appService.getAppTitle(),
  304. email,
  305. expiredAt: formattedExpiredAt,
  306. url: oneTimeUrl,
  307. },
  308. });
  309. }
  310. export const registerAction = (crowi: Crowi) => {
  311. const User = mongoose.model<IUser, { isRegisterableEmail; isEmailValid }>(
  312. 'User',
  313. );
  314. return async (req, res) => {
  315. const registerForm = req.body.registerForm || {};
  316. const email = registerForm.email;
  317. const isRegisterableEmail = await User.isRegisterableEmail(email);
  318. const registrationMode = configManager.getConfig(
  319. 'security:registrationMode',
  320. );
  321. const isEmailValid = await User.isEmailValid(email);
  322. if (registrationMode === RegistrationMode.CLOSED) {
  323. return res.apiv3Err(['message.registration_closed'], 400);
  324. }
  325. if (!isRegisterableEmail) {
  326. req.body.registerForm.email = email;
  327. return res.apiv3Err(['message.email_address_is_already_registered'], 400);
  328. }
  329. if (!isEmailValid) {
  330. return res.apiv3Err(['message.email_address_could_not_be_used'], 400);
  331. }
  332. try {
  333. await makeRegistrationEmailToken(email, crowi);
  334. } catch (err) {
  335. return res.apiv3Err(err);
  336. }
  337. return res.apiv3({ redirectTo: '/login#register' });
  338. };
  339. };