Merged PR 71: API実装(ユーザー登録)

## 概要
[Task1593: API実装(ユーザー登録)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/1593)

https://dev.azure.com/ODMSCloud/ODMS%20Cloud/_git/ODMS%20Cloud
users.controllerにアクセストークン取得処理を追加
users.serviceにユーザ追加処理を追加
user.entityにauto_renew、license_alert、notificationを追加
users.repository.serviceにユーザ追加・AuthorId検索処理を追加

## レビューポイント
処理の記載場所が適切かどうか
期待通りの処理になっているかどうか
テストコードの記載方法が正しいかどうか

## UIの変更
なし

## 動作確認状況
ローカルでのビルド・実行を確認

## 補足
テスト実装について不安要素があります。
・テストの粒度はこれでよいのでしょうか?
・テスト実行に40分かかってしまうのですが実装方法を間違えている箇所がありそうでしょうか?
This commit is contained in:
oura.a 2023-05-11 09:05:54 +00:00
parent d297301212
commit e9af39bd47
11 changed files with 724 additions and 73 deletions

View File

@ -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;
//  }
};

View File

@ -20,7 +20,11 @@ export const ErrorCodes = [
'E000104', // トークン署名エラー
'E000105', // トークン発行元エラー
'E000106', // トークンアルゴリズムエラー
'E000107', // トークン不足エラー
'E000108', // トークン権限エラー
'E010201', // 未認証ユーザエラー
'E010202', // 認証済ユーザエラー
'E010203', // 管理ユーザ権限エラー
'E010301', // メールアドレス登録済みエラー
'E010302', // authorId重複エラー
] as const;

View File

@ -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',
};

View File

@ -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;
};

View File

@ -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<Promise<{ sub: string }>, []>()
.mockResolvedValue(changePassword),
createUser:
createUser instanceof Error
? jest.fn<Promise<ConflictError>, []>().mockRejectedValue(createUser)
: jest
.fn<Promise<string | ConflictError>, []>()
.mockResolvedValue(createUser),
};
};
export type ConfigMockValue = {
get: string | Error;
};
export const makeUsersServiceMock = async (
cryptoMockValue: CryptoMockValue,
usersRepositoryMockValue: UsersRepositoryMockValue,
adB2cMockValue: AdB2cMockValue,
sendGridMockValue: SendGridMockValue,
configMockValue: ConfigMockValue,
): Promise<UsersService> => {
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<Promise<void>, []>().mockRejectedValue(getPublicKey)
: jest.fn<Promise<string>, []>().mockResolvedValue(getPublicKey),
getPrivateKey:
getPrivateKey instanceof Error
? jest.fn<Promise<void>, []>().mockRejectedValue(getPrivateKey)
: jest.fn<Promise<string>, []>().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<Promise<void>, []>().mockResolvedValue(updateUserVerified),
findUserById:
findUserById instanceof Error
? jest.fn<Promise<User>, []>().mockRejectedValue(findUserById)
? jest.fn<Promise<void>, []>().mockRejectedValue(findUserById)
: jest.fn<Promise<User>, []>().mockResolvedValue(findUserById),
createNormalUser:
createNormalUser instanceof Error
? createNormalUser.name == 'ER_DUP_ENTRY'
? jest.fn<Promise<void>, []>().mockRejectedValue(aIdError)
: jest.fn<Promise<void>, []>().mockRejectedValue(createNormalUser)
: jest.fn<Promise<User>, []>().mockResolvedValue(createNormalUser),
};
};
export const makeSendGridMock = (value: SendGridMockValue) => {
const { sendMail, createMailContentFromEmailConfirmForNormalUser } = value;
return {
sendMail:
sendMail instanceof Error
? jest.fn<Promise<void>, []>().mockRejectedValue(sendMail)
: jest.fn<Promise<void>, []>().mockResolvedValue(sendMail),
createMailContentFromEmailConfirmForNormalUser:
createMailContentFromEmailConfirmForNormalUser instanceof Error
? jest
.fn<Promise<void>, []>()
.mockRejectedValue(createMailContentFromEmailConfirmForNormalUser)
: jest
.fn<Promise<{ subject: string; text: string; html: string }>, []>()
.mockResolvedValue(createMailContentFromEmailConfirmForNormalUser),
};
};
export const makeConfigMock = (value: ConfigMockValue) => {
const { get } = value;
return {
get:
get instanceof Error
? jest.fn<Promise<void>, []>().mockRejectedValue(get)
: jest.fn<Promise<string>, []>().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,
};
};

View File

@ -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<SignupResponse> {
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>(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 {};
}

View File

@ -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),
);
});

View File

@ -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<void> {
//アクセストークンからユーザー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<string>('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

View File

@ -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<number>('EMAIL_CONFIRM_LIFETIME') ?? 0;
const privateKey =
this.configService.get<string>('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<string>('APP_DOMAIN');
const path = 'mail-confirm/';
return {
subject: 'Verify your new account',
text: `The verification URL. ${domains}${path}?verify=${token}`,
html: `<p>The verification URL.<p><a href="${domains}${path}?verify=${token}">${domains}${path}?verify=${token}"</a>`,
};
}
/**
*
* @param accountId ID

View File

@ -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;

View File

@ -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<User> {
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<User | undefined> {
const user = await this.dataSource.getRepository(User).findOne({
where: {