Parcourir la source

Merge pull request #7581 from weseek/imprv/119788-120045-can-use-sing-up-registration-setting-without-mail-setting

imprv: Can use registration approval request without mail settings
Yuki Takei il y a 3 ans
Parent
commit
3284c1f2a2

+ 1 - 1
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)"

+ 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);

+ 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);
+};

+ 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);