diff --git a/dictation_server/src/common/auth/auth.ts b/dictation_server/src/common/auth/auth.ts new file mode 100644 index 0000000..90d8479 --- /dev/null +++ b/dictation_server/src/common/auth/auth.ts @@ -0,0 +1,16 @@ +/** + * 権限の過不足を確認する + * @param {string[]} + * @return {boolean} + */ +export const confirmPermission = (authority: string): boolean => { + console.log(authority); + + return true; + // TODO 将来的にscopeの内容に応じた処理を入れることになる + //  if (authority.startsWith('hogehoge')) { + //    return true; + //  } else { + //    return false; + //  } +}; diff --git a/dictation_server/src/common/error/code.ts b/dictation_server/src/common/error/code.ts index 87fe6b2..e19c4ac 100644 --- a/dictation_server/src/common/error/code.ts +++ b/dictation_server/src/common/error/code.ts @@ -20,7 +20,11 @@ export const ErrorCodes = [ 'E000104', // トークン署名エラー 'E000105', // トークン発行元エラー 'E000106', // トークンアルゴリズムエラー + 'E000107', // トークン不足エラー + 'E000108', // トークン権限エラー 'E010201', // 未認証ユーザエラー 'E010202', // 認証済ユーザエラー + 'E010203', // 管理ユーザ権限エラー 'E010301', // メールアドレス登録済みエラー + 'E010302', // authorId重複エラー ] as const; diff --git a/dictation_server/src/common/error/message.ts b/dictation_server/src/common/error/message.ts index 0a10188..31f5be9 100644 --- a/dictation_server/src/common/error/message.ts +++ b/dictation_server/src/common/error/message.ts @@ -9,7 +9,11 @@ export const errors: Errors = { E000104: 'Token invalid signature Error.', E000105: 'Token invalid issuer Error.', E000106: 'Token invalid algorithm Error.', + E000107: 'Token is not exist Error.', + E000108: 'Token authority failed Error.', E010201: 'Email not verified user Error.', E010202: 'Email already verified user Error.', + E010203: 'Administrator Permissions Error.', E010301: 'This email user already created Error', + E010302: 'This AuthorId already used Error', }; diff --git a/dictation_server/src/common/http/helper.ts b/dictation_server/src/common/http/helper.ts new file mode 100644 index 0000000..660ceee --- /dev/null +++ b/dictation_server/src/common/http/helper.ts @@ -0,0 +1,17 @@ +import { Request } from 'express'; + +/** + * アクセストークンを取り出す + * @param {Request} + * @return {string | undefined} + */ +export const retrieveAccessToken = (req: Request): string | undefined => { + const header = req.header('Authorization'); + + if (typeof header === 'string') { + if (header.startsWith('Bearer ')) { + return header.substring('Bearer '.length, header.length); + } + } + return undefined; +}; 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 5f081e7..1cf4072 100644 --- a/dictation_server/src/features/users/test/users.service.mock.ts +++ b/dictation_server/src/features/users/test/users.service.mock.ts @@ -2,26 +2,30 @@ 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 { AdB2cService } from '../../../gateways/adb2c/adb2c.service'; -import { ConfigModule, ConfigService } from '@nestjs/config'; +import { + AdB2cService, + ConflictError, +} from '../../../gateways/adb2c/adb2c.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 { User } from 'src/repositories/users/entity/user.entity'; export type CryptoMockValue = { getPublicKey: string | Error; - getPrivateKey: string | Error; }; export type UsersRepositoryMockValue = { updateUserVerified: undefined | Error; findUserById: User | Error; + createNormalUser: User | Error; }; export type AdB2cMockValue = { getMetaData: B2cMetadata | Error; getSignKeySets: JwkSignKey[] | Error; changePassword: { sub: string } | Error; + createUser: string | ConflictError | Error; }; export type SendGridMockValue = { @@ -30,6 +34,9 @@ export type SendGridMockValue = { text: string; html: string; }; + createMailContentFromEmailConfirmForNormalUser: + | { subject: string; text: string; html: string } + | Error; sendMail: undefined | Error; }; @@ -51,6 +58,7 @@ export const makeDefaultAdB2cMockValue = (): AdB2cMockValue => { changePassword: { sub: 'TEST9999', }, + createUser: '001', }; }; @@ -73,7 +81,7 @@ export const makeSendGridServiceMock = (value: SendGridMockValue) => { }; export const makeAdB2cServiceMock = (value: AdB2cMockValue) => { - const { getMetaData, getSignKeySets, changePassword } = value; + const { getMetaData, getSignKeySets, changePassword, createUser } = value; return { getMetaData: @@ -92,14 +100,25 @@ export const makeAdB2cServiceMock = (value: AdB2cMockValue) => { : jest .fn, []>() .mockResolvedValue(changePassword), + createUser: + createUser instanceof Error + ? jest.fn, []>().mockRejectedValue(createUser) + : jest + .fn, []>() + .mockResolvedValue(createUser), }; }; +export type ConfigMockValue = { + get: string | Error; +}; + export const makeUsersServiceMock = async ( cryptoMockValue: CryptoMockValue, usersRepositoryMockValue: UsersRepositoryMockValue, adB2cMockValue: AdB2cMockValue, sendGridMockValue: SendGridMockValue, + configMockValue: ConfigMockValue, ): Promise => { const module: TestingModule = await Test.createTestingModule({ providers: [UsersService], @@ -118,10 +137,10 @@ export const makeUsersServiceMock = async ( return makeUsersRepositoryMock(usersRepositoryMockValue); case AdB2cService: return makeAdB2cServiceMock(adB2cMockValue); - case ConfigService: - return {}; case SendGridService: - return makeSendGridServiceMock(sendGridMockValue); + return makeSendGridMock(sendGridMockValue); + case ConfigService: + return makeConfigMock(configMockValue); } }) .compile(); @@ -130,23 +149,26 @@ export const makeUsersServiceMock = async ( }; export const makeCryptoServiceMock = (value: CryptoMockValue) => { - const { getPublicKey, getPrivateKey } = value; + const { getPublicKey } = value; return { getPublicKey: getPublicKey instanceof Error ? jest.fn, []>().mockRejectedValue(getPublicKey) : jest.fn, []>().mockResolvedValue(getPublicKey), - getPrivateKey: - getPrivateKey instanceof Error - ? jest.fn, []>().mockRejectedValue(getPrivateKey) - : jest.fn, []>().mockResolvedValue(getPrivateKey), }; }; +class authorIdError extends Error { + constructor(public code: string, e?: string) { + super(e); + } +} + export const makeUsersRepositoryMock = (value: UsersRepositoryMockValue) => { - const { updateUserVerified } = value; - const { findUserById } = value; + const { updateUserVerified, findUserById, createNormalUser } = value; + + const aIdError = new authorIdError('ER_DUP_ENTRY'); return { updateUserVerified: @@ -155,11 +177,45 @@ export const makeUsersRepositoryMock = (value: UsersRepositoryMockValue) => { : jest.fn, []>().mockResolvedValue(updateUserVerified), findUserById: findUserById instanceof Error - ? jest.fn, []>().mockRejectedValue(findUserById) + ? jest.fn, []>().mockRejectedValue(findUserById) : jest.fn, []>().mockResolvedValue(findUserById), + createNormalUser: + createNormalUser instanceof Error + ? createNormalUser.name == 'ER_DUP_ENTRY' + ? jest.fn, []>().mockRejectedValue(aIdError) + : jest.fn, []>().mockRejectedValue(createNormalUser) + : jest.fn, []>().mockResolvedValue(createNormalUser), }; }; +export const makeSendGridMock = (value: SendGridMockValue) => { + const { sendMail, createMailContentFromEmailConfirmForNormalUser } = value; + + return { + sendMail: + sendMail instanceof Error + ? jest.fn, []>().mockRejectedValue(sendMail) + : jest.fn, []>().mockResolvedValue(sendMail), + createMailContentFromEmailConfirmForNormalUser: + createMailContentFromEmailConfirmForNormalUser instanceof Error + ? jest + .fn, []>() + .mockRejectedValue(createMailContentFromEmailConfirmForNormalUser) + : jest + .fn, []>() + .mockResolvedValue(createMailContentFromEmailConfirmForNormalUser), + }; +}; +export const makeConfigMock = (value: ConfigMockValue) => { + const { get } = value; + + return { + get: + get instanceof Error + ? jest.fn, []>().mockRejectedValue(get) + : jest.fn, []>().mockResolvedValue(get), + }; +}; export const makeDefaultCryptoMockValue = (): CryptoMockValue => { return { getPublicKey: [ @@ -173,51 +229,34 @@ export const makeDefaultCryptoMockValue = (): CryptoMockValue => { 'OQIDAQAB', '-----END PUBLIC KEY-----', ].join('\n'), - // XXX メール認証API実装時に用意したが使っていないので不要なら削除 - getPrivateKey: [ - '-----BEGIN RSA PRIVATE KEY-----', - 'MIIEowIBAAKCAQEA5IZZNgDew9eGmuFTezwdHYLSaJvUPPIKYoiOeVLD1paWNI51', - '7Vkaoh0ngprcKOdv6T1N07V4igK7mOim2zY3yCTR6wcWR3PfFJrl9vh5SOo79koZ', - 'oJb27YiM4jtxfx2dezzp0T2GoNR5rRolPUbWFJXnDe0DVXYXpJLb4LAlF2XAyYX0', - 'SYKUVUsJnzm5k4xbXtnwPwVbpm0EdswBE6qSfiL9zWk9dvHoKzSnfSDzDFoFcEoV', - 'chawzYXf/MM1YR4wo5XyzECc6Q5Ah4z522//mBNNaDHv83Yuw3mGShT73iJ0JQdk', - 'Tturshv2Ecma38r6ftrIwNYXw4VVatJM8+GOOQIDAQABAoIBADrwp7u097+dK/tw', - 'WD61n3DIGAqg/lmFt8X4IH8MKLSE/FKr16CS1bqwOEuIM3ZdUtDeXd9Xs7IsyEPE', - '5ZwuXK7DSF0M4+Mj8Ip49Q0Aww9aUoLQU9HGfgN/r4599GTrt31clZXA/6Mlighq', - 'cOZgCcEfdItz8OMu5SQuOIW4CKkCuaWnPOP26UqZocaXNZfpZH0iFLATMMH/TT8x', - 'ay9ToHTQYE17ijdQ/EOLSwoeDV1CU1CIE3P4YfLJjvpKptly5dTevriHEzBi70Jx', - '/KEPUn9Jj2gZafrUxRVhmMbm1zkeYxL3gsqRuTzRjEeeILuZhSJyCkQZyUNARxsg', - 'QY4DZfECgYEA+YLKUtmYTx60FS6DJ4s31TAsXY8kwhq/lB9E3GBZKDd0DPayXEeK', - '4UWRQDTT6MI6fedW69FOZJ5sFLp8HQpcssb4Weq9PCpDhNTx8MCbdH3Um5QR3vfW', - 'aKq/1XM8MDUnx5XcNYd87Aw3azvJAvOPr69as8IPnj6sKaRR9uQjbYUCgYEA6nfV', - '5j0qmn0EJXZJblk4mvvjLLoWSs17j9YlrZJlJxXMDFRYtgnelv73xMxOMvcGoxn5', - 'ifs7dpaM2x5EmA6jVU5sYaB/beZGEPWqPYGyjIwXPvUGAAv8Gbnvpp+xlSco/Dum', - 'Iq0w+43ry5/xWh6CjfrvKV0J2bDOiJwPEdu/8iUCgYEAnBBSvL+dpN9vhFAzeOh7', - 'Y71eAqcmNsLEUcG9MJqTKbSFwhYMOewF0iHRWHeylEPokhfBJn8kqYrtz4lVWFTC', - '5o/Nh3BsLNXCpbMMIapXkeWiti1HgE9ErPMgSkJpwz18RDpYIqM8X+jEQS6D7HSr', - 'yxfDg+w+GJza0rEVE3hfMIECgYBw+KZ2VfhmEWBjEHhXE+QjQMR3s320MwebCUqE', - 'NCpKx8TWF/naVC0MwfLtvqbbBY0MHyLN6d//xpA9r3rLbRojqzKrY2KiuDYAS+3n', - 'zssRzxoQOozWju+8EYu30/ADdqfXyIHG6X3VZs87AGiQzGyJLmP3oR1y5y7MQa09', - 'JI16hQKBgHK5uwJhGa281Oo5/FwQ3uYLymbNwSGrsOJXiEu2XwJEXwVi2ELOKh4/', - '03pBk3Kva3fIwEK+vCzDNnxShIQqBE76/2I1K1whOfoUehhYvKHGaXl2j70Zz9Ks', - 'rkGW1cx7p+yDqATDrwHBHTHFh5bUTTn8dN40n0e0W/llurpbBkJM', - '-----END RSA PRIVATE KEY-----', - ].join('\n'), + }; +}; + +export const makeDefaultSendGridlValue = (): SendGridMockValue => { + return { + sendMail: undefined, + createMailContentFromEmailConfirm: { subject: '', text: '', html: '' }, + createMailContentFromEmailConfirmForNormalUser: { + subject: 'test', + text: 'test', + html: 'test', + }, + }; +}; +export const makeDefaultConfigValue = (): ConfigMockValue => { + return { + get: `test@example.co.jp`, }; }; // 個別のテストケースに対応してそれぞれのMockを用意するのは無駄が多いのでテストケース内で個別の値を設定する export const makeDefaultUsersRepositoryMockValue = (): UsersRepositoryMockValue => { + const newUser = new User(); + newUser.id = 111; return { updateUserVerified: undefined, - findUserById: undefined, + findUserById: newUser, + createNormalUser: newUser, }; }; - -export const makeSendGridServiceMockValue = (): SendGridMockValue => { - return { - createMailContentFromEmailConfirm: { subject: '', text: '', html: '' }, - sendMail: undefined, - }; -}; diff --git a/dictation_server/src/features/users/users.controller.ts b/dictation_server/src/features/users/users.controller.ts index 1cba98a..d66bdb8 100644 --- a/dictation_server/src/features/users/users.controller.ts +++ b/dictation_server/src/features/users/users.controller.ts @@ -1,4 +1,12 @@ -import { Body, Controller, Get, HttpStatus, Post, Req } from '@nestjs/common'; +import { + Body, + Controller, + Get, + HttpStatus, + Post, + Req, + HttpException, +} from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, @@ -16,11 +24,20 @@ import { } 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') export class UsersController { - constructor(private readonly usersService: UsersService) {} + constructor( + private readonly usersService: UsersService, + private readonly cryptoService: CryptoService, + ) {} @ApiResponse({ status: HttpStatus.OK, @@ -119,8 +136,57 @@ export class UsersController { @Req() req: Request, @Body() body: SignupRequest, ): Promise { - console.log(req.header('Authorization')); - console.log(body); + const { + name, + role, + email, + autoRenew, + licenseAlert, + notification, + authorId, + typistGroupId, + } = body; + + // アクセストークンにより権限を確認する + 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, + ); + } + + //ユーザ作成処理 + await this.usersService.createUser( + payload, + name, + role, + email, + autoRenew, + licenseAlert, + notification, + authorId, + typistGroupId, + ); return {}; } diff --git a/dictation_server/src/features/users/users.service.spec.ts b/dictation_server/src/features/users/users.service.spec.ts index 90132aa..2d66c70 100644 --- a/dictation_server/src/features/users/users.service.spec.ts +++ b/dictation_server/src/features/users/users.service.spec.ts @@ -5,21 +5,25 @@ import { makeDefaultUsersRepositoryMockValue, makeUsersServiceMock, makeDefaultAdB2cMockValue, - makeSendGridServiceMockValue, + makeDefaultSendGridlValue, + makeDefaultConfigValue, } from './test/users.service.mock'; import { EmailAlreadyVerifiedError } from '../../repositories/users/users.repository.service'; +import { AccessToken } from 'src/common/token'; describe('UsersService', () => { it('ユーザの仮登録時に払い出されるトークンにより、未認証のユーザが認証済みになる', async () => { const cryptoMockValue = makeDefaultCryptoMockValue(); const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); const adb2cParam = makeDefaultAdB2cMockValue(); - const sendGridMockValue = makeSendGridServiceMockValue(); + 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'; @@ -40,14 +44,19 @@ describe('UsersService', () => { created_at: new Date(), updated_by: 'string;', updated_at: new Date(), + auto_renew: true, + license_alert: true, + notification: true, }; const adb2cParam = makeDefaultAdB2cMockValue(); - const sendGridMockValue = makeSendGridServiceMockValue(); + const configMockValue = makeDefaultConfigValue(); + const sendGridMockValue = makeDefaultSendGridlValue(); const service = await makeUsersServiceMock( cryptoMockValue, usersRepositoryMockValue, adb2cParam, sendGridMockValue, + configMockValue, ); const token = @@ -59,12 +68,14 @@ describe('UsersService', () => { const cryptoMockValue = makeDefaultCryptoMockValue(); const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); const adb2cParam = makeDefaultAdB2cMockValue(); - const sendGridMockValue = makeSendGridServiceMockValue(); + const sendgridMockValue = makeDefaultSendGridlValue(); + const configMockValue = makeDefaultConfigValue(); const service = await makeUsersServiceMock( cryptoMockValue, usersRepositoryMockValue, adb2cParam, - sendGridMockValue, + sendgridMockValue, + configMockValue, ); const token = 'invalid.id.token'; await expect(service.confirmUser(token)).rejects.toEqual( @@ -86,14 +97,19 @@ describe('UsersService', () => { created_at: new Date(), updated_by: 'string;', updated_at: new Date(), + auto_renew: true, + license_alert: true, + notification: true, }; const adb2cParam = makeDefaultAdB2cMockValue(); - const sendGridMockValue = makeSendGridServiceMockValue(); + const sendGridMockValue = makeDefaultSendGridlValue(); + const configMockValue = makeDefaultConfigValue(); const service = await makeUsersServiceMock( cryptoMockValue, usersRepositoryMockValue, adb2cParam, sendGridMockValue, + configMockValue, ); const token = 'invalid.id.token'; await expect(service.confirmUserAndInitPassword(token)).rejects.toEqual( @@ -104,7 +120,9 @@ describe('UsersService', () => { const cryptoMockValue = makeDefaultCryptoMockValue(); const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); const adb2cParam = makeDefaultAdB2cMockValue(); - const sendGridMockValue = makeSendGridServiceMockValue(); + const sendgridMockValue = makeDefaultSendGridlValue(); + const configMockValue = makeDefaultConfigValue(); + usersRepositoryMockValue.updateUserVerified = new EmailAlreadyVerifiedError(); @@ -112,7 +130,8 @@ describe('UsersService', () => { cryptoMockValue, usersRepositoryMockValue, adb2cParam, - sendGridMockValue, + sendgridMockValue, + configMockValue, ); const token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw'; @@ -134,9 +153,13 @@ describe('UsersService', () => { created_at: new Date(), updated_by: 'string;', updated_at: new Date(), + auto_renew: true, + license_alert: true, + notification: true, }; const adb2cParam = makeDefaultAdB2cMockValue(); - const sendGridMockValue = makeSendGridServiceMockValue(); + const sendGridMockValue = makeDefaultSendGridlValue(); + const configMockValue = makeDefaultConfigValue(); usersRepositoryMockValue.updateUserVerified = new EmailAlreadyVerifiedError(); @@ -145,6 +168,7 @@ describe('UsersService', () => { usersRepositoryMockValue, adb2cParam, sendGridMockValue, + configMockValue, ); const token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw'; @@ -156,14 +180,16 @@ describe('UsersService', () => { const cryptoMockValue = makeDefaultCryptoMockValue(); const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); const adb2cParam = makeDefaultAdB2cMockValue(); - const sendGridMockValue = makeSendGridServiceMockValue(); + const sendgridMockValue = makeDefaultSendGridlValue(); + const configMockValue = makeDefaultConfigValue(); usersRepositoryMockValue.updateUserVerified = new Error('DB error'); const service = await makeUsersServiceMock( cryptoMockValue, usersRepositoryMockValue, adb2cParam, - sendGridMockValue, + sendgridMockValue, + configMockValue, ); const token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw'; @@ -188,16 +214,20 @@ describe('UsersService', () => { created_at: new Date(), updated_by: 'string;', updated_at: new Date(), + auto_renew: true, + license_alert: true, + notification: true, }; const adb2cParam = makeDefaultAdB2cMockValue(); - const sendGridMockValue = makeSendGridServiceMockValue(); + const sendGridMockValue = makeDefaultSendGridlValue(); usersRepositoryMockValue.updateUserVerified = new Error('DB error'); - + const configMockValue = makeDefaultConfigValue(); const service = await makeUsersServiceMock( cryptoMockValue, usersRepositoryMockValue, adb2cParam, sendGridMockValue, + configMockValue, ); const token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw'; @@ -208,4 +238,262 @@ describe('UsersService', () => { ), ); }); + it('管理者権限のあるアクセストークンを使用して、新規ユーザが追加される(role:None)', 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 name = 'test_user1'; + const role = 'None'; + const email = 'test1@example.co.jp'; + const autoRenew = true; + const licenseAlert = true; + const notification = true; + const token: AccessToken = { userId: '0001', scope: '' }; + expect( + await service.createUser( + token, + name, + role, + email, + autoRenew, + licenseAlert, + notification, + ), + ).toEqual(undefined); + }); +}); + +describe('UsersService', () => { + it('管理者権限のあるアクセストークンを使用して、新規ユーザが追加される(role:Author)', 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 name = 'test_user2'; + const role = 'Author'; + const email = 'test2@example.co.jp'; + const autoRenew = true; + const licenseAlert = true; + const notification = true; + const authorId = 'testID'; + const token: AccessToken = { userId: '0001', scope: '' }; + expect( + await service.createUser( + token, + name, + role, + email, + autoRenew, + licenseAlert, + notification, + authorId, + ), + ).toEqual(undefined); + }); +}); + +describe('UsersService', () => { + it('管理者権限のあるアクセストークンを使用して、新規ユーザが追加される(role:Transcriptioninst)', 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 name = 'test_user3'; + const role = 'Transcriptioninst'; + const email = 'test3@example.co.jp'; + const autoRenew = true; + const licenseAlert = true; + const notification = true; + const typistGroupId = 111; + const token: AccessToken = { userId: '0001', scope: '' }; + expect( + await service.createUser( + token, + name, + role, + email, + autoRenew, + licenseAlert, + notification, + undefined, + typistGroupId, + ), + ).toEqual(undefined); + }); +}); + +it('DBネットワークエラーとなる場合、エラーとなる。', async () => { + const cryptoMockValue = makeDefaultCryptoMockValue(); + const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const adb2cParam = makeDefaultAdB2cMockValue(); + const sendgridMockValue = makeDefaultSendGridlValue(); + const configMockValue = makeDefaultConfigValue(); + usersRepositoryMockValue.createNormalUser = new Error('DB error'); + const service = await makeUsersServiceMock( + cryptoMockValue, + usersRepositoryMockValue, + adb2cParam, + sendgridMockValue, + configMockValue, + ); + const name = 'test_user5'; + const role = 'Transcriptioninst'; + const email = 'test5@example.co.jp'; + const autoRenew = true; + const licenseAlert = true; + const notification = true; + const token: AccessToken = { userId: '0001', scope: '' }; + await expect( + service.createUser( + token, + name, + role, + email, + autoRenew, + licenseAlert, + notification, + ), + ).rejects.toEqual( + new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ), + ); +}); +it('Azure ADB2Cでネットワークエラーとなる場合、エラーとなる。', async () => { + const cryptoMockValue = makeDefaultCryptoMockValue(); + const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const adb2cParam = makeDefaultAdB2cMockValue(); + adb2cParam.createUser = new Error(); + const sendgridMockValue = makeDefaultSendGridlValue(); + const configMockValue = makeDefaultConfigValue(); + const service = await makeUsersServiceMock( + cryptoMockValue, + usersRepositoryMockValue, + adb2cParam, + sendgridMockValue, + configMockValue, + ); + const name = 'test_user6'; + const role = 'Transcriptioninst'; + const email = 'test6@example.co.jp'; + const autoRenew = true; + const licenseAlert = true; + const notification = true; + const token: AccessToken = { userId: '0001', scope: '' }; + await expect( + service.createUser( + token, + name, + role, + email, + autoRenew, + licenseAlert, + notification, + ), + ).rejects.toEqual( + new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ), + ); +}); +it('メールアドレスが重複している場合、エラーとなる。', async () => { + const cryptoMockValue = makeDefaultCryptoMockValue(); + const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const adb2cParam = makeDefaultAdB2cMockValue(); + adb2cParam.createUser = { reason: 'email', message: 'ObjectConflict' }; + const sendgridMockValue = makeDefaultSendGridlValue(); + const configMockValue = makeDefaultConfigValue(); + const service = await makeUsersServiceMock( + cryptoMockValue, + usersRepositoryMockValue, + adb2cParam, + sendgridMockValue, + configMockValue, + ); + const name = 'test_user7'; + const role = 'Transcriptioninst'; + const email = 'test7@example.co.jp'; + const autoRenew = true; + const licenseAlert = true; + const notification = true; + const token: AccessToken = { userId: '0001', scope: '' }; + await expect( + service.createUser( + token, + name, + role, + email, + autoRenew, + licenseAlert, + notification, + ), + ).rejects.toEqual( + new HttpException(makeErrorResponse('E010301'), HttpStatus.BAD_REQUEST), + ); +}); +it('AuthorIDが重複している場合、エラーとなる。', async () => { + const cryptoMockValue = makeDefaultCryptoMockValue(); + const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const adb2cParam = makeDefaultAdB2cMockValue(); + const sendgridMockValue = makeDefaultSendGridlValue(); + const configMockValue = makeDefaultConfigValue(); + usersRepositoryMockValue.createNormalUser = new Error(); + usersRepositoryMockValue.createNormalUser.name = 'ER_DUP_ENTRY'; + + const service = await makeUsersServiceMock( + cryptoMockValue, + usersRepositoryMockValue, + adb2cParam, + sendgridMockValue, + configMockValue, + ); + const name = 'test_user8'; + const role = 'Author'; + const email = 'test8@example.co.jp'; + const autoRenew = true; + const licenseAlert = true; + const notification = true; + const authorId = 'testID'; + const token: AccessToken = { userId: '0001', scope: '' }; + await expect( + service.createUser( + token, + name, + role, + email, + autoRenew, + licenseAlert, + notification, + authorId, + ), + ).rejects.toEqual( + new HttpException(makeErrorResponse('E010302'), HttpStatus.BAD_REQUEST), + ); }); diff --git a/dictation_server/src/features/users/users.service.ts b/dictation_server/src/features/users/users.service.ts index ffc8ffa..b91e20e 100644 --- a/dictation_server/src/features/users/users.service.ts +++ b/dictation_server/src/features/users/users.service.ts @@ -6,10 +6,16 @@ import { EmailAlreadyVerifiedError, } from '../../repositories/users/users.repository.service'; import { makeErrorResponse } from '../../common/error/makeErrorResponse'; -import { makePassword } from '../../common/password/password'; -import { AdB2cService } from '../../gateways/adb2c/adb2c.service'; +import { + AdB2cService, + ConflictError, + isConflictError, +} from '../../gateways/adb2c/adb2c.service'; import { ConfigService } from '@nestjs/config'; 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'; @Injectable() export class UsersService { @@ -72,6 +78,127 @@ export class UsersService { } } + /** + * ユーザを作成する + * @param token + * @returns account + */ + async createUser( + accessToken: AccessToken, + name: string, + role: string, + email: string, + autoRenew: boolean, + licenseAlert: boolean, + notification: boolean, + authorId?: string | undefined, + groupID?: number | undefined, + ): Promise { + //アクセストークンからユーザーIDを取得する + // TODO アクセストークンの中身が具体的に確定したら、型変換を取り払う必要があるかも + this.logger.log(`[IN] ${this.createUser.name}`); + const userId = Number(accessToken.userId); + + //DBよりアクセス者の所属するアカウントIDを取得する + let adminUser: User; + try { + adminUser = await this.usersRepository.findUserById(userId); + } catch (e) { + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + const accountId = adminUser.account_id; + + // ランダムなパスワードを生成する + const ramdomPassword = makePassword(); + + //Azure AD B2Cにユーザーを新規登録する + let externalUser: { sub: string } | ConflictError; + try { + // idpにユーザーを作成 + externalUser = await this.adB2cService.createUser( + email, + ramdomPassword, + name, + ); + } catch (e) { + console.log('create externalUser failed'); + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + // メールアドレス重複エラー + if (isConflictError(externalUser)) { + throw new HttpException( + makeErrorResponse('E010301'), + HttpStatus.BAD_REQUEST, + ); + } + + //Azure AD B2Cに登録したユーザー情報のID(sub)と受け取った情報を使ってDBにユーザーを登録する + let newUser: User; + // TODO 本来はNULLだが、テーブル定義に誤ってNOTNULLが付いているため、一時的に適当な値を設定 + const accepted_terms_version = 'xxx'; + try { + // ユーザ作成 + newUser = await this.usersRepository.createNormalUser( + accountId, + externalUser.sub, + role, + autoRenew, + licenseAlert, + notification, + authorId, + accepted_terms_version, + ); + } catch (e) { + console.log('create user failed'); + switch (e.code) { + case 'ER_DUP_ENTRY': + //AuthorID重複エラー + throw new HttpException( + makeErrorResponse('E010302'), + HttpStatus.BAD_REQUEST, + ); + default: + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + //Email送信用のコンテンツを作成する + try { + // メールの送信元を取得 + const from = this.configService.get('MAIL_FROM') ?? ''; + + // メールの内容を構成 + const { subject, text, html } = + await this.sendgridService.createMailContentFromEmailConfirmForNormalUser( + accountId, + newUser.id, + email, + ); + + //SendGridAPIを呼び出してメールを送信する + await this.sendgridService.sendMail(email, from, subject, text, html); + } catch (e) { + console.log('create user failed'); + console.log(`[NOT IMPLEMENT] [RECOVER] delete user: ${newUser.id}`); + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + this.logger.log(`[OUT] ${this.createUser.name}`); + return; + } /** * confirm User And Init Password * @param token ユーザ仮登録時に払いだされるトークン diff --git a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts index b5ffe81..c1975b5 100644 --- a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts +++ b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts @@ -47,6 +47,43 @@ export class SendGridService { }; } + /** + * Email認証用のメールコンテンツを作成する(一般ユーザ向け) + * @param accountId 認証対象のユーザーが所属するアカウントのID + * @param userId 認証対象のユーザーのID + * @param email 認証対象のユーザーのメールアドレス + * @returns メールのサブジェクトとコンテンツ + */ + //TODO 中身が管理ユーザ向けのままなので、修正の必要あり + async createMailContentFromEmailConfirmForNormalUser( + accountId: number, + userId: number, + email: string, + ): Promise<{ subject: string; text: string; html: string }> { + const lifetime = + this.configService.get('EMAIL_CONFIRM_LIFETIME') ?? 0; + const privateKey = + this.configService.get('JWT_PRIVATE_KEY')?.replace('\\n', '\n') ?? + ''; + const token = sign<{ accountId: number; userId: number; email: string }>( + { + accountId, + userId, + email, + }, + lifetime, + privateKey, + ); + const domains = this.configService.get('APP_DOMAIN'); + const path = 'mail-confirm/'; + + return { + subject: 'Verify your new account', + text: `The verification URL. ${domains}${path}?verify=${token}`, + html: `

The verification URL.

${domains}${path}?verify=${token}"`, + }; + } + /** * メールを送信する * @param accountId アカウントID diff --git a/dictation_server/src/repositories/users/entity/user.entity.ts b/dictation_server/src/repositories/users/entity/user.entity.ts index f6610ed..21961c0 100644 --- a/dictation_server/src/repositories/users/entity/user.entity.ts +++ b/dictation_server/src/repositories/users/entity/user.entity.ts @@ -29,6 +29,15 @@ export class User { @Column() email_verified: boolean; + @Column() + auto_renew: boolean; + + @Column() + license_alert: boolean; + + @Column() + notification: boolean; + @Column('timestamp', { nullable: true }) deleted_at?: Date; diff --git a/dictation_server/src/repositories/users/users.repository.service.ts b/dictation_server/src/repositories/users/users.repository.service.ts index 33e585d..0b23501 100644 --- a/dictation_server/src/repositories/users/users.repository.service.ts +++ b/dictation_server/src/repositories/users/users.repository.service.ts @@ -5,6 +5,7 @@ import { User } from './entity/user.entity'; // UsersRepositoryServiceで発生するエラーを定義 export class EmailAlreadyVerifiedError extends Error {} export class UserNotFoundError extends Error {} +export class AuthorIdAlreadyExistError extends Error {} @Injectable() export class UsersRepositoryService { @@ -35,6 +36,49 @@ export class UsersRepositoryService { return createdEntity; } + /** + * 一般ユーザを作成する + * @param account_id + * @param external_id + * @param role + * @param autoRenew + * @param licenseAlert + * @param notification + * @param authorId + * @returns User + */ + async createNormalUser( + accountId: number, + externalUserId: string, + role: string, + auto_renew: boolean, + license_alert: boolean, + notification: boolean, + author_id: string, + accepted_terms_version: string, + ): Promise { + const user = new User(); + + user.role = role; + user.account_id = accountId; + user.external_id = externalUserId; + user.auto_renew = auto_renew; + user.license_alert = license_alert; + user.notification = notification; + user.author_id = author_id; + user.accepted_terms_version = accepted_terms_version; + + const createdEntity = await this.dataSource.transaction( + async (entityManager) => { + const repo = entityManager.getRepository(User); + const newUser = repo.create(user); + const persisted = await repo.save(newUser); + return persisted; + }, + ); + return createdEntity; + } + async findVerifiedUser(sub: string): Promise { const user = await this.dataSource.getRepository(User).findOne({ where: {