From 3f7a9ed11a5e459f8953ed951ebbb4cc27240f4b Mon Sep 17 00:00:00 2001 From: "x.sunamoto.k" Date: Fri, 12 May 2023 01:27:19 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=2075:=20API=E5=AE=9F=E8=A3=85?= =?UTF-8?q?=EF=BC=88=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC=E4=B8=80=E8=A6=A7?= =?UTF-8?q?=E5=8F=96=E5=BE=97=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task1592: API実装(ユーザー一覧取得)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/1592) - ユーザ一覧取得のAPIを実装 - アクセストークンにより権限を確認する  - src/common/jwt/jwt.ts verifyAuthority([Task1593: API実装(ユーザー登録)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/1593)で作成)を呼び出すため追って再修正します。(レビュー対象外です)  - src/features/users/users.controller.ts getUsersから   src/features/users/users.service.ts getUsersへ - DBから同一アカウントのユーザ一覧を取得する  - findSameAccountUsersを新規作成 - Azure AD B2Cからユーザーを取得してマージ  - src/gateways/adb2c/adb2c.service.ts getUserを新規作成  - マージはfor文でまわしています(力技) - マージした結果を返却 - 影響範囲  - usersテーブルの変更が入るときにマージ部分の手直しが要ります。(TODOを添えています) ## レビューポイント - 新規に作成したfindSameAccountUsersの妥当性 - 新規に作成したgetUserの妥当性 →Azureからの返り値はsrc/common/token/types.tsに定義済。  (Azure AD B2Cから取得できた項目で再定義) ## UIの変更 - 特になし ## 動作確認状況 - ローカルでビルド、テストを実行した後に動作を確認済。 ## 補足 - ご不便をおかけしました。よろしくお願いします。 --- dictation_server/src/common/token/index.ts | 10 +- dictation_server/src/common/token/types.ts | 5 + .../features/users/test/users.service.mock.ts | 81 +++++++++++-- .../features/users/users.controller.spec.ts | 6 +- .../src/features/users/users.controller.ts | 49 ++++++-- .../src/features/users/users.module.ts | 8 +- .../src/features/users/users.service.spec.ts | 108 +++++++++++++++++- .../src/features/users/users.service.ts | 80 +++++++++++-- .../src/gateways/adb2c/adb2c.service.ts | 26 ++++- .../users/users.repository.service.ts | 17 +++ 10 files changed, 347 insertions(+), 43 deletions(-) diff --git a/dictation_server/src/common/token/index.ts b/dictation_server/src/common/token/index.ts index 7c1b5ef..415676a 100644 --- a/dictation_server/src/common/token/index.ts +++ b/dictation_server/src/common/token/index.ts @@ -4,8 +4,16 @@ import type { IDToken, JwkSignKey, RefreshToken, + Aadb2cUser, } from './types'; import { isIDToken } from './typeguard'; -export type { AccessToken, B2cMetadata, IDToken, JwkSignKey, RefreshToken }; +export type { + AccessToken, + B2cMetadata, + IDToken, + JwkSignKey, + RefreshToken, + Aadb2cUser, +}; export { isIDToken }; diff --git a/dictation_server/src/common/token/types.ts b/dictation_server/src/common/token/types.ts index f815686..f4eea6a 100644 --- a/dictation_server/src/common/token/types.ts +++ b/dictation_server/src/common/token/types.ts @@ -28,3 +28,8 @@ export type JwkSignKey = { e: string; n: string; }; + +export type Aadb2cUser = { + displayName: string; + mail: string; +}; diff --git a/dictation_server/src/features/users/test/users.service.mock.ts b/dictation_server/src/features/users/test/users.service.mock.ts index 1cf4072..a59284a 100644 --- a/dictation_server/src/features/users/test/users.service.mock.ts +++ b/dictation_server/src/features/users/test/users.service.mock.ts @@ -1,15 +1,15 @@ +import { ConfigModule, ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { UsersService } from '../users.service'; -import { UsersRepositoryService } from '../../../repositories/users/users.repository.service'; -import { CryptoService } from '../../../gateways/crypto/crypto.service'; +import { Aadb2cUser, B2cMetadata, JwkSignKey } from '../../../common/token'; import { AdB2cService, ConflictError, } from '../../../gateways/adb2c/adb2c.service'; +import { CryptoService } from '../../../gateways/crypto/crypto.service'; import { SendGridService } from '../../../gateways/sendgrid/sendgrid.service'; -import { ConfigModule, ConfigService } from '@nestjs/config'; import { User } from '../../../repositories/users/entity/user.entity'; -import { JwkSignKey, B2cMetadata } from '../../../common/token'; +import { UsersRepositoryService } from '../../../repositories/users/users.repository.service'; +import { UsersService } from '../users.service'; export type CryptoMockValue = { getPublicKey: string | Error; @@ -19,6 +19,7 @@ export type UsersRepositoryMockValue = { updateUserVerified: undefined | Error; findUserById: User | Error; createNormalUser: User | Error; + findSameAccountUsers: User[] | Error; }; export type AdB2cMockValue = { @@ -26,6 +27,7 @@ export type AdB2cMockValue = { getSignKeySets: JwkSignKey[] | Error; changePassword: { sub: string } | Error; createUser: string | ConflictError | Error; + getUser: Aadb2cUser | Error; }; export type SendGridMockValue = { @@ -59,6 +61,10 @@ export const makeDefaultAdB2cMockValue = (): AdB2cMockValue => { sub: 'TEST9999', }, createUser: '001', + getUser: { + displayName: 'Hanako Sato', + mail: 'hanako@sample.com', + }, }; }; @@ -81,7 +87,8 @@ export const makeSendGridServiceMock = (value: SendGridMockValue) => { }; export const makeAdB2cServiceMock = (value: AdB2cMockValue) => { - const { getMetaData, getSignKeySets, changePassword, createUser } = value; + const { getMetaData, getSignKeySets, changePassword, createUser, getUser } = + value; return { getMetaData: @@ -106,6 +113,14 @@ export const makeAdB2cServiceMock = (value: AdB2cMockValue) => { : jest .fn, []>() .mockResolvedValue(createUser), + getUser: + getUser instanceof Error + ? jest + .fn, []>() + .mockRejectedValue(getUser) + : jest + .fn, []>() + .mockResolvedValue(getUser), }; }; @@ -166,7 +181,12 @@ class authorIdError extends Error { } export const makeUsersRepositoryMock = (value: UsersRepositoryMockValue) => { - const { updateUserVerified, findUserById, createNormalUser } = value; + const { + updateUserVerified, + findUserById, + createNormalUser, + findSameAccountUsers, + } = value; const aIdError = new authorIdError('ER_DUP_ENTRY'); @@ -185,6 +205,14 @@ export const makeUsersRepositoryMock = (value: UsersRepositoryMockValue) => { ? jest.fn, []>().mockRejectedValue(aIdError) : jest.fn, []>().mockRejectedValue(createNormalUser) : jest.fn, []>().mockResolvedValue(createNormalUser), + findSameAccountUsers: + findSameAccountUsers instanceof Error + ? jest + .fn, []>() + .mockRejectedValue(findSameAccountUsers) + : jest + .fn, []>() + .mockResolvedValue(findSameAccountUsers), }; }; @@ -206,6 +234,7 @@ export const makeSendGridMock = (value: SendGridMockValue) => { .mockResolvedValue(createMailContentFromEmailConfirmForNormalUser), }; }; + export const makeConfigMock = (value: ConfigMockValue) => { const { get } = value; @@ -216,6 +245,7 @@ export const makeConfigMock = (value: ConfigMockValue) => { : jest.fn, []>().mockResolvedValue(get), }; }; + export const makeDefaultCryptoMockValue = (): CryptoMockValue => { return { getPublicKey: [ @@ -243,6 +273,7 @@ export const makeDefaultSendGridlValue = (): SendGridMockValue => { }, }; }; + export const makeDefaultConfigValue = (): ConfigMockValue => { return { get: `test@example.co.jp`, @@ -254,9 +285,45 @@ export const makeDefaultUsersRepositoryMockValue = (): UsersRepositoryMockValue => { const newUser = new User(); newUser.id = 111; + + const user1 = new User(); + user1.id = 2; + user1.external_id = 'ede66c43-9b9d-4222-93ed-5f11c96e08e2'; + user1.account_id = 1234567890123456; + user1.role = 'none'; + user1.author_id = '6cce347f-0cf1-a15e-19ab-d00988b643f9'; + user1.accepted_terms_version = '1.0'; + user1.email_verified = true; + user1.auto_renew = false; + user1.license_alert = false; + user1.notification = false; + user1.deleted_at = null; + user1.created_by = 'test'; + user1.created_at = new Date(); + user1.updated_by = null; + user1.updated_at = null; + + const user2 = new User(); + user2.id = 3; + user2.external_id = '698176dc-4028-4638-a452-f00bf62a7781'; + user2.account_id = 1234567890123456; + user2.role = 'none'; + user2.author_id = '551c4077-5b55-a38c-2c55-cd1edd537aa8'; + user2.accepted_terms_version = '1.0'; + user2.email_verified = true; + user2.auto_renew = false; + user2.license_alert = false; + user2.notification = false; + user2.deleted_at = null; + user2.created_by = 'test'; + user2.created_at = new Date(); + user2.updated_by = null; + user2.updated_at = null; + return { updateUserVerified: undefined, findUserById: newUser, createNormalUser: newUser, + findSameAccountUsers: [user1, user2], }; }; diff --git a/dictation_server/src/features/users/users.controller.spec.ts b/dictation_server/src/features/users/users.controller.spec.ts index f7c7bbe..90544fe 100644 --- a/dictation_server/src/features/users/users.controller.spec.ts +++ b/dictation_server/src/features/users/users.controller.spec.ts @@ -1,18 +1,22 @@ import { Test, TestingModule } from '@nestjs/testing'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; +import { CryptoService } from '../../gateways/crypto/crypto.service'; describe('UsersController', () => { let controller: UsersController; const mockUserService = {}; + const mockCryptoService = {}; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [UsersController], - providers: [UsersService], + providers: [UsersService, CryptoService], }) .overrideProvider(UsersService) .useValue(mockUserService) + .overrideProvider(CryptoService) + .useValue(mockCryptoService) .compile(); controller = module.get(UsersController); diff --git a/dictation_server/src/features/users/users.controller.ts b/dictation_server/src/features/users/users.controller.ts index d66bdb8..7e618a2 100644 --- a/dictation_server/src/features/users/users.controller.ts +++ b/dictation_server/src/features/users/users.controller.ts @@ -2,10 +2,10 @@ import { Body, Controller, Get, + HttpException, HttpStatus, Post, Req, - HttpException, } from '@nestjs/common'; import { ApiBearerAuth, @@ -13,7 +13,14 @@ import { ApiResponse, ApiTags, } from '@nestjs/swagger'; +import { Request } from 'express'; +import { confirmPermission } from '../../common/auth/auth'; +import { makeErrorResponse } from '../../common/error/makeErrorResponse'; import { ErrorResponse } from '../../common/error/types/types'; +import { retrieveAccessToken } from '../../common/http/helper'; +import { isVerifyError, verify } from '../../common/jwt/jwt'; +import { AccessToken } from '../../common/token'; +import { CryptoService } from '../../gateways/crypto/crypto.service'; import { ConfirmRequest, ConfirmResponse, @@ -23,13 +30,6 @@ import { SignupResponse, } from './types/types'; import { UsersService } from './users.service'; -import { Request } from 'express'; -import { verify, isVerifyError } from '../../common/jwt/jwt'; -import { CryptoService } from '../../gateways/crypto/crypto.service'; -import { makeErrorResponse } from '../../common/error/makeErrorResponse'; -import { AccessToken } from '../../common/token'; -import { retrieveAccessToken } from '../../common/http/helper'; -import { confirmPermission } from '../../common/auth/auth'; @ApiTags('users') @Controller('users') @@ -106,7 +106,38 @@ export class UsersController { @Get() async getUsers(@Req() req: Request): Promise { console.log(req.header('Authorization')); - return { users: [] }; + + // アクセストークンにより権限を確認する + const pubKey = await this.cryptoService.getPublicKey(); + const accessToken = retrieveAccessToken(req); + + // アクセストークンが存在しない場合のエラー + if (accessToken == undefined) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + const payload = verify(accessToken, pubKey); + + // アクセストークン形式エラー + if (isVerifyError(payload)) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + + // アクセストークンの権限不足エラー + if (!confirmPermission(payload.scope)) { + throw new HttpException( + makeErrorResponse('E000108'), + HttpStatus.UNAUTHORIZED, + ); + } + + const users = await this.usersService.getUsers(accessToken); + return { users }; } @ApiResponse({ diff --git a/dictation_server/src/features/users/users.module.ts b/dictation_server/src/features/users/users.module.ts index e907e15..072c88d 100644 --- a/dictation_server/src/features/users/users.module.ts +++ b/dictation_server/src/features/users/users.module.ts @@ -1,11 +1,11 @@ import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { AdB2cModule } from '../../gateways/adb2c/adb2c.module'; import { CryptoModule } from '../../gateways/crypto/crypto.module'; +import { SendGridModule } from '../../gateways/sendgrid/sendgrid.module'; +import { UsersRepositoryModule } from '../../repositories/users/users.repository.module'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; -import { UsersRepositoryModule } from '../../repositories/users/users.repository.module'; -import { AdB2cModule } from '../../gateways/adb2c/adb2c.module'; -import { SendGridModule } from '../../gateways/sendgrid/sendgrid.module'; -import { ConfigModule } from '@nestjs/config'; @Module({ imports: [ diff --git a/dictation_server/src/features/users/users.service.spec.ts b/dictation_server/src/features/users/users.service.spec.ts index 2d66c70..6c30d24 100644 --- a/dictation_server/src/features/users/users.service.spec.ts +++ b/dictation_server/src/features/users/users.service.spec.ts @@ -1,15 +1,17 @@ import { HttpException, HttpStatus } from '@nestjs/common'; +import { AccessToken } from 'src/common/token'; import { makeErrorResponse } from '../../common/error/makeErrorResponse'; +import { User as EntityUser } from '../../repositories/users/entity/user.entity'; +import { EmailAlreadyVerifiedError } from '../../repositories/users/users.repository.service'; import { + makeDefaultAdB2cMockValue, + makeDefaultConfigValue, makeDefaultCryptoMockValue, + makeDefaultSendGridlValue, makeDefaultUsersRepositoryMockValue, makeUsersServiceMock, - makeDefaultAdB2cMockValue, - makeDefaultSendGridlValue, - makeDefaultConfigValue, } from './test/users.service.mock'; -import { EmailAlreadyVerifiedError } from '../../repositories/users/users.repository.service'; -import { AccessToken } from 'src/common/token'; +import { User } from './types/types'; describe('UsersService', () => { it('ユーザの仮登録時に払い出されるトークンにより、未認証のユーザが認証済みになる', async () => { @@ -497,3 +499,99 @@ it('AuthorIDが重複している場合、エラーとなる。', async () => { new HttpException(makeErrorResponse('E010302'), HttpStatus.BAD_REQUEST), ); }); + +it('ユーザの一覧を取得する', async () => { + const cryptoMockValue = makeDefaultCryptoMockValue(); + const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const adb2cParam = makeDefaultAdB2cMockValue(); + const sendgridMockValue = makeDefaultSendGridlValue(); + const configMockValue = makeDefaultConfigValue(); + const service = await makeUsersServiceMock( + cryptoMockValue, + usersRepositoryMockValue, + adb2cParam, + sendgridMockValue, + configMockValue, + ); + const token = + 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw'; + + expect(await service.getUsers(token)).toEqual(expectedUsers); +}); + +const expectedUsers = [ + { + name: 'Hanako Sato', + role: 'none', + authorId: '6cce347f-0cf1-a15e-19ab-d00988b643f9', + typistGroupName: '', + email: 'hanako@sample.com', + emailVerified: true, + autoRenew: false, + licenseAlert: false, + notification: false, + }, + { + name: 'Hanako Sato', + role: 'none', + authorId: '551c4077-5b55-a38c-2c55-cd1edd537aa8', + typistGroupName: '', + email: 'hanako@sample.com', + emailVerified: true, + autoRenew: false, + licenseAlert: false, + notification: false, + }, +]; + +it('ユーザの一覧を取得に失敗する', async () => { + const cryptoMockValue = makeDefaultCryptoMockValue(); + const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const adb2cParam = makeDefaultAdB2cMockValue(); + const sendgridMockValue = makeDefaultSendGridlValue(); + const configMockValue = makeDefaultConfigValue(); + usersRepositoryMockValue.findSameAccountUsers = new Error( + 'Failure to acquire', + ); + + const service = await makeUsersServiceMock( + cryptoMockValue, + usersRepositoryMockValue, + adb2cParam, + sendgridMockValue, + configMockValue, + ); + + const token = + 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw'; + + await expect(service.getUsers(token)).rejects.toEqual( + new HttpException(makeErrorResponse('E009999'), HttpStatus.NOT_FOUND), + ); +}); + +it('ユーザの一覧を0件取得する', async () => { + const cryptoMockValue = makeDefaultCryptoMockValue(); + const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const adb2cParam = makeDefaultAdB2cMockValue(); + const sendgridMockValue = makeDefaultSendGridlValue(); + const configMockValue = makeDefaultConfigValue(); + + // モックでDBからのユーザ取得を空にする + const noDbUsers: EntityUser[] = []; + usersRepositoryMockValue.findSameAccountUsers = noDbUsers; + + const service = await makeUsersServiceMock( + cryptoMockValue, + usersRepositoryMockValue, + adb2cParam, + sendgridMockValue, + configMockValue, + ); + + const token = + 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw'; + + const emptyMergedUsers: User[] = []; + expect(await service.getUsers(token)).toEqual(emptyMergedUsers); +}); diff --git a/dictation_server/src/features/users/users.service.ts b/dictation_server/src/features/users/users.service.ts index b91e20e..317a006 100644 --- a/dictation_server/src/features/users/users.service.ts +++ b/dictation_server/src/features/users/users.service.ts @@ -1,21 +1,22 @@ import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; -import { isVerifyError, verify } from '../../common/jwt'; -import { CryptoService } from '../../gateways/crypto/crypto.service'; -import { - UsersRepositoryService, - EmailAlreadyVerifiedError, -} from '../../repositories/users/users.repository.service'; +import { ConfigService } from '@nestjs/config'; import { makeErrorResponse } from '../../common/error/makeErrorResponse'; +import { isVerifyError, verify } from '../../common/jwt'; +import { makePassword } from '../../common/password/password'; +import { AccessToken } from '../../common/token'; import { AdB2cService, ConflictError, isConflictError, } from '../../gateways/adb2c/adb2c.service'; -import { ConfigService } from '@nestjs/config'; +import { CryptoService } from '../../gateways/crypto/crypto.service'; import { SendGridService } from '../../gateways/sendgrid/sendgrid.service'; -import { User } from '../../repositories/users/entity/user.entity'; -import { makePassword } from '../../common/password/password'; -import { AccessToken } from '../../common/token'; +import { User as EntityUser } from '../../repositories/users/entity/user.entity'; +import { + EmailAlreadyVerifiedError, + UsersRepositoryService, +} from '../../repositories/users/users.repository.service'; +import { User } from './types/types'; @Injectable() export class UsersService { @@ -100,7 +101,7 @@ export class UsersService { const userId = Number(accessToken.userId); //DBよりアクセス者の所属するアカウントIDを取得する - let adminUser: User; + let adminUser: EntityUser; try { adminUser = await this.usersRepository.findUserById(userId); } catch (e) { @@ -141,7 +142,7 @@ export class UsersService { } //Azure AD B2Cに登録したユーザー情報のID(sub)と受け取った情報を使ってDBにユーザーを登録する - let newUser: User; + let newUser: EntityUser; // TODO 本来はNULLだが、テーブル定義に誤ってNOTNULLが付いているため、一時的に適当な値を設定 const accepted_terms_version = 'xxx'; try { @@ -261,4 +262,59 @@ export class UsersService { } } } + + /** + * Get Users + * @param accessToken + * @returns users + */ + async getUsers(accessToken: string): Promise { + this.logger.log(`[IN] ${this.getUsers.name}`); + + try { + // DBよりアクセス者の所属するアカウントを取得する + const pubKey = await this.cryptoService.getPublicKey(); + const payload = verify(accessToken, pubKey); + if (isVerifyError(payload)) { + throw new Error(`${payload.reason} | ${payload.message}`); + } + + // DBから同一アカウントのユーザ一覧を取得する + const dbUsers = await this.usersRepository.findSameAccountUsers( + Number(payload.userId), + ); + + // 値をマージして定義されたレスポンス通りに返す + const users: User[] = []; + // TODO 膨大なループが発生することが見込まれ商用には耐えないので、本実装時に修正予定 + for (let i = 0; i < dbUsers.length; i++) { + // Azure AD B2Cからユーザーを取得する + const aadb2cUser = await this.adB2cService.getUser( + dbUsers[i].external_id, + ); + + const user = new User(); + user.name = aadb2cUser.displayName; + user.role = dbUsers[i].role; + user.authorId = dbUsers[i].author_id; + // TODO DBから取得できるようになるため暫定 + user.typistGroupName = ''; + user.email = aadb2cUser.mail; + user.emailVerified = dbUsers[i].email_verified; + user.autoRenew = dbUsers[i].auto_renew; + user.licenseAlert = dbUsers[i].license_alert; + user.notification = dbUsers[i].notification; + users.push(user); + } + return users; + } catch (e) { + this.logger.error(`error=${e}`); + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.NOT_FOUND, + ); + } finally { + this.logger.log(`[OUT] ${this.getUsers.name}`); + } + } } diff --git a/dictation_server/src/gateways/adb2c/adb2c.service.ts b/dictation_server/src/gateways/adb2c/adb2c.service.ts index beaafe3..088b847 100644 --- a/dictation_server/src/gateways/adb2c/adb2c.service.ts +++ b/dictation_server/src/gateways/adb2c/adb2c.service.ts @@ -1,10 +1,10 @@ +import { ClientSecretCredential } from '@azure/identity'; +import { Client } from '@microsoft/microsoft-graph-client'; +import { TokenCredentialAuthenticationProvider } from '@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials'; import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import axios from 'axios'; -import { JwkSignKey, B2cMetadata } from '../../common/token'; -import { Client } from '@microsoft/microsoft-graph-client'; -import { TokenCredentialAuthenticationProvider } from '@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials'; -import { ClientSecretCredential } from '@azure/identity'; +import { Aadb2cUser, B2cMetadata, JwkSignKey } from '../../common/token'; export type ConflictError = { reason: 'email'; @@ -165,4 +165,22 @@ export class AdB2cService { this.logger.log(`[OUT] ${this.changePassword.name}`); } } + + /** + * Azure AD B2Cからユーザ情報を取得する + * @param externalId 外部ユーザーID + * @returns ユーザ情報 + */ + async getUser(externalId: string): Promise { + this.logger.log(`[IN] ${this.getUser.name}`); + + try { + return await this.graphClient.api(`users/${externalId}`).get(); + } catch (e) { + this.logger.error(e); + throw e; + } finally { + this.logger.log(`[OUT] ${this.getUser.name}`); + } + } } diff --git a/dictation_server/src/repositories/users/users.repository.service.ts b/dictation_server/src/repositories/users/users.repository.service.ts index 0b23501..8e1c40b 100644 --- a/dictation_server/src/repositories/users/users.repository.service.ts +++ b/dictation_server/src/repositories/users/users.repository.service.ts @@ -146,4 +146,21 @@ export class UsersRepositoryService { return await repo.update({ id: targetUser.id }, targetUser); }); } + + /** + * 同じアカウントIDを持つユーザーを探す + * @param userId + * @returns User[] + */ + async findSameAccountUsers(userId: number): Promise { + const dbUser = await this.dataSource + .getRepository(User) + .findOne({ where: [{ id: userId }] }); + + const dbUsers = await this.dataSource + .getRepository(User) + .find({ where: [{ account_id: dbUser.account_id }] }); + + return dbUsers; + } }