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

Merge branch 'feat/ldap-group-sync' into feat/123277-125405-external-user-group-index-ui

Futa Arai 2 лет назад
Родитель
Сommit
39d4e7324d

+ 2 - 2
apps/app/src/components/Me/DisassociateModal.tsx

@@ -9,13 +9,13 @@ import {
 } from 'reactstrap';
 
 import { toastSuccess, toastError } from '~/client/util/toastr';
-import { IExternalAccount } from '~/interfaces/external-account';
+import { IExternalAccountHasId } from '~/interfaces/external-account';
 import { usePersonalSettings, useSWRxPersonalExternalAccounts } from '~/stores/personal-settings';
 
 type Props = {
   isOpen: boolean,
   onClose: () => void,
-  accountForDisassociate: IExternalAccount,
+  accountForDisassociate: IExternalAccountHasId,
 }
 
 

+ 48 - 0
apps/app/src/features/external-user-group/server/models/external-user-group-relation.integ.ts

@@ -0,0 +1,48 @@
+import mongoose from 'mongoose';
+
+import ExternalUserGroup from './external-user-group';
+import ExternalUserGroupRelation from './external-user-group-relation';
+
+// TODO: use actual user model after ~/server/models/user.js becomes importable in vitest
+// ref: https://github.com/vitest-dev/vitest/issues/846
+const userSchema = new mongoose.Schema({
+  name: { type: String },
+  username: { type: String, required: true, unique: true },
+  email: { type: String, unique: true, sparse: true },
+}, {
+  timestamps: true,
+});
+const User = mongoose.model('User', userSchema);
+
+describe('ExternalUserGroupRelation model', () => {
+  const userId1 = new mongoose.Types.ObjectId();
+  const groupId1 = new mongoose.Types.ObjectId();
+  const groupId2 = new mongoose.Types.ObjectId();
+
+  describe('createRelations', () => {
+    let user1;
+
+    beforeAll(async() => {
+      user1 = await User.create({
+        _id: userId1, name: 'user1', username: 'user1', email: 'user1@example.com',
+      });
+      await ExternalUserGroup.insertMany(
+        {
+          _id: groupId1, name: 'test group 1', externalId: 'testExternalId', provider: 'testProvider',
+        },
+        {
+          _id: groupId2, name: 'test group 2', externalId: 'testExternalId', provider: 'testProvider',
+        },
+      );
+    });
+
+    it('creates relation for user', async() => {
+      await ExternalUserGroupRelation.createRelations([groupId1, groupId2], user1);
+      const relations = await ExternalUserGroupRelation.find();
+      const idCombinations = relations.map((relation) => {
+        return [relation.relatedGroup, relation.relatedUser];
+      });
+      expect(idCombinations).toStrictEqual([[groupId1, userId1], [groupId2, userId1]]);
+    });
+  });
+});

+ 73 - 0
apps/app/src/features/external-user-group/server/models/external-user-group.integ.ts

@@ -0,0 +1,73 @@
+import mongoose from 'mongoose';
+
+import ExternalUserGroup from './external-user-group';
+
+describe('ExternalUserGroup model', () => {
+  describe('findAndUpdateOrCreateGroup', () => {
+    const groupId = new mongoose.Types.ObjectId();
+    beforeAll(async() => {
+      await ExternalUserGroup.create({
+        _id: groupId, name: 'test group', externalId: 'testExternalId', provider: 'testProvider',
+      });
+    });
+
+    it('finds and updates existing group', async() => {
+      const group = await ExternalUserGroup.findAndUpdateOrCreateGroup('edited test group', 'testExternalId', 'testProvider');
+      expect(group.id).toBe(groupId.toString());
+      expect(group.name).toBe('edited test group');
+    });
+
+    it('creates new group with parent', async() => {
+      expect(await ExternalUserGroup.count()).toBe(1);
+      const newGroup = await ExternalUserGroup.findAndUpdateOrCreateGroup(
+        'new group', 'nonExistentExternalId', 'testProvider', undefined, groupId.toString(),
+      );
+      expect(await ExternalUserGroup.count()).toBe(2);
+      expect(newGroup.parent.toString()).toBe(groupId.toString());
+    });
+
+    it('throws error when parent does not exist', async() => {
+      try {
+        await ExternalUserGroup.findAndUpdateOrCreateGroup(
+          'new group', 'nonExistentExternalId', 'testProvider', undefined, new mongoose.Types.ObjectId(),
+        );
+      }
+      catch (e) {
+        expect(e.message).toBe('Parent does not exist.');
+      }
+    });
+  });
+
+  describe('findGroupsWithAncestorsRecursively', () => {
+    const childGroupId = new mongoose.Types.ObjectId();
+    const parentGroupId = new mongoose.Types.ObjectId();
+    const grandParentGroupId = new mongoose.Types.ObjectId();
+
+    beforeAll(async() => {
+      await ExternalUserGroup.deleteMany();
+      await ExternalUserGroup.create({
+        _id: grandParentGroupId, name: 'grand parent group', externalId: 'grandParentExternalId', provider: 'testProvider',
+      });
+      await ExternalUserGroup.create({
+        _id: parentGroupId, name: 'parent group', externalId: 'parentExternalId', provider: 'testProvider', parent: grandParentGroupId,
+      });
+      await ExternalUserGroup.create({
+        _id: childGroupId, name: 'child group', externalId: 'childExternalId', provider: 'testProvider', parent: parentGroupId,
+      });
+    });
+
+    it('finds ancestors for child', async() => {
+      const childGroup = await ExternalUserGroup.findById(childGroupId);
+      const groups = await ExternalUserGroup.findGroupsWithAncestorsRecursively(childGroup);
+      const groupIds = groups.map(group => group.id);
+      expect(groupIds).toStrictEqual([grandParentGroupId.toString(), parentGroupId.toString(), childGroupId.toString()]);
+    });
+
+    it('finds ancestors for child, excluding child', async() => {
+      const childGroup = await ExternalUserGroup.findById(childGroupId);
+      const groups = await ExternalUserGroup.findGroupsWithAncestorsRecursively(childGroup, []);
+      const groupIds = groups.map(group => group.id);
+      expect(groupIds).toStrictEqual([grandParentGroupId.toString(), parentGroupId.toString()]);
+    });
+  });
+});

+ 2 - 2
apps/app/src/features/external-user-group/server/models/external-user-group.ts

@@ -29,13 +29,13 @@ schema.plugin(mongoosePaginate);
 /**
  * Find group that has specified externalId and update, or create one if it doesn't exist.
  * @param name ExternalUserGroup name
- * @param name ExternalUserGroup description
  * @param name ExternalUserGroup externalId
  * @param name ExternalUserGroup provider
+ * @param name ExternalUserGroup description
  * @param name ExternalUserGroup parentId
  * @returns ExternalUserGroupDocument[]
  */
-schema.statics.findAndUpdateOrCreateGroup = async function(name, description, externalId, provider, parentId) {
+schema.statics.findAndUpdateOrCreateGroup = async function(name: string, externalId: string, provider: string, description?: string, parentId?: string) {
   // create without parent
   if (parentId == null) {
     return this.findOneAndUpdate({ externalId }, { name, description, provider }, { upsert: true, new: true });

+ 4 - 5
apps/app/src/features/external-user-group/server/service/external-user-group-sync-service.ts

@@ -1,6 +1,5 @@
-import mongoose from 'mongoose';
-
 import { IUserHasId } from '~/interfaces/user';
+import ExternalAccount from '~/server/models/external-account';
 import { excludeTestIdsFromTargetIds } from '~/server/util/compare-objectId';
 
 import { configManager } from '../../../../server/service/config-manager';
@@ -61,7 +60,7 @@ abstract class ExternalUserGroupSyncService {
   */
   async createUpdateExternalUserGroup(node: ExternalUserGroupTreeNode, parentId?: string): Promise<IExternalUserGroupHasId> {
     const externalUserGroup = await ExternalUserGroup.findAndUpdateOrCreateGroup(
-      node.name, node.description, node.id, this.groupProviderType, parentId,
+      node.name, node.id, this.groupProviderType, node.description, parentId,
     );
     await Promise.all(node.userInfos.map((userInfo) => {
       return (async() => {
@@ -92,7 +91,6 @@ abstract class ExternalUserGroupSyncService {
    */
   async getMemberUser(userInfo: ExternalUserInfo): Promise<IUserHasId | null> {
     const autoGenerateUserOnGroupSync = configManager?.getConfig('crowi', `external-user-group:${this.groupProviderType}:autoGenerateUserOnGroupSync`);
-    const ExternalAccount = mongoose.model('ExternalAccount');
 
     const getExternalAccount = async() => {
       if (autoGenerateUserOnGroupSync && externalAccountService != null) {
@@ -106,7 +104,8 @@ abstract class ExternalUserGroupSyncService {
     const externalAccount = await getExternalAccount();
 
     if (externalAccount != null) {
-      return externalAccount.getPopulatedUser();
+      await externalAccount.populate('user');
+      return externalAccount.user;
     }
     return null;
   }

+ 4 - 3
apps/app/src/interfaces/external-account.ts

@@ -1,11 +1,12 @@
-import { Ref } from '@growi/core';
+import { HasObjectId, Ref } from '@growi/core';
 
 import { IUser } from '~/interfaces/user';
 
 
-export type IExternalAccount<ID = string> = {
-  _id: ID,
+export type IExternalAccount = {
   providerType: string,
   accountId: string,
   user: Ref<IUser>,
 }
+
+export type IExternalAccountHasId = IExternalAccount & HasObjectId

+ 0 - 187
apps/app/src/server/models/external-account.js

@@ -1,187 +0,0 @@
-// disable no-return-await for model functions
-/* eslint-disable no-return-await */
-import { NullUsernameToBeRegisteredError } from '~/server/models/errors';
-
-const debug = require('debug')('growi:models:external-account');
-const mongoose = require('mongoose');
-const mongoosePaginate = require('mongoose-paginate-v2');
-const uniqueValidator = require('mongoose-unique-validator');
-
-const ObjectId = mongoose.Schema.Types.ObjectId;
-
-/*
- * define schema
- */
-const schema = new mongoose.Schema({
-  providerType: { type: String, required: true },
-  accountId: { type: String, required: true },
-  user: { type: ObjectId, ref: 'User', required: true },
-}, {
-  timestamps: { createdAt: true, updatedAt: false },
-});
-// compound index
-schema.index({ providerType: 1, accountId: 1 }, { unique: true });
-// apply plugins
-schema.plugin(mongoosePaginate);
-schema.plugin(uniqueValidator);
-
-/**
- * The Exception class thrown when User.username is duplicated when creating user
- *
- * @class DuplicatedUsernameException
- */
-class DuplicatedUsernameException {
-
-  constructor(message, user) {
-    this.name = this.constructor.name;
-    this.message = message;
-    this.user = user;
-  }
-
-}
-
-/**
- * ExternalAccount Class
- *
- * @class ExternalAccount
- */
-class ExternalAccount {
-
-  /**
-   * limit items num for pagination
-   *
-   * @readonly
-   * @static
-   * @memberof ExternalAccount
-   */
-  static get DEFAULT_LIMIT() {
-    return 50;
-  }
-
-  static set crowi(crowi) {
-    this._crowi = crowi;
-  }
-
-  static get crowi() {
-    return this._crowi;
-  }
-
-  /**
-   * get the populated user entity
-   *
-   * @returns Promise<User>
-   * @memberof ExternalAccount
-   */
-  getPopulatedUser() {
-    return this.populate('user')
-      .then((account) => {
-        return account.user;
-      });
-  }
-
-  /**
-   * find an account or register if not found
-   *
-   * @static
-   * @param {string} providerType
-   * @param {string} accountId
-   * @param {object} usernameToBeRegistered the username of User entity that will be created when accountId is not found
-   * @param {object} nameToBeRegistered the name of User entity that will be created when accountId is not found
-   * @param {object} mailToBeRegistered the mail of User entity that will be created when accountId is not found
-   * @param {boolean} isSameUsernameTreatedAsIdenticalUser
-   * @param {boolean} isSameEmailTreatedAsIdenticalUser
-   * @returns {Promise<ExternalAccount>}
-   * @memberof ExternalAccount
-   */
-  static findOrRegister(providerType, accountId,
-      usernameToBeRegistered, nameToBeRegistered, mailToBeRegistered,
-      isSameUsernameTreatedAsIdenticalUser, isSameEmailTreatedAsIdenticalUser) {
-    //
-    return this.findOne({ providerType, accountId })
-      .then((account) => {
-        // ExternalAccount is found
-        if (account != null) {
-          debug(`ExternalAccount '${accountId}' is found `, account);
-          return account;
-        }
-
-        if (usernameToBeRegistered == null) {
-          throw new NullUsernameToBeRegisteredError('username_should_not_be_null');
-        }
-
-        const User = ExternalAccount.crowi.model('User');
-
-        let promise = User.findOne({ username: usernameToBeRegistered });
-        if (isSameUsernameTreatedAsIdenticalUser && isSameEmailTreatedAsIdenticalUser) {
-          promise = promise
-            .then((user) => {
-              if (user == null) { return User.findOne({ email: mailToBeRegistered }) }
-              return user;
-            });
-        }
-        else if (isSameEmailTreatedAsIdenticalUser) {
-          promise = User.findOne({ email: mailToBeRegistered });
-        }
-
-        return promise
-          .then((user) => {
-            // when the User that have the same `username` exists
-            if (user != null) {
-              throw new DuplicatedUsernameException(`User '${usernameToBeRegistered}' already exists`, user);
-            }
-            if (nameToBeRegistered == null) {
-              // eslint-disable-next-line no-param-reassign
-              nameToBeRegistered = '';
-            }
-
-            // create a new User with STATUS_ACTIVE
-            debug(`ExternalAccount '${accountId}' is not found, it is going to be registered.`);
-            return User.createUser(nameToBeRegistered, usernameToBeRegistered, mailToBeRegistered, undefined, undefined, User.STATUS_ACTIVE);
-          })
-          .then((newUser) => {
-            return this.associate(providerType, accountId, newUser);
-          });
-      });
-  }
-
-  /**
-   * Create ExternalAccount document and associate to existing User
-   *
-   * @param {string} providerType
-   * @param {string} accountId
-   * @param {object} user
-   */
-  static associate(providerType, accountId, user) {
-    return this.create({ providerType, accountId, user: user._id });
-  }
-
-  /**
-   * find all entities with pagination
-   *
-   * @see https://github.com/edwardhotchkiss/mongoose-paginate
-   *
-   * @static
-   * @param {any} opts mongoose-paginate options object
-   * @returns {Promise<any>} mongoose-paginate result object
-   * @memberof ExternalAccount
-   */
-  static findAllWithPagination(opts) {
-    const query = {};
-    const options = Object.assign({ populate: 'user' }, opts);
-    if (options.sort == null) {
-      options.sort = { accountId: 1, createdAt: 1 };
-    }
-    if (options.limit == null) {
-      options.limit = ExternalAccount.DEFAULT_LIMIT;
-    }
-
-    return this.paginate(query, options);
-  }
-
-}
-
-module.exports = function(crowi) {
-  ExternalAccount.crowi = crowi;
-  schema.loadClass(ExternalAccount);
-  return mongoose.model('ExternalAccount', schema);
-};

+ 153 - 0
apps/app/src/server/models/external-account.ts

@@ -0,0 +1,153 @@
+// disable no-return-await for model functions
+/* eslint-disable no-return-await */
+import { IUserHasId } from '@growi/core';
+import { Schema, Model, Document } from 'mongoose';
+
+import { IExternalAccount, IExternalAccountHasId } from '~/interfaces/external-account';
+import { NullUsernameToBeRegisteredError } from '~/server/models/errors';
+
+import { getOrCreateModel } from '../util/mongoose-utils';
+
+const debug = require('debug')('growi:models:external-account');
+const mongoose = require('mongoose');
+const mongoosePaginate = require('mongoose-paginate-v2');
+const uniqueValidator = require('mongoose-unique-validator');
+
+const ObjectId = mongoose.Schema.Types.ObjectId;
+
+interface ExternalAccountDocument extends IExternalAccount, Document {}
+
+export interface ExternalAccountModel extends Model<ExternalAccountDocument> {
+  [x:string]: any, // for old methods
+}
+
+const schema = new Schema<ExternalAccountDocument, ExternalAccountModel>({
+  providerType: { type: String, required: true },
+  accountId: { type: String, required: true },
+  user: { type: ObjectId, ref: 'User', required: true },
+}, {
+  timestamps: { createdAt: true, updatedAt: false },
+});
+// compound index
+schema.index({ providerType: 1, accountId: 1 }, { unique: true });
+// apply plugins
+schema.plugin(mongoosePaginate);
+schema.plugin(uniqueValidator);
+
+/**
+ * limit items num for pagination
+ */
+const DEFAULT_LIMIT = 50;
+
+/**
+ * The Exception class thrown when User.username is duplicated when creating user
+ *
+ * @class DuplicatedUsernameException
+ */
+class DuplicatedUsernameException {
+
+  name: string;
+
+  message: string;
+
+  user: IUserHasId;
+
+  constructor(message, user) {
+    this.name = this.constructor.name;
+    this.message = message;
+    this.user = user;
+  }
+
+}
+
+/**
+ * find an account or register if not found
+ */
+schema.statics.findOrRegister = function(
+    isSameUsernameTreatedAsIdenticalUser: boolean,
+    isSameEmailTreatedAsIdenticalUser: boolean,
+    providerType: string,
+    accountId: string,
+    usernameToBeRegistered?: string,
+    nameToBeRegistered?: string,
+    mailToBeRegistered?: string,
+): Promise<IExternalAccountHasId> {
+//
+  return this.findOne({ providerType, accountId })
+    .then((account) => {
+    // ExternalAccount is found
+      if (account != null) {
+        debug(`ExternalAccount '${accountId}' is found `, account);
+        return account;
+      }
+
+      if (usernameToBeRegistered == null) {
+        throw new NullUsernameToBeRegisteredError('username_should_not_be_null');
+      }
+
+      const User = mongoose.model('User');
+
+      let promise = User.findOne({ username: usernameToBeRegistered });
+      if (isSameUsernameTreatedAsIdenticalUser && isSameEmailTreatedAsIdenticalUser) {
+        promise = promise
+          .then((user) => {
+            if (user == null) { return User.findOne({ email: mailToBeRegistered }) }
+            return user;
+          });
+      }
+      else if (isSameEmailTreatedAsIdenticalUser) {
+        promise = User.findOne({ email: mailToBeRegistered });
+      }
+
+      return promise
+        .then((user) => {
+        // when the User that have the same `username` exists
+          if (user != null) {
+            throw new DuplicatedUsernameException(`User '${usernameToBeRegistered}' already exists`, user);
+          }
+          if (nameToBeRegistered == null) {
+          // eslint-disable-next-line no-param-reassign
+            nameToBeRegistered = '';
+          }
+
+          // create a new User with STATUS_ACTIVE
+          debug(`ExternalAccount '${accountId}' is not found, it is going to be registered.`);
+          return User.createUser(nameToBeRegistered, usernameToBeRegistered, mailToBeRegistered, undefined, undefined, User.STATUS_ACTIVE);
+        })
+        .then((newUser) => {
+          return this.associate(providerType, accountId, newUser);
+        });
+    });
+};
+
+/**
+ * Create ExternalAccount document and associate to existing User
+ */
+schema.statics.associate = function(providerType: string, accountId: string, user: IUserHasId) {
+  return this.create({ providerType, accountId, user: user._id });
+};
+
+/**
+ * find all entities with pagination
+ *
+ * @see https://github.com/edwardhotchkiss/mongoose-paginate
+ *
+ * @static
+ * @param {any} opts mongoose-paginate options object
+ * @returns {Promise<any>} mongoose-paginate result object
+ * @memberof ExternalAccount
+ */
+schema.statics.findAllWithPagination = function(opts) {
+  const query = {};
+  const options = Object.assign({ populate: 'user' }, opts);
+  if (options.sort == null) {
+    options.sort = { accountId: 1, createdAt: 1 };
+  }
+  if (options.limit == null) {
+    options.limit = DEFAULT_LIMIT;
+  }
+
+  return this.paginate(query, options);
+};
+
+export default getOrCreateModel<ExternalAccountDocument, ExternalAccountModel>('ExternalAccount', schema);

+ 0 - 1
apps/app/src/server/models/index.js

@@ -6,7 +6,6 @@ module.exports = {
   // PageArchive: require('./page-archive'),
   PageTagRelation: require('./page-tag-relation'),
   User: require('./user'),
-  ExternalAccount: require('./external-account'),
   Revision: require('./revision'),
   Bookmark: require('./bookmark'),
   Comment: require('./comment'),

+ 2 - 1
apps/app/src/server/routes/apiv3/personal-setting.js

@@ -10,6 +10,7 @@ import loggerFactory from '~/utils/logger';
 import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 import EditorSettings from '../../models/editor-settings';
+import ExternalAccount from '../../models/external-account';
 import InAppNotificationSettings from '../../models/in-app-notification-settings';
 
 
@@ -75,7 +76,7 @@ module.exports = (crowi) => {
   const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
   const addActivity = generateAddActivityMiddleware(crowi);
 
-  const { User, ExternalAccount } = crowi.models;
+  const { User } = crowi.models;
 
   const activityEvent = crowi.event('activity');
 

+ 1 - 1
apps/app/src/server/routes/apiv3/users.js

@@ -2,6 +2,7 @@ import { ErrorV3 } from '@growi/core';
 
 import { SupportedAction } from '~/interfaces/activity';
 import Activity from '~/server/models/activity';
+import ExternalAccount from '~/server/models/external-account';
 import { configManager } from '~/server/service/config-manager';
 import loggerFactory from '~/utils/logger';
 
@@ -87,7 +88,6 @@ module.exports = (crowi) => {
   const {
     User,
     Page,
-    ExternalAccount,
     UserGroupRelation,
   } = crowi.models;
 

+ 5 - 5
apps/app/src/server/routes/login-passport.js

@@ -224,7 +224,7 @@ module.exports = function(crowi, app) {
       return next(new ErrorV3('message.external_account_not_exist'));
     }
 
-    const user = await externalAccount.getPopulatedUser();
+    const user = (await externalAccount.populate('user')).user;
 
     // login
     await req.logIn(user, (err) => {
@@ -392,7 +392,7 @@ module.exports = function(crowi, app) {
       return next(new ExternalAccountLoginError('message.sign_in_failure'));
     }
 
-    const user = await externalAccount.getPopulatedUser();
+    const user = (await externalAccount.populate('user')).user;
 
     // login
     req.logIn(user, async(err) => {
@@ -435,7 +435,7 @@ module.exports = function(crowi, app) {
       return next(new ExternalAccountLoginError('message.sign_in_failure'));
     }
 
-    const user = await externalAccount.getPopulatedUser();
+    const user = (await externalAccount.populate('user')).user;
 
     // login
     req.logIn(user, async(err) => {
@@ -486,7 +486,7 @@ module.exports = function(crowi, app) {
     }
 
     // login
-    const user = await externalAccount.getPopulatedUser();
+    const user = (await externalAccount.populate('user')).user;
     req.logIn(user, async(err) => {
       if (err) { debug(err.message); return next(new ExternalAccountLoginError(err.message)) }
 
@@ -544,7 +544,7 @@ module.exports = function(crowi, app) {
       return next(new ExternalAccountLoginError('message.sign_in_failure'));
     }
 
-    const user = await externalAccount.getPopulatedUser();
+    const user = (await externalAccount.populate('user')).user;
 
     // login
     req.logIn(user, (err) => {

+ 0 - 1
apps/app/src/server/routes/me.js

@@ -51,7 +51,6 @@
 module.exports = function(crowi, app) {
   const models = crowi.models;
   const UserGroupRelation = models.UserGroupRelation;
-  const ExternalAccount = models.ExternalAccount;
   const ApiResponse = require('../util/apiResponse');
 
   // , pluginService = require('../service/plugin')

+ 8 - 7
apps/app/src/server/service/external-account.ts

@@ -1,11 +1,11 @@
 import { ErrorV3 } from '^/../../packages/core/dist';
-import mongoose from 'mongoose';
 
 import { LoginErrorCode } from '~/interfaces/errors/login-error';
-import { IExternalAccount } from '~/interfaces/external-account';
+import { IExternalAccountHasId } from '~/interfaces/external-account';
 import loggerFactory from '~/utils/logger';
 
 import { NullUsernameToBeRegisteredError } from '../models/errors';
+import ExternalAccount from '../models/external-account';
 
 import PassportService from './passport';
 
@@ -20,23 +20,24 @@ class ExternalAccountService {
     this.passportService = passportService;
   }
 
-  async getOrCreateUser(userInfo: {id: string, username: string, name?: string, email?: string}, providerId: string): Promise<IExternalAccount | undefined> {
+  async getOrCreateUser(
+      userInfo: {id: string, username: string, name?: string, email?: string},
+      providerId: string,
+  ): Promise<IExternalAccountHasId | undefined> {
     // get option
     const isSameUsernameTreatedAsIdenticalUser = this.passportService.isSameUsernameTreatedAsIdenticalUser(providerId);
     const isSameEmailTreatedAsIdenticalUser = this.passportService.isSameEmailTreatedAsIdenticalUser(providerId);
 
-    const ExternalAccount = mongoose.model('ExternalAccount') as any;
-
     try {
       // find or register(create) user
       const externalAccount = await ExternalAccount.findOrRegister(
+        isSameUsernameTreatedAsIdenticalUser,
+        isSameEmailTreatedAsIdenticalUser,
         providerId,
         userInfo.id,
         userInfo.username,
         userInfo.name,
         userInfo.email,
-        isSameUsernameTreatedAsIdenticalUser,
-        isSameEmailTreatedAsIdenticalUser,
       );
       return externalAccount;
     }

+ 2 - 2
apps/app/src/stores/personal-settings.tsx

@@ -2,7 +2,7 @@ import { ErrorV3 } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import useSWR, { SWRConfiguration, SWRResponse } from 'swr';
 
-import { IExternalAccount } from '~/interfaces/external-account';
+import { IExternalAccountHasId } from '~/interfaces/external-account';
 import { IUser } from '~/interfaces/user';
 import { useIsGuestUser } from '~/stores/context';
 import loggerFactory from '~/utils/logger';
@@ -104,7 +104,7 @@ export const usePersonalSettings = (config?: SWRConfiguration): SWRResponse<IUse
   };
 };
 
-export const useSWRxPersonalExternalAccounts = (): SWRResponse<IExternalAccount[], Error> => {
+export const useSWRxPersonalExternalAccounts = (): SWRResponse<IExternalAccountHasId[], Error> => {
   return useSWR(
     '/personal-setting/external-accounts',
     endpoint => apiv3Get(endpoint).then(response => response.data.externalAccounts),