Просмотр исходного кода

Merge pull request #7591 from weseek/imprv/119788-mail-setting-and-id-password-login-specification

imprv: Allow registering without GROWI email settings for ID/Password authentication's restricted registration
Yuki Takei 3 лет назад
Родитель
Сommit
5bdfbd5037

+ 1 - 2
apps/app/public/static/locales/en_US/admin.json

@@ -106,8 +106,7 @@
       "password_reset_desc": "when forgot password, users are able to reset it by themselves.",
       "email_authentication": "Email authentication on user registration",
       "enable_email_authentication": "Enable email authentication",
-      "enable_email_authentication_desc": "Email authentication is going to be performed for user registration.",
-      "need_complete_mail_setting_warning": "To use the following functions, please complete the mail settings."
+      "enable_email_authentication_desc": "Email authentication is going to be performed for user registration."
     },
     "ldap": {
       "enable_ldap": "Enable LDAP",

+ 2 - 1
apps/app/public/static/locales/en_US/commons.json

@@ -20,7 +20,8 @@
   },
   "alert": {
     "siteUrl_is_not_set": "'Site URL' is NOT set. Set it from the {{link}}",
-    "please_enable_mailer": "Please setup mailer first."
+    "please_enable_mailer": "Please setup mailer first.",
+    "password_reset_please_enable_mailer": "Please setup mailer first."
   },
   "headers": {
     "app_settings": "App Settings"

+ 3 - 2
apps/app/public/static/locales/en_US/translation.json

@@ -572,7 +572,7 @@
   "login": {
     "title": "Login",
     "sign_in_error": "Login error",
-    "registration_successful": "registration_successful. Please wait for administrator approval.",
+    "registration_successful": "Registration successful. Please wait for administrator approval.",
     "Setup": "Setup",
     "enabled_ldap_has_configuration_problem":"LDAP is enabled but the configuration has something wrong.",
     "set_env_var_for_logs": "(Please set the environment variables <code>DEBUG=crowi:service:PassportService</code> to get the logs)"
@@ -659,7 +659,8 @@
     "success_to_send_email": "Success to send email",
     "feature_is_unavailable": "This feature is unavailable.",
     "incorrect_token_or_expired_url": "The token is incorrect or the URL has expired. Please resend a password reset request via the link below.",
-    "password_and_confirm_password_does_not_match": "Password and confirm password does not match"
+    "password_and_confirm_password_does_not_match": "Password and confirm password does not match",
+    "please_enable_mailer_alert": "The password reset feature is disabled because email setup has not been completed. Please ask administrator to complete the email setup."
   },
   "emoji" :{
     "title": "Pick an Emoji",

+ 1 - 2
apps/app/public/static/locales/ja_JP/admin.json

@@ -114,8 +114,7 @@
       "password_reset_desc": "ログイン時のパスワードを忘れた際に、ユーザー自身がパスワードを再設定できます。",
       "email_authentication": "ユーザー登録時のメール認証",
       "enable_email_authentication": "メール認証を有効にする",
-      "enable_email_authentication_desc": "ユーザー登録時にメール認証を行います。",
-      "need_complete_mail_setting_warning": "以下の機能を使えるようにするには、メール設定を完了させてください。"
+      "enable_email_authentication_desc": "ユーザー登録時にメール認証を行います。"
     },
     "ldap": {
       "enable_ldap": "LDAP を有効にする",

+ 2 - 1
apps/app/public/static/locales/ja_JP/commons.json

@@ -20,7 +20,8 @@
   },
   "alert": {
     "siteUrl_is_not_set": "'サイトURL' が設定されていません。{{link}} から設定してください。",
-    "please_enable_mailer": "メール認証を有効にするには、メール設定を完了させてください。"
+    "please_enable_mailer": "メール認証を有効にするには、メール設定を完了させてください。",
+    "password_reset_please_enable_mailer": "パスワード再設定を有効にするには、メール設定を完了させてください。"
   },
   "headers": {
     "app_settings": "アプリ設定"

+ 2 - 1
apps/app/public/static/locales/ja_JP/translation.json

@@ -693,7 +693,8 @@
     "success_to_send_email": "メールを送信しました",
     "feature_is_unavailable": "この機能を利用することはできません。",
     "incorrect_token_or_expired_url":"トークンが正しくないか、URLの有効期限が切れています。 以下のリンクからパスワードリセットリクエストを再送信してください。",
-    "password_and_confirm_password_does_not_match": "パスワードと確認パスワードが一致しません"
+    "password_and_confirm_password_does_not_match": "パスワードと確認パスワードが一致しません",
+    "please_enable_mailer_alert": "メール設定が完了していないため、パスワード再設定機能が無効になっています。メール設定を完了させるよう管理者に依頼してください。"
   },
   "emoji" :{
     "title": "絵文字を選択",

+ 1 - 2
apps/app/public/static/locales/zh_CN/admin.json

@@ -114,8 +114,7 @@
       "password_reset_desc": "忘记密码时,用户可以自行重置",
       "email_authentication": "用户注册时的电子邮件身份验证",
       "enable_email_authentication": "启用电子邮件身份验证",
-      "enable_email_authentication_desc": "用户注册将执行电子邮件身份验证。",
-      "need_complete_mail_setting_warning": "要使用以下功能,请完成邮件设置。"
+      "enable_email_authentication_desc": "用户注册将执行电子邮件身份验证。"
 		},
 		"ldap": {
 			"enable_ldap": "Enable LDAP",

+ 2 - 1
apps/app/public/static/locales/zh_CN/commons.json

@@ -20,7 +20,8 @@
   },
   "alert": {
     "siteUrl_is_not_set": "主页URL未设置,通过 {{link}} 设置",
-    "please_enable_mailer": "请先设置邮件程序。"
+    "please_enable_mailer": "请先设置邮件程序。",
+    "password_reset_please_enable_mailer": "请先设置邮件程序。"
   },
   "headers": {
     "app_settings": "系统设置"

+ 2 - 1
apps/app/public/static/locales/zh_CN/translation.json

@@ -665,7 +665,8 @@
     "success_to_send_email": "我发了一封电子邮件",
     "feature_is_unavailable": "此功能不可用",
     "incorrect_token_or_expired_url":"令牌不正确或 URL 已过期。 请通过以下链接重新发送密码重置请求",
-    "password_and_confirm_password_does_not_match": "密码和确认密码不匹配"
+    "password_and_confirm_password_does_not_match": "密码和确认密码不匹配",
+    "please_enable_mailer_alert": "密码重置功能被禁用,因为电子邮件设置尚未完成。请要求管理员完成电子邮件的设置。"
   },
   "emoji" :{
     "title": "选择一个表情符号",

+ 12 - 14
apps/app/src/components/Admin/Security/LocalSecuritySettingContents.jsx

@@ -1,9 +1,9 @@
 import React from 'react';
 
 import { useTranslation } from 'next-i18next';
+import Link from 'next/link';
 import PropTypes from 'prop-types';
 
-
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminLocalSecurityContainer from '~/client/services/AdminLocalSecurityContainer';
 import { toastSuccess, toastError } from '~/client/util/toastr';
@@ -52,17 +52,6 @@ class LocalSecuritySettingContents extends React.Component {
         )}
         <h2 className="alert-anchor border-bottom">{t('security_settings.Local.name')}</h2>
 
-        {!isMailerSetup && (
-          <div className="row">
-            <div className="col-12">
-              <div className="alert alert-danger">
-                <span>{t('security_settings.Local.need_complete_mail_setting_warning')}</span>
-                <a href="/admin/app#mail-settings"> <i className="fa fa-link"></i> {t('admin:app_setting.mail_settings')}</a>
-              </div>
-            </div>
-          </div>
-        )}
-
         {adminLocalSecurityContainer.state.useOnlyEnvVars && (
           <p
             className="alert alert-info"
@@ -146,7 +135,6 @@ class LocalSecuritySettingContents extends React.Component {
                     </button>
                   </div>
                 </div>
-
                 <p className="form-text text-muted small">{t('security_settings.register_limitation_desc')}</p>
               </div>
             </div>
@@ -189,6 +177,14 @@ class LocalSecuritySettingContents extends React.Component {
                     {t('security_settings.Local.enable_password_reset_by_users')}
                   </label>
                 </div>
+                {!isMailerSetup && (
+                  <div className="alert alert-warning p-1 my-1 small d-inline-block">
+                    <span>{t('commons:alert.password_reset_please_enable_mailer')}</span>
+                    <Link href="/admin/app#mail-settings">
+                      <i className="fa fa-link"></i> {t('app_setting.mail_settings')}
+                    </Link>
+                  </div>
+                )}
                 <p className="form-text text-muted small">
                   {t('security_settings.Local.password_reset_desc')}
                 </p>
@@ -213,7 +209,9 @@ class LocalSecuritySettingContents extends React.Component {
                 {!isMailerSetup && (
                   <div className="alert alert-warning p-1 my-1 small d-inline-block">
                     <span>{t('commons:alert.please_enable_mailer')}</span>
-                    <a href="/admin/app#mail-settings"> <i className="fa fa-link"></i> {t('app_setting.mail_settings')}</a>
+                    <Link href="/admin/app#mail-settings">
+                      <i className="fa fa-link"></i> {t('app_setting.mail_settings')}
+                    </Link>
                   </div>
                 )}
                 <p className="form-text text-muted small">

+ 20 - 3
apps/app/src/components/InAppNotification/InAppNotificationElm.tsx

@@ -8,11 +8,12 @@ import { DropdownItem } from 'reactstrap';
 
 import { IInAppNotificationOpenable } from '~/client/interfaces/in-app-notification-openable';
 import { apiv3Post } from '~/client/util/apiv3-client';
+import { SupportedTargetModel } from '~/interfaces/activity';
 import { IInAppNotification, InAppNotificationStatuses } from '~/interfaces/in-app-notification';
 
 // Change the display for each targetmodel
 import PageModelNotification from './PageNotification/PageModelNotification';
-
+import UserModelNotification from './PageNotification/UserModelNotification';
 
 interface Props {
   notification: IInAppNotification & HasObjectId
@@ -40,6 +41,10 @@ const InAppNotificationElm: FC<Props> = (props: Props) => {
   };
 
   const getActionUsers = () => {
+    if (notification.targetModel === SupportedTargetModel.MODEL_USER) {
+      return notification.target.username;
+    }
+
     const latestActionUsers = notification.actionUsers.slice(0, 3);
     const latestUsers = latestActionUsers.map((user) => {
       return `@${user.name}`;
@@ -75,7 +80,6 @@ const InAppNotificationElm: FC<Props> = (props: Props) => {
         <div className="position-absolute" style={{ top: 10, left: 10 }}>
           <UserPicture user={actionUsers[1]} size="md" noTooltip />
         </div>
-
       </div>
     );
   };
@@ -139,6 +143,10 @@ const InAppNotificationElm: FC<Props> = (props: Props) => {
       actionMsg = 'commented on';
       actionIcon = 'icon-bubble';
       break;
+    case 'USER_REGISTRATION_APPROVAL_REQUEST':
+      actionMsg = 'requested registration approval';
+      actionIcon = 'icon-bubble';
+      break;
     default:
       actionMsg = '';
       actionIcon = '';
@@ -163,7 +171,7 @@ const InAppNotificationElm: FC<Props> = (props: Props) => {
         >
         </span>
         {renderActionUserPictures()}
-        {notification.targetModel === 'Page' && (
+        {notification.targetModel === SupportedTargetModel.MODEL_PAGE && (
           <PageModelNotification
             ref={notificationRef}
             notification={notification}
@@ -172,6 +180,15 @@ const InAppNotificationElm: FC<Props> = (props: Props) => {
             actionUsers={actionUsers}
           />
         )}
+        {notification.targetModel === SupportedTargetModel.MODEL_USER && (
+          <UserModelNotification
+            ref={notificationRef}
+            notification={notification}
+            actionMsg={actionMsg}
+            actionIcon={actionIcon}
+            actionUsers={actionUsers}
+          />
+        )}
       </div>
     </TagElem>
   );

+ 46 - 0
apps/app/src/components/InAppNotification/PageNotification/UserModelNotification.tsx

@@ -0,0 +1,46 @@
+import React, {
+  forwardRef, ForwardRefRenderFunction, useImperativeHandle,
+} from 'react';
+
+import { HasObjectId } from '@growi/core';
+import { useRouter } from 'next/router';
+
+import type { IInAppNotificationOpenable } from '~/client/interfaces/in-app-notification-openable';
+import type { IInAppNotification } from '~/interfaces/in-app-notification';
+
+import FormattedDistanceDate from '../../FormattedDistanceDate';
+
+const UserModelNotification: ForwardRefRenderFunction<IInAppNotificationOpenable, {
+  notification: IInAppNotification & HasObjectId
+  actionMsg: string
+  actionIcon: string
+  actionUsers: string
+}> = ({
+  notification, actionMsg, actionIcon, actionUsers,
+}, ref) => {
+  const router = useRouter();
+
+  // publish open()
+  useImperativeHandle(ref, () => ({
+    open() {
+      router.push('/admin/users');
+    },
+  }));
+
+  return (
+    <div className="p-2 overflow-hidden">
+      <div className="text-truncate">
+        <b>{actionUsers}</b> {actionMsg}
+      </div>
+      <i className={`${actionIcon} mr-2`} />
+      <FormattedDistanceDate
+        id={notification._id}
+        date={notification.createdAt}
+        isShowTooltip={false}
+        differenceForAvoidingFormat={Number.POSITIVE_INFINITY}
+      />
+    </div>
+  );
+};
+
+export default forwardRef(UserModelNotification);

+ 33 - 14
apps/app/src/components/PasswordResetRequestForm.tsx

@@ -5,10 +5,11 @@ import Link from 'next/link';
 
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { toastSuccess, toastError } from '~/client/util/toastr';
-
+import { useIsMailerSetup } from '~/stores/context';
 
 const PasswordResetRequestForm: FC = () => {
   const { t } = useTranslation();
+  const { data: isMailerSetup } = useIsMailerSetup();
   const [email, setEmail] = useState('');
 
   const changeEmail = useCallback((inputValue) => {
@@ -33,20 +34,38 @@ const PasswordResetRequestForm: FC = () => {
 
   return (
     <form onSubmit={sendPasswordResetRequestMail}>
-      <h3>{ t('forgot_password.password_reset_request_desc') }</h3>
-      <div className="form-group">
-        <div className="input-group">
-          <input name="email" placeholder="E-mail Address" className="form-control" type="email" onChange={e => changeEmail(e.target.value)} />
+      {!isMailerSetup ? (
+        <div className="alert alert-danger">
+          {t('forgot_password.please_enable_mailer_alert')}
         </div>
-      </div>
-      <div className="form-group">
-        <button
-          className="btn btn-lg btn-primary btn-block"
-          type="submit"
-        >
-          {t('forgot_password.send')}
-        </button>
-      </div>
+      ) : (
+        <>
+          <h1><i className="icon-lock large"></i></h1>
+          <h1 className="text-center">{ t('forgot_password.forgot_password') }</h1>
+          <h3>{t('forgot_password.password_reset_request_desc')}</h3>
+          <div className="form-group">
+            <div className="input-group">
+              <input
+                name="email"
+                placeholder="E-mail Address"
+                className="form-control"
+                type="email"
+                disabled={!isMailerSetup}
+                onChange={e => changeEmail(e.target.value)}
+              />
+            </div>
+          </div>
+          <div className="form-group">
+            <button
+              className="btn btn-lg btn-primary btn-block"
+              type="submit"
+              disabled={!isMailerSetup}
+            >
+              {t('forgot_password.send')}
+            </button>
+          </div>
+        </>
+      )}
       <Link href='/login' prefetch={false}>
         <i className="icon-login mr-1" />{t('forgot_password.return_to_login')}
       </Link>

+ 6 - 0
apps/app/src/interfaces/activity.ts

@@ -4,10 +4,12 @@ import { IUser } from './user';
 
 // Model
 const MODEL_PAGE = 'Page';
+const MODEL_USER = 'User';
 const MODEL_COMMENT = 'Comment';
 
 // Action
 const ACTION_UNSETTLED = 'UNSETTLED';
+const ACTION_USER_REGISTRATION_APPROVAL_REQUEST = 'USER_REGISTRATION_APPROVAL_REQUEST';
 const ACTION_USER_REGISTRATION_SUCCESS = 'USER_REGISTRATION_SUCCESS';
 const ACTION_USER_LOGIN_WITH_LOCAL = 'USER_LOGIN_WITH_LOCAL';
 const ACTION_USER_LOGIN_WITH_LDAP = 'USER_LOGIN_WITH_LDAP';
@@ -162,6 +164,7 @@ const ACTION_ADMIN_SEARCH_INDICES_REBUILD = 'ADMIN_SEARCH_INDICES_REBUILD';
 
 export const SupportedTargetModel = {
   MODEL_PAGE,
+  MODEL_USER,
 } as const;
 
 export const SupportedEventModel = {
@@ -182,6 +185,7 @@ export const SupportedActionCategory = {
 
 export const SupportedAction = {
   ACTION_UNSETTLED,
+  ACTION_USER_REGISTRATION_APPROVAL_REQUEST,
   ACTION_USER_REGISTRATION_SUCCESS,
   ACTION_USER_LOGIN_WITH_LOCAL,
   ACTION_USER_LOGIN_WITH_LDAP,
@@ -349,6 +353,7 @@ export const EssentialActionGroup = {
   ACTION_PAGE_RECURSIVELY_DELETE_COMPLETELY,
   ACTION_PAGE_RECURSIVELY_REVERT,
   ACTION_COMMENT_CREATE,
+  ACTION_USER_REGISTRATION_APPROVAL_REQUEST,
 } as const;
 
 export const ActionGroupSize = {
@@ -375,6 +380,7 @@ export const SmallActionGroup = {
 // SmallActionGroup + Action by all General Users - PAGE_VIEW
 export const MediumActionGroup = {
   ...SmallActionGroup,
+  ACTION_USER_REGISTRATION_APPROVAL_REQUEST,
   ACTION_USER_REGISTRATION_SUCCESS,
   ACTION_USER_FOGOT_PASSWORD,
   ACTION_USER_RESET_PASSWORD,

+ 8 - 4
apps/app/src/interfaces/in-app-notification.ts

@@ -1,5 +1,7 @@
 import type { IPageSnapshot } from '~/models/serializers/in-app-notification-snapshot/page';
+import type { IUserSnapshot } from '~/models/serializers/in-app-notification-snapshot/user';
 
+import { SupportedTargetModelType, SupportedActionType } from './activity';
 import { IPage } from './page';
 import { IUser } from './user';
 
@@ -9,16 +11,18 @@ export enum InAppNotificationStatuses {
   STATUS_OPENED = 'OPENED',
 }
 
+// TODO: do not use any type
+// https://redmine.weseek.co.jp/issues/120632
 export interface IInAppNotification {
   user: IUser
-  targetModel: 'Page'
-  target: IPage
-  action: 'COMMENT' | 'LIKE'
+  targetModel: SupportedTargetModelType
+  target: any
+  action: SupportedActionType
   status: InAppNotificationStatuses
   actionUsers: IUser[]
   createdAt: Date
   snapshot: string
-  parsedSnapshot?: IPageSnapshot
+  parsedSnapshot?: any
 }
 
 /*

+ 15 - 0
apps/app/src/models/serializers/in-app-notification-snapshot/user.ts

@@ -0,0 +1,15 @@
+import type { IUser } from '~/interfaces/user';
+
+export interface IUserSnapshot {
+  username: string
+}
+
+export const stringifySnapshot = (user: IUser): string => {
+  return JSON.stringify({
+    username: user.username,
+  });
+};
+
+export const parseSnapshot = (snapshot: string): IUserSnapshot => {
+  return JSON.parse(snapshot);
+};

+ 20 - 7
apps/app/src/pages/forgot-password.page.tsx

@@ -1,18 +1,24 @@
 import React from 'react';
 
 import { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
-import { useTranslation } from 'next-i18next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 
+import { CrowiRequest } from '~/interfaces/crowi-request';
+import { useIsMailerSetup } from '~/stores/context';
+
 import {
   CommonProps, getNextI18NextConfig, getServerSideCommonProps,
 } from './utils/commons';
 
 const PasswordResetRequestForm = dynamic(() => import('~/components/PasswordResetRequestForm'), { ssr: false });
 
-const ForgotPasswordPage: NextPage = () => {
-  const { t } = useTranslation();
+type Props = CommonProps & {
+  isMailerSetup: boolean,
+};
+
+const ForgotPasswordPage: NextPage<Props> = (props: Props) => {
+  useIsMailerSetup(props.isMailerSetup);
 
   return (
     <div id="main" className="main">
@@ -21,8 +27,6 @@ const ForgotPasswordPage: NextPage = () => {
           <div className="row justify-content-md-center">
             <div className="col-md-6 mt-5">
               <div className="text-center">
-                <h1><i className="icon-lock large"></i></h1>
-                <h1 className="text-center">{ t('forgot_password.forgot_password') }</h1>
                 <PasswordResetRequestForm />
               </div>
             </div>
@@ -34,11 +38,19 @@ const ForgotPasswordPage: NextPage = () => {
 };
 
 // eslint-disable-next-line max-len
-async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: CommonProps, namespacesRequired?: string[] | undefined): Promise<void> {
+async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
   const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
   props._nextI18Next = nextI18NextConfig._nextI18Next;
 }
 
+const injectServerConfigurations = async(context: GetServerSidePropsContext, props: Props): Promise<void> => {
+  const req: CrowiRequest = context.req as CrowiRequest;
+  const { crowi } = req;
+  const { mailService } = crowi;
+
+  props.isMailerSetup = mailService.isMailerSetup;
+};
+
 export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
   const result = await getServerSideCommonProps(context);
 
@@ -48,8 +60,9 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
     throw new Error('invalid getSSP result');
   }
 
-  const props: CommonProps = result.props as CommonProps;
+  const props: Props = result.props as Props;
 
+  injectServerConfigurations(context, props);
   await injectNextI18NextConfigurations(context, props, ['translation', 'commons']);
 
   return {

+ 19 - 8
apps/app/src/server/routes/login.js

@@ -1,4 +1,4 @@
-import { SupportedAction } from '~/interfaces/activity';
+import { SupportedAction, SupportedTargetModel } from '~/interfaces/activity';
 import loggerFactory from '~/utils/logger';
 
 // disable all of linting
@@ -10,7 +10,7 @@ module.exports = function(crowi, app) {
   const path = require('path');
   const User = crowi.model('User');
   const {
-    configManager, appService, aclService, mailService,
+    configManager, appService, aclService, mailService, activityService,
   } = crowi;
   const activityEvent = crowi.event('activity');
 
@@ -42,12 +42,28 @@ module.exports = function(crowi, app) {
       .forEach(result => logger.error(result.reason));
   }
 
+  async function sendNotificationToAllAdmins(user) {
+    const adminUsers = await User.findAdmins();
+    const activity = await activityService.createActivity({
+      action: SupportedAction.ACTION_USER_REGISTRATION_APPROVAL_REQUEST,
+      target: user,
+      targetModel: SupportedTargetModel.MODEL_USER,
+    });
+    await activityEvent.emit('updated', activity, user, adminUsers);
+    return;
+  }
+
   const registerSuccessHandler = async function(req, res, userData, registrationMode) {
     const parameters = { action: SupportedAction.ACTION_USER_REGISTRATION_SUCCESS };
     activityEvent.emit('update', res.locals.activity._id, parameters);
 
+    const isMailerSetup = mailService.isMailerSetup ?? false;
+
     if (registrationMode === aclService.labels.SECURITY_REGISTRATION_MODE_RESTRICTED) {
-      await sendEmailToAllAdmins(userData);
+      sendNotificationToAllAdmins(userData);
+      if (isMailerSetup) {
+        await sendEmailToAllAdmins(userData);
+      }
       return res.apiv3({});
     }
 
@@ -142,11 +158,6 @@ module.exports = function(crowi, app) {
       }
 
       const registrationMode = configManager.getConfig('crowi', 'security:registrationMode');
-      const isMailerSetup = mailService.isMailerSetup ?? false;
-
-      if (!isMailerSetup && registrationMode === aclService.labels.SECURITY_REGISTRATION_MODE_RESTRICTED) {
-        return res.apiv3Err(['message.email_settings_is_not_setup'], 403);
-      }
 
       User.createUserByEmailAndPassword(name, username, email, password, undefined, async(err, userData) => {
         if (err) {

+ 19 - 8
apps/app/src/server/service/in-app-notification.ts

@@ -6,7 +6,8 @@ import { Types } from 'mongoose';
 
 import { AllEssentialActions, SupportedAction } from '~/interfaces/activity';
 import { InAppNotificationStatuses, PaginateResult } from '~/interfaces/in-app-notification';
-import { stringifySnapshot } from '~/models/serializers/in-app-notification-snapshot/page';
+import * as pageSerializers from '~/models/serializers/in-app-notification-snapshot/page';
+import * as userSerializers from '~/models/serializers/in-app-notification-snapshot/user';
 import { ActivityDocument } from '~/server/models/activity';
 import {
   InAppNotification,
@@ -17,7 +18,6 @@ import Subscription from '~/server/models/subscription';
 import loggerFactory from '~/utils/logger';
 
 import Crowi from '../crowi';
-import { PageDocument } from '../models/page';
 import { RoomPrefix, getRoomNameWithId } from '../util/socket-io-helpers';
 
 
@@ -51,11 +51,13 @@ export default class InAppNotificationService {
   }
 
   initActivityEventListeners(): void {
-    this.activityEvent.on('updated', async(activity: ActivityDocument, target: IPage, descendantsSubscribedUsers?: Ref<IUser>[]) => {
+    // TODO: do not use any type
+    // https://redmine.weseek.co.jp/issues/120632
+    this.activityEvent.on('updated', async(activity: ActivityDocument, target: any, users?: Ref<IUser>[]) => {
       try {
         const shouldNotification = activity != null && target != null && (AllEssentialActions as ReadonlyArray<string>).includes(activity.action);
         if (shouldNotification) {
-          await this.createInAppNotification(activity, target, descendantsSubscribedUsers);
+          await this.createInAppNotification(activity, target, users);
         }
       }
       catch (err) {
@@ -199,9 +201,18 @@ export default class InAppNotificationService {
     return;
   };
 
-  createInAppNotification = async function(activity: ActivityDocument, target: IPage, descendantsSubscribedUsers?: Ref<IUser>[]): Promise<void> {
+  // TODO: do not use any type
+  // https://redmine.weseek.co.jp/issues/120632
+  createInAppNotification = async function(activity: ActivityDocument, target, users?: Ref<IUser>[]): Promise<void> {
+    if (activity.action === SupportedAction.ACTION_USER_REGISTRATION_APPROVAL_REQUEST) {
+      const snapshot = userSerializers.stringifySnapshot(target);
+      await this.upsertByActivity(users, activity, snapshot);
+      await this.emitSocketIo(users);
+      return;
+    }
+
     const shouldNotification = activity != null && target != null && (AllEssentialActions as ReadonlyArray<string>).includes(activity.action);
-    const snapshot = stringifySnapshot(target);
+    const snapshot = pageSerializers.stringifySnapshot(target);
     if (shouldNotification) {
       let mentionedUsers: IUser[] = [];
       if (activity.action === SupportedAction.ACTION_COMMENT_CREATE) {
@@ -209,9 +220,9 @@ export default class InAppNotificationService {
       }
       const notificationTargetUsers = await activity?.getNotificationTargetUsers();
       let notificationDescendantsUsers = [];
-      if (descendantsSubscribedUsers != null) {
+      if (users != null) {
         const User = this.crowi.model('User');
-        const descendantsUsers = descendantsSubscribedUsers.filter(item => (item.toString() !== activity.user._id.toString()));
+        const descendantsUsers = users.filter(item => (item.toString() !== activity.user._id.toString()));
         notificationDescendantsUsers = await User.find({
           _id: { $in: descendantsUsers },
           status: User.STATUS_ACTIVE,

+ 13 - 2
apps/app/src/stores/in-app-notification.ts

@@ -1,7 +1,9 @@
 import useSWR, { SWRConfiguration, SWRResponse } from 'swr';
 
+import { SupportedTargetModel } from '~/interfaces/activity';
 import type { InAppNotificationStatuses, IInAppNotification, PaginateResult } from '~/interfaces/in-app-notification';
-import { parseSnapshot } from '~/models/serializers/in-app-notification-snapshot/page';
+import * as pageSerializers from '~/models/serializers/in-app-notification-snapshot/page';
+import * as userSerializers from '~/models/serializers/in-app-notification-snapshot/user';
 import loggerFactory from '~/utils/logger';
 
 import { apiv3Get } from '../client/util/apiv3-client';
@@ -23,7 +25,16 @@ export const useSWRxInAppNotifications = <Data, Error>(
       const inAppNotificationPaginateResult = response.data as inAppNotificationPaginateResult;
       inAppNotificationPaginateResult.docs.forEach((doc) => {
         try {
-          doc.parsedSnapshot = parseSnapshot(doc.snapshot as string);
+          switch (doc.targetModel) {
+            case SupportedTargetModel.MODEL_PAGE:
+              doc.parsedSnapshot = pageSerializers.parseSnapshot(doc.snapshot);
+              break;
+            case SupportedTargetModel.MODEL_USER:
+              doc.parsedSnapshot = userSerializers.parseSnapshot(doc.snapshot);
+              break;
+            default:
+              throw new Error(`No serializer found for targetModel: ${doc.targetModel}`);
+          }
         }
         catch (err) {
           logger.warn('Failed to parse snapshot', err);