Browse Source

Merge pull request #7999 from weseek/feat/124385-126715-page-permission-using-external-user-group

enable selecting external user group for page grant
Yuki Takei 2 years ago
parent
commit
4fa428c452

+ 5 - 4
apps/app/src/components/Admin/UserGroupDetail/UserGroupDetailPage.tsx

@@ -9,10 +9,6 @@ import dynamic from 'next/dynamic';
 import Link from 'next/link';
 import { useRouter } from 'next/router';
 
-import {
-  useAncestorUserGroups,
-  useChildUserGroupList, useUserGroup, useUserGroupRelationList, useUserGroupRelations,
-} from '~/client/services/user-group';
 import {
   apiv3Get, apiv3Put, apiv3Delete, apiv3Post,
 } from '~/client/util/apiv3-client';
@@ -25,6 +21,11 @@ import { useUpdateUserGroupConfirmModal } from '~/stores/modal';
 import { useSWRxUserGroupPages, useSWRxSelectableParentUserGroups, useSWRxSelectableChildUserGroups } from '~/stores/user-group';
 import loggerFactory from '~/utils/logger';
 
+import {
+  useAncestorUserGroups,
+  useChildUserGroupList, useUserGroup, useUserGroupRelationList, useUserGroupRelations,
+} from './use-user-group-resource';
+
 import styles from './UserGroupDetailPage.module.scss';
 
 const logger = loggerFactory('growi:services:AdminCustomizeContainer');

+ 0 - 0
apps/app/src/client/services/user-group.ts → apps/app/src/components/Admin/UserGroupDetail/use-user-group-resource.ts


+ 1 - 1
apps/app/src/components/SavePageControls.tsx

@@ -18,7 +18,7 @@ import { useSWRxCurrentPage } from '~/stores/page';
 import { useSelectedGrant } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 
-import GrantSelector from './SavePageControls/GrantSelector';
+import { GrantSelector } from './SavePageControls/GrantSelector';
 
 
 declare global {

+ 15 - 27
apps/app/src/components/SavePageControls/GrantSelector.tsx → apps/app/src/components/SavePageControls/GrantSelector/GrantSelector.tsx

@@ -1,7 +1,7 @@
 import React, { useCallback, useState } from 'react';
 
 import { isPopulated } from '@growi/core';
-import type { GroupType, IUserGroupHasId } from '@growi/core';
+import type { GroupType, GrantedGroup } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import {
   UncontrolledDropdown,
@@ -12,8 +12,8 @@ import {
 
 import type { IPageGrantData } from '~/interfaces/page';
 import { useCurrentUser } from '~/stores/context';
-import { useSWRxMyUserGroupRelations } from '~/stores/user-group';
 
+import { useMyUserGroups } from './use-my-user-groups';
 
 const AVAILABLE_GRANTS = [
   {
@@ -47,7 +47,7 @@ type Props = {
 /**
  * Page grant select component
  */
-const GrantSelector = (props: Props): JSX.Element => {
+export const GrantSelector = (props: Props): JSX.Element => {
   const { t } = useTranslation();
 
   const {
@@ -63,12 +63,12 @@ const GrantSelector = (props: Props): JSX.Element => {
   const { data: currentUser } = useCurrentUser();
 
   const shouldFetch = isSelectGroupModalShown;
-  const { data: myUserGroupRelations, mutate: mutateMyUserGroupRelations } = useSWRxMyUserGroupRelations(shouldFetch);
+  const { data: myUserGroups, update: updateMyUserGroups } = useMyUserGroups(shouldFetch);
 
   const showSelectGroupModal = useCallback(() => {
-    mutateMyUserGroupRelations();
+    updateMyUserGroups();
     setIsSelectGroupModalShown(true);
-  }, [mutateMyUserGroupRelations]);
+  }, [updateMyUserGroups]);
 
   /**
    * change event handler for grant selector
@@ -85,9 +85,9 @@ const GrantSelector = (props: Props): JSX.Element => {
     }
   }, [onUpdateGrant, showSelectGroupModal]);
 
-  const groupListItemClickHandler = useCallback((grantGroup: IUserGroupHasId) => {
-    if (onUpdateGrant != null) {
-      onUpdateGrant({ grant: 5, grantedGroups: [{ id: grantGroup._id, name: grantGroup.name, type: 'UserGroup' }] });
+  const groupListItemClickHandler = useCallback((grantGroup: GrantedGroup) => {
+    if (onUpdateGrant != null && isPopulated(grantGroup.item)) {
+      onUpdateGrant({ grant: 5, grantedGroups: [{ id: grantGroup.item._id, name: grantGroup.item.name, type: grantGroup.type }] });
     }
 
     // hide modal
@@ -161,7 +161,7 @@ const GrantSelector = (props: Props): JSX.Element => {
     }
 
     // show spinner
-    if (myUserGroupRelations == null) {
+    if (myUserGroups == null) {
       return (
         <div className="my-3 text-center">
           <i className="fa fa-lg fa-spinner fa-pulse mx-auto text-muted"></i>
@@ -169,16 +169,7 @@ const GrantSelector = (props: Props): JSX.Element => {
       );
     }
 
-    // extract IUserGroupHasId
-    const userRelatedGroups: IUserGroupHasId[] = myUserGroupRelations
-      .map((relation) => {
-        // relation.relatedGroup should be populated by server
-        return isPopulated(relation.relatedGroup) ? relation.relatedGroup : undefined;
-      })
-      // exclude undefined elements
-      .filter((elem): elem is IUserGroupHasId => elem != null);
-
-    if (userRelatedGroups.length === 0) {
+    if (myUserGroups.length === 0) {
       return (
         <div>
           <h4>{t('user_group.belonging_to_no_group')}</h4>
@@ -191,10 +182,10 @@ const GrantSelector = (props: Props): JSX.Element => {
 
     return (
       <div className="list-group">
-        { userRelatedGroups.map((group) => {
+        { myUserGroups.map((group) => {
           return (
-            <button key={group._id} type="button" className="list-group-item list-group-item-action" onClick={() => groupListItemClickHandler(group)}>
-              <h5>{group.name}</h5>
+            <button key={group.item._id} type="button" className="list-group-item list-group-item-action" onClick={() => groupListItemClickHandler(group)}>
+              <h5>{group.item.name}</h5>
               {/* TODO: Replace <div className="small">(TBD) List group members</div> */}
             </button>
           );
@@ -202,7 +193,7 @@ const GrantSelector = (props: Props): JSX.Element => {
       </div>
     );
 
-  }, [currentUser?.admin, groupListItemClickHandler, myUserGroupRelations, shouldFetch, t]);
+  }, [currentUser?.admin, groupListItemClickHandler, myUserGroups, shouldFetch, t]);
 
   return (
     <>
@@ -225,7 +216,4 @@ const GrantSelector = (props: Props): JSX.Element => {
       ) }
     </>
   );
-
 };
-
-export default GrantSelector;

+ 1 - 0
apps/app/src/components/SavePageControls/GrantSelector/index.ts

@@ -0,0 +1 @@
+export * from './GrantSelector';

+ 38 - 0
apps/app/src/components/SavePageControls/GrantSelector/use-my-user-groups.ts

@@ -0,0 +1,38 @@
+import { GroupType } from '@growi/core';
+
+import { useSWRxMyExternalUserGroups } from '~/features/external-user-group/client/stores/external-user-group';
+import { useSWRxMyUserGroups } from '~/stores/user-group';
+
+// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+export const useMyUserGroups = (shouldFetch: boolean) => {
+  const { data: myUserGroups, mutate: mutateMyUserGroups } = useSWRxMyUserGroups(shouldFetch);
+  const { data: myExternalUserGroups, mutate: mutateMyExternalUserGroups } = useSWRxMyExternalUserGroups(shouldFetch);
+
+  const update = () => {
+    mutateMyUserGroups();
+    mutateMyExternalUserGroups();
+  };
+
+  if (myUserGroups == null || myExternalUserGroups == null) {
+    return { data: null, update };
+  }
+
+  const myUserGroupsData = myUserGroups
+    .map((group) => {
+      return {
+        item: group,
+        type: GroupType.userGroup,
+      };
+    });
+  const myExternalUserGroupsData = myExternalUserGroups
+    .map((group) => {
+      return {
+        item: group,
+        type: GroupType.externalUserGroup,
+      };
+    });
+
+  const data = [...myUserGroupsData, ...myExternalUserGroupsData];
+
+  return { data, update };
+};

+ 10 - 1
apps/app/src/features/external-user-group/client/stores/external-user-group.ts

@@ -4,7 +4,9 @@ import useSWRImmutable from 'swr/immutable';
 
 import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
 import { IExternalUserGroupHasId, IExternalUserGroupRelationHasId, LdapGroupSyncSettings } from '~/features/external-user-group/interfaces/external-user-group';
-import { ChildUserGroupListResult, IUserGroupRelationHasIdPopulatedUser, UserGroupRelationListResult } from '~/interfaces/user-group-response';
+import {
+  ChildUserGroupListResult, IUserGroupRelationHasIdPopulatedUser, UserGroupListResult, UserGroupRelationListResult,
+} from '~/interfaces/user-group-response';
 
 export const useSWRxLdapGroupSyncSettings = (): SWRResponse<LdapGroupSyncSettings, Error> => {
   return useSWR(
@@ -15,6 +17,13 @@ export const useSWRxLdapGroupSyncSettings = (): SWRResponse<LdapGroupSyncSetting
   );
 };
 
+export const useSWRxMyExternalUserGroups = (shouldFetch: boolean): SWRResponse<IExternalUserGroupHasId[], Error> => {
+  return useSWR(
+    shouldFetch ? '/me/external-user-groups' : null,
+    endpoint => apiv3Get<UserGroupListResult<IExternalUserGroupHasId>>(endpoint).then(result => result.data.userGroups),
+  );
+};
+
 export const useSWRxExternalUserGroup = (groupId: string | null): SWRResponse<IExternalUserGroupHasId, Error> => {
   return useSWRImmutable(
     groupId != null ? `/external-user-groups/${groupId}` : null,

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

@@ -21,6 +21,10 @@ describe('ExternalUserGroupRelation model', () => {
   let user2;
   const userId2 = new mongoose.Types.ObjectId();
 
+  const groupId1 = new mongoose.Types.ObjectId();
+  const groupId2 = new mongoose.Types.ObjectId();
+  const groupId3 = new mongoose.Types.ObjectId();
+
   beforeAll(async() => {
     user1 = await User.create({
       _id: userId1, name: 'user1', username: 'user1', email: 'user1@example.com',
@@ -29,28 +33,25 @@ describe('ExternalUserGroupRelation model', () => {
     user2 = await User.create({
       _id: userId2, name: 'user2', username: 'user2', email: 'user2@example.com',
     });
+
+    await ExternalUserGroup.insertMany([
+      {
+        _id: groupId1, name: 'test group 1', externalId: 'testExternalId', provider: 'testProvider',
+      },
+      {
+        _id: groupId2, name: 'test group 2', externalId: 'testExternalId2', provider: 'testProvider',
+      },
+      {
+        _id: groupId3, name: 'test group 3', externalId: 'testExternalId3', provider: 'testProvider',
+      },
+    ]);
   });
 
   afterEach(async() => {
-    await ExternalUserGroup.deleteMany();
     await ExternalUserGroupRelation.deleteMany();
   });
 
   describe('createRelations', () => {
-    const groupId1 = new mongoose.Types.ObjectId();
-    const groupId2 = new mongoose.Types.ObjectId();
-
-    beforeAll(async() => {
-      await ExternalUserGroup.insertMany([
-        {
-          _id: groupId1, name: 'test group 1', externalId: 'testExternalId', provider: 'testProvider',
-        },
-        {
-          _id: groupId2, name: 'test group 2', externalId: 'testExternalId2', provider: 'testProvider',
-        },
-      ]);
-    });
-
     it('creates relation for user', async() => {
       await ExternalUserGroupRelation.createRelations([groupId1, groupId2], user1);
       const relations = await ExternalUserGroupRelation.find();
@@ -62,11 +63,10 @@ describe('ExternalUserGroupRelation model', () => {
   });
 
   describe('removeAllInvalidRelations', () => {
-    const groupId1 = new mongoose.Types.ObjectId();
-    const groupId2 = new mongoose.Types.ObjectId();
-
     beforeAll(async() => {
-      await ExternalUserGroupRelation.createRelations([groupId1, groupId2], user1);
+      const nonExistentGroupId1 = new mongoose.Types.ObjectId();
+      const nonExistentGroupId2 = new mongoose.Types.ObjectId();
+      await ExternalUserGroupRelation.createRelations([nonExistentGroupId1, nonExistentGroupId2], user1);
     });
 
     it('removes invalid relations', async() => {
@@ -81,10 +81,6 @@ describe('ExternalUserGroupRelation model', () => {
   });
 
   describe('findAllUserIdsForUserGroups', () => {
-    const groupId1 = new mongoose.Types.ObjectId();
-    const groupId2 = new mongoose.Types.ObjectId();
-    const groupId3 = new mongoose.Types.ObjectId();
-
     beforeAll(async() => {
       await ExternalUserGroupRelation.createRelations([groupId1, groupId2], user1);
       await ExternalUserGroupRelation.create({ relatedGroup: groupId3, relatedUser: user2._id });
@@ -97,10 +93,6 @@ describe('ExternalUserGroupRelation model', () => {
   });
 
   describe('findAllUserGroupIdsRelatedToUser', () => {
-    const groupId1 = new mongoose.Types.ObjectId();
-    const groupId2 = new mongoose.Types.ObjectId();
-    const groupId3 = new mongoose.Types.ObjectId();
-
     beforeAll(async() => {
       await ExternalUserGroupRelation.createRelations([groupId1, groupId2], user1);
       await ExternalUserGroupRelation.create({ relatedGroup: groupId3, relatedUser: user2._id });
@@ -114,4 +106,25 @@ describe('ExternalUserGroupRelation model', () => {
       expect(groupIds2).toStrictEqual([groupId3]);
     });
   });
+
+  describe('findAllRelationForUser', () => {
+    beforeAll(async() => {
+      await ExternalUserGroupRelation.createRelations([groupId1, groupId2], user1);
+      await ExternalUserGroupRelation.create({ relatedGroup: groupId3, relatedUser: user2._id });
+    });
+
+    it('finds all relations for user with group populated', async() => {
+      const relations = await ExternalUserGroupRelation.findAllRelationForUser(user1);
+      const populatedGroupIds = relations.map((relation) => {
+        return typeof relation.relatedGroup !== 'string' ? relation.relatedGroup._id : null;
+      });
+      expect(populatedGroupIds).toStrictEqual([groupId1, groupId2]);
+
+      const relations2 = await ExternalUserGroupRelation.findAllRelationForUser(user2);
+      const populatedGroupIds2 = relations2.map((relation) => {
+        return typeof relation.relatedGroup !== 'string' ? relation.relatedGroup._id : null;
+      });
+      expect(populatedGroupIds2).toStrictEqual([groupId3]);
+    });
+  });
 });

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

@@ -22,6 +22,8 @@ export interface ExternalUserGroupRelationModel extends Model<ExternalUserGroupR
   findGroupsWithDescendantsByGroupAndUser: (group: ExternalUserGroupDocument, user) => Promise<ExternalUserGroupDocument[]>,
 
   countByGroupIdsAndUser: (userGroupIds: ObjectIdLike[], userData) => Promise<number>
+
+  findAllRelationForUser: (user) => Promise<ExternalUserGroupRelationDocument[]>
 }
 
 const schema = new Schema<ExternalUserGroupRelationDocument, ExternalUserGroupRelationModel>({
@@ -47,4 +49,6 @@ schema.statics.findAllUserIdsForUserGroups = UserGroupRelation.findAllUserIdsFor
 
 schema.statics.findAllUserGroupIdsRelatedToUser = UserGroupRelation.findAllUserGroupIdsRelatedToUser;
 
+schema.statics.findAllRelationForUser = UserGroupRelation.findAllRelationForUser;
+
 export default getOrCreateModel<ExternalUserGroupRelationDocument, ExternalUserGroupRelationModel>('ExternalUserGroupRelation', schema);

+ 2 - 2
apps/app/src/interfaces/user-group-response.ts

@@ -9,8 +9,8 @@ export type UserGroupResult = {
   userGroup: IUserGroupHasId,
 }
 
-export type UserGroupListResult = {
-  userGroups: IUserGroupHasId[],
+export type UserGroupListResult<TUSERGROUP extends IUserGroupHasId = IUserGroupHasId> = {
+  userGroups: TUSERGROUP[],
 };
 
 export type ChildUserGroupListResult<TUSERGROUP extends IUserGroupHasId = IUserGroupHasId> = {

+ 3 - 1
apps/app/src/server/models/user-group-relation.ts

@@ -26,6 +26,8 @@ export interface UserGroupRelationModel extends Model<UserGroupRelationDocument>
   findGroupsWithDescendantsByGroupAndUser: (group: UserGroupDocument, user) => Promise<UserGroupDocument[]>,
 
   countByGroupIdsAndUser: (userGroupIds: ObjectIdLike[], userData) => Promise<number>
+
+  findAllRelationForUser: (user) => Promise<UserGroupRelationDocument[]>
 }
 
 /*
@@ -121,7 +123,7 @@ schema.statics.findAllRelationForUserGroups = function(userGroups) {
  * @returns {Promise<UserGroupRelation[]>}
  * @memberof UserGroupRelation
  */
-schema.statics.findAllRelationForUser = function(user) {
+schema.statics.findAllRelationForUser = function(user): Promise<UserGroupRelationDocument[]> {
   return this
     .find({ relatedUser: user.id })
     .populate('relatedGroup')

+ 2 - 0
apps/app/src/server/routes/apiv3/index.js

@@ -118,5 +118,7 @@ module.exports = (crowi, app) => {
   router.use('/questionnaire', require('~/features/questionnaire/server/routes/apiv3/questionnaire')(crowi));
   router.use('/templates', require('~/features/templates/server/routes/apiv3')(crowi));
 
+  router.use('/me', require('./me')(crowi));
+
   return [router, routerForAdmin, routerForAuth];
 };

+ 66 - 0
apps/app/src/server/routes/apiv3/me.ts

@@ -0,0 +1,66 @@
+import { type IUserHasId, isPopulated } from '@growi/core';
+import { Router, Request } from 'express';
+
+import ExternalUserGroupRelation from '~/features/external-user-group/server/models/external-user-group-relation';
+import loggerFactory from '~/utils/logger';
+
+import UserGroupRelation from '../../models/user-group-relation';
+
+import { ApiV3Response } from './interfaces/apiv3-response';
+
+const logger = loggerFactory('growi:routes:apiv3:me');
+
+const router = Router();
+
+interface AuthorizedRequest extends Request {
+  user?: IUserHasId
+}
+
+module.exports = function(crowi) {
+  const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
+  const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
+
+  const ApiResponse = require('../../util/apiResponse');
+
+  /**
+   * retrieve user-group documents
+   */
+  router.get('/user-groups', accessTokenParser, loginRequiredStrictly, async(req: AuthorizedRequest, res: ApiV3Response) => {
+    try {
+      const userGroupRelations = await UserGroupRelation.findAllRelationForUser(req.user);
+      const userGroups = userGroupRelations.map((relation) => {
+        // relation.relatedGroup should be populated
+        return isPopulated(relation.relatedGroup) ? relation.relatedGroup : undefined;
+      })
+        // exclude undefined elements
+        .filter(elem => elem != null);
+      return res.json(ApiResponse.success({ userGroups }));
+    }
+    catch (e) {
+      logger.error(e);
+      return res.apiv3Err(e, 500);
+    }
+  });
+
+  /**
+   * retrieve external-user-group-relation documents
+   */
+  router.get('/external-user-groups', accessTokenParser, loginRequiredStrictly, async(req: AuthorizedRequest, res: ApiV3Response) => {
+    try {
+      const userGroupRelations = await ExternalUserGroupRelation.findAllRelationForUser(req.user);
+      const userGroups = userGroupRelations.map((relation) => {
+        // relation.relatedGroup should be populated
+        return isPopulated(relation.relatedGroup) ? relation.relatedGroup : undefined;
+      })
+      // exclude undefined elements
+        .filter(elem => elem != null);
+      return res.json(ApiResponse.success({ userGroups }));
+    }
+    catch (e) {
+      logger.error(e);
+      return res.apiv3Err(e, 500);
+    }
+  });
+
+  return router;
+};

+ 0 - 3
apps/app/src/server/routes/index.js

@@ -41,7 +41,6 @@ module.exports = function(crowi, app) {
   const page = require('./page')(crowi, app);
   const login = require('./login')(crowi, app);
   const loginPassport = require('./login-passport')(crowi, app);
-  const me = require('./me')(crowi, app);
   const admin = require('./admin')(crowi, app);
   const attachment = require('./attachment')(crowi, app);
   const comment = require('./comment')(crowi, app);
@@ -122,8 +121,6 @@ module.exports = function(crowi, app) {
 
   apiV1Router.get('/search'                        , accessTokenParser , loginRequired , search.api.search);
 
-  apiV1Router.get('/me/user-group-relations'  , accessTokenParser , loginRequiredStrictly , me.api.userGroupRelations);
-
   // HTTP RPC Styled API (に徐々に移行していいこうと思う)
   apiV1Router.get('/pages.list'          , accessTokenParser , loginRequired , page.api.list);
   apiV1Router.post('/pages.update'       , accessTokenParser , loginRequiredStrictly , excludeReadOnlyUser, addActivity, page.api.update);

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

@@ -1,103 +0,0 @@
-import UserGroupRelation from '../models/user-group-relation';
-
-/**
- * @swagger
- *
- *  components:
- *    schemas:
- *      UserGroup:
- *        description: UserGroup
- *        type: object
- *        properties:
- *          __v:
- *            type: number
- *            description: record version
- *            example: 0
- *          _id:
- *            type: string
- *            description: user group ID
- *            example: 5e2d56c1e35da4004ef7e0b0
- *          createdAt:
- *            type: string
- *            description: date created at
- *            example: 2010-01-01T00:00:00.000Z
- */
-
-/**
- * @swagger
- *
- *  components:
- *    schemas:
- *      UserGroupRelation:
- *        description: UserGroupRelation
- *        type: object
- *        properties:
- *          __v:
- *            type: number
- *            description: record version
- *            example: 0
- *          _id:
- *            type: string
- *            description: user group relation ID
- *            example: 5e2d56cbe35da4004ef7e0b1
- *          relatedGroup:
- *            $ref: '#/components/schemas/UserGroup'
- *          relatedUser:
- *            $ref: '#/components/schemas/User/properties/_id'
- *          createdAt:
- *            type: string
- *            description: date created at
- *            example: 2010-01-01T00:00:00.000Z
- */
-
-module.exports = function(crowi, app) {
-  const ApiResponse = require('../util/apiResponse');
-
-  // , pluginService = require('../service/plugin')
-
-  const actions = {};
-
-  const api = {};
-  actions.api = api;
-
-  /**
-   * @swagger
-   *
-   *   /me/user-group-relations:
-   *     get:
-   *       tags: [Me, CrowiCompatibles]
-   *       operationId: getUserGroupRelations
-   *       summary: /me/user-group-relations
-   *       description: Get user group relations
-   *       responses:
-   *         200:
-   *           description: Succeeded to get user group relations.
-   *           content:
-   *             application/json:
-   *               schema:
-   *                 properties:
-   *                   ok:
-   *                     $ref: '#/components/schemas/V1Response/properties/ok'
-   *                   userGroupRelations:
-   *                     type: array
-   *                     items:
-   *                       $ref: '#/components/schemas/UserGroupRelation'
-   *         403:
-   *           $ref: '#/components/responses/403'
-   *         500:
-   *           $ref: '#/components/responses/500'
-   */
-  /**
-   * retrieve user-group-relation documents
-   * @param {object} req
-   * @param {object} res
-   */
-  api.userGroupRelations = function(req, res) {
-    UserGroupRelation.findAllRelationForUser(req.user)
-      .then((userGroupRelations) => {
-        return res.json(ApiResponse.success({ userGroupRelations }));
-      });
-  };
-
-  return actions;
-};

+ 6 - 9
apps/app/src/stores/user-group.tsx

@@ -1,9 +1,10 @@
-import type { IPageHasId, IUserGroupHasId, IUserGroupRelationHasId } from '@growi/core';
+import type {
+  IPageHasId, IUserGroupHasId, IUserGroupRelationHasId,
+} from '@growi/core';
 import { type SWRResponseWithUtils, withUtils } from '@growi/core/dist/swr';
 import useSWR, { SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
-import { apiGet } from '~/client/util/apiv1-client';
 import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
 import {
   IUserGroupRelationHasIdPopulatedUser,
@@ -12,14 +13,10 @@ import {
 } from '~/interfaces/user-group-response';
 
 
-type MyUserGroupRelationsResult = {
-  userGroupRelations: IUserGroupRelationHasId[],
-}
-
-export const useSWRxMyUserGroupRelations = (shouldFetch: boolean): SWRResponse<IUserGroupRelationHasId[], Error> => {
+export const useSWRxMyUserGroups = (shouldFetch: boolean): SWRResponse<IUserGroupHasId[], Error> => {
   return useSWR(
-    shouldFetch ? '/me/user-group-relations' : null,
-    endpoint => apiGet(endpoint).then(result => (result as MyUserGroupRelationsResult).userGroupRelations),
+    shouldFetch ? '/me/user-groups' : null,
+    endpoint => apiv3Get<UserGroupListResult>(endpoint).then(result => result.data.userGroups),
   );
 };