Merged PR 82: API実装(メール認証)
## 概要 [Task1594: API実装(メール認証)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/1594) - メール認証APIを作成 - src/api/common/にpasswordを追加(ランダムパスワード発行ロジック) - src/fuatures/gateway/adb2c.service.tsにユーザのパスワードを変更するメソッドchangePasswordを追加 - user.service.spec.tsにメール認証と仮パスワード発行のテストケースを追加 - 影響範囲 (user.service.spec.tsで行っていた既存のテストケース) ## レビューポイント - commonにpasswordを追加したが、配置として適切かどうか - user.service.tsのエラー発生時のロジックが十分であるか ## 動作確認状況 - ローカルで確認
This commit is contained in:
parent
94ab0e5b0d
commit
d297301212
3
dictation_server/src/common/password/index.ts
Normal file
3
dictation_server/src/common/password/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { makePassword } from './password';
|
||||
|
||||
export { makePassword };
|
||||
45
dictation_server/src/common/password/password.ts
Normal file
45
dictation_server/src/common/password/password.ts
Normal file
@ -0,0 +1,45 @@
|
||||
export const makePassword = (): string => {
|
||||
// パスワードの文字数を決定
|
||||
const passLength = 8;
|
||||
|
||||
// パスワードに使用可能な文字を決定(今回はアルファベットの大文字と小文字 + 数字 + symbolsの記号)
|
||||
const lowerCase = 'abcdefghijklmnopqrstuvwxyz';
|
||||
const upperCase = lowerCase.toLocaleUpperCase();
|
||||
const numbers = '0123456789';
|
||||
const symbols = "!@#$%^&*()+-={}[]:;'<>,./?_∼\\";
|
||||
const chars = lowerCase + upperCase + numbers + symbols;
|
||||
|
||||
// 英字の大文字、英字の小文字、アラビア数字、記号(!@#$%^&*()+-={}[]:;'<>,./?_∼\)から2種類以上組み合わせ
|
||||
const charaTypePattern =
|
||||
/^((?=.*[a-z])(?=.*[A-Z])|(?=.*[a-z])(?=.*[\d])|(?=.*[a-z])(?=.*[!@#$%^&*()+-={}:;'<>,./?_~[\\\]])|(?=.*[A-Z])(?=.*[\d])|(?=.*[A-Z])(?=.*[!@#$%^&*()+-={}:;'<>,./?_~[\\\]])|(?=.*[\d])(?=.*[!@#$%^&*()+-={}:;'<>,./?_~[\\\]]))[a-zA-Z\d!@#$%^&*()+-={}:;'<>,./?_~[\\\]]/;
|
||||
|
||||
// 同じ文字の3連続は禁止
|
||||
const repeatPattern = /(.)\1{2,}/;
|
||||
|
||||
// 特定文字列は禁止
|
||||
const unavailableCharaPattern =
|
||||
/password|passwd|test|admin|administrator|sysadmin|0123|1234|2345|3456|4567|5678|6789|9876|8765|7654|6543|5432|4321|3210/;
|
||||
|
||||
// autoGeneratedPasswordが以上の条件を満たせばvalidがtrueになる
|
||||
let valid = false;
|
||||
let autoGeneratedPassword: string;
|
||||
|
||||
while (!valid) {
|
||||
autoGeneratedPassword = '';
|
||||
// パスワードをランダムに決定
|
||||
while (autoGeneratedPassword.length < passLength) {
|
||||
// 上で決定したcharsの中からランダムに1文字ずつ追加
|
||||
const index = Math.floor(Math.random() * chars.length);
|
||||
autoGeneratedPassword += chars[index];
|
||||
}
|
||||
|
||||
// パスワードが上で決定した条件をすべて満たしているかチェック
|
||||
// 条件を満たすまでループ
|
||||
valid =
|
||||
autoGeneratedPassword.length == passLength &&
|
||||
charaTypePattern.test(autoGeneratedPassword) &&
|
||||
!repeatPattern.test(autoGeneratedPassword) &&
|
||||
!unavailableCharaPattern.test(autoGeneratedPassword);
|
||||
}
|
||||
return autoGeneratedPassword;
|
||||
};
|
||||
@ -2,21 +2,113 @@ 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 { SendGridService } from '../../../gateways/sendgrid/sendgrid.service';
|
||||
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;
|
||||
};
|
||||
|
||||
export type AdB2cMockValue = {
|
||||
getMetaData: B2cMetadata | Error;
|
||||
getSignKeySets: JwkSignKey[] | Error;
|
||||
changePassword: { sub: string } | Error;
|
||||
};
|
||||
|
||||
export type SendGridMockValue = {
|
||||
createMailContentFromEmailConfirm: {
|
||||
subject: string;
|
||||
text: string;
|
||||
html: string;
|
||||
};
|
||||
sendMail: undefined | Error;
|
||||
};
|
||||
|
||||
export const makeDefaultAdB2cMockValue = (): AdB2cMockValue => {
|
||||
return {
|
||||
getMetaData: {
|
||||
issuer: 'issuer',
|
||||
},
|
||||
getSignKeySets: [
|
||||
{
|
||||
kid: 'kid',
|
||||
nbf: 1111111111,
|
||||
use: 'sig',
|
||||
kty: 'RSA',
|
||||
e: 'e',
|
||||
n: 'n',
|
||||
},
|
||||
],
|
||||
changePassword: {
|
||||
sub: 'TEST9999',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const makeSendGridServiceMock = (value: SendGridMockValue) => {
|
||||
const { createMailContentFromEmailConfirm, sendMail } = value;
|
||||
return {
|
||||
createMailContentFromEmailConfirm:
|
||||
createMailContentFromEmailConfirm instanceof Error
|
||||
? jest
|
||||
.fn<Promise<void>, []>()
|
||||
.mockRejectedValue(createMailContentFromEmailConfirm)
|
||||
: jest
|
||||
.fn<Promise<{ subject: string; text: string; html: string }>, []>()
|
||||
.mockResolvedValue(createMailContentFromEmailConfirm),
|
||||
sendMail:
|
||||
sendMail instanceof Error
|
||||
? jest.fn<Promise<void>, []>().mockRejectedValue(sendMail)
|
||||
: jest.fn<Promise<void>, []>().mockResolvedValue(sendMail),
|
||||
};
|
||||
};
|
||||
|
||||
export const makeAdB2cServiceMock = (value: AdB2cMockValue) => {
|
||||
const { getMetaData, getSignKeySets, changePassword } = value;
|
||||
|
||||
return {
|
||||
getMetaData:
|
||||
getMetaData instanceof Error
|
||||
? jest.fn<Promise<void>, []>().mockRejectedValue(getMetaData)
|
||||
: jest.fn<Promise<B2cMetadata>, []>().mockResolvedValue(getMetaData),
|
||||
getSignKeySets:
|
||||
getSignKeySets instanceof Error
|
||||
? jest.fn<Promise<void>, []>().mockRejectedValue(getSignKeySets)
|
||||
: jest
|
||||
.fn<Promise<JwkSignKey[]>, []>()
|
||||
.mockResolvedValue(getSignKeySets),
|
||||
changePassword:
|
||||
changePassword instanceof Error
|
||||
? jest.fn<Promise<void>, []>().mockRejectedValue(changePassword)
|
||||
: jest
|
||||
.fn<Promise<{ sub: string }>, []>()
|
||||
.mockResolvedValue(changePassword),
|
||||
};
|
||||
};
|
||||
|
||||
export const makeUsersServiceMock = async (
|
||||
cryptoMockValue: CryptoMockValue,
|
||||
usersRepositoryMockValue: UsersRepositoryMockValue,
|
||||
adB2cMockValue: AdB2cMockValue,
|
||||
sendGridMockValue: SendGridMockValue,
|
||||
): Promise<UsersService> => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [UsersService],
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
ignoreEnvFile: true,
|
||||
ignoreEnvVars: true,
|
||||
}),
|
||||
],
|
||||
})
|
||||
.useMocker((token) => {
|
||||
switch (token) {
|
||||
@ -24,6 +116,12 @@ export const makeUsersServiceMock = async (
|
||||
return makeCryptoServiceMock(cryptoMockValue);
|
||||
case UsersRepositoryService:
|
||||
return makeUsersRepositoryMock(usersRepositoryMockValue);
|
||||
case AdB2cService:
|
||||
return makeAdB2cServiceMock(adB2cMockValue);
|
||||
case ConfigService:
|
||||
return {};
|
||||
case SendGridService:
|
||||
return makeSendGridServiceMock(sendGridMockValue);
|
||||
}
|
||||
})
|
||||
.compile();
|
||||
@ -32,24 +130,33 @@ export const makeUsersServiceMock = async (
|
||||
};
|
||||
|
||||
export const makeCryptoServiceMock = (value: CryptoMockValue) => {
|
||||
const { getPublicKey } = value;
|
||||
const { getPublicKey, getPrivateKey } = 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),
|
||||
};
|
||||
};
|
||||
|
||||
export const makeUsersRepositoryMock = (value: UsersRepositoryMockValue) => {
|
||||
const { updateUserVerified } = value;
|
||||
const { findUserById } = value;
|
||||
|
||||
return {
|
||||
updateUserVerified:
|
||||
updateUserVerified instanceof Error
|
||||
? jest.fn<Promise<void>, []>().mockRejectedValue(updateUserVerified)
|
||||
: jest.fn<Promise<void>, []>().mockResolvedValue(updateUserVerified),
|
||||
findUserById:
|
||||
findUserById instanceof Error
|
||||
? jest.fn<Promise<User>, []>().mockRejectedValue(findUserById)
|
||||
: jest.fn<Promise<User>, []>().mockResolvedValue(findUserById),
|
||||
};
|
||||
};
|
||||
|
||||
@ -66,6 +173,36 @@ 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'),
|
||||
};
|
||||
};
|
||||
|
||||
@ -74,5 +211,13 @@ export const makeDefaultUsersRepositoryMockValue =
|
||||
(): UsersRepositoryMockValue => {
|
||||
return {
|
||||
updateUserVerified: undefined,
|
||||
findUserById: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
export const makeSendGridServiceMockValue = (): SendGridMockValue => {
|
||||
return {
|
||||
createMailContentFromEmailConfirm: { subject: '', text: '', html: '' },
|
||||
sendMail: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
@ -65,6 +65,7 @@ export class UsersController {
|
||||
@Body() body: ConfirmRequest,
|
||||
): Promise<ConfirmResponse> {
|
||||
console.log(body);
|
||||
await this.usersService.confirmUserAndInitPassword(body.token);
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
@ -3,9 +3,18 @@ import { CryptoModule } from '../../gateways/crypto/crypto.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: [CryptoModule, UsersRepositoryModule],
|
||||
imports: [
|
||||
CryptoModule,
|
||||
UsersRepositoryModule,
|
||||
AdB2cModule,
|
||||
SendGridModule,
|
||||
ConfigModule,
|
||||
],
|
||||
controllers: [UsersController],
|
||||
providers: [UsersService],
|
||||
})
|
||||
|
||||
@ -4,6 +4,8 @@ import {
|
||||
makeDefaultCryptoMockValue,
|
||||
makeDefaultUsersRepositoryMockValue,
|
||||
makeUsersServiceMock,
|
||||
makeDefaultAdB2cMockValue,
|
||||
makeSendGridServiceMockValue,
|
||||
} from './test/users.service.mock';
|
||||
import { EmailAlreadyVerifiedError } from '../../repositories/users/users.repository.service';
|
||||
|
||||
@ -11,21 +13,58 @@ describe('UsersService', () => {
|
||||
it('ユーザの仮登録時に払い出されるトークンにより、未認証のユーザが認証済みになる', async () => {
|
||||
const cryptoMockValue = makeDefaultCryptoMockValue();
|
||||
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
const sendGridMockValue = makeSendGridServiceMockValue();
|
||||
const service = await makeUsersServiceMock(
|
||||
cryptoMockValue,
|
||||
usersRepositoryMockValue,
|
||||
adb2cParam,
|
||||
sendGridMockValue,
|
||||
);
|
||||
const token =
|
||||
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw';
|
||||
expect(await service.confirmUser(token)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('トークンの形式が不正な場合、形式不正エラーとなる。', async () => {
|
||||
it('ユーザーが発行されたパスワードでログインできるようにする', async () => {
|
||||
const cryptoMockValue = makeDefaultCryptoMockValue();
|
||||
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
|
||||
usersRepositoryMockValue.findUserById = {
|
||||
id: 1,
|
||||
external_id: 'TEST9999',
|
||||
account_id: 1,
|
||||
role: 'None',
|
||||
accepted_terms_version: 'string',
|
||||
email_verified: false,
|
||||
created_by: 'string;',
|
||||
created_at: new Date(),
|
||||
updated_by: 'string;',
|
||||
updated_at: new Date(),
|
||||
};
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
const sendGridMockValue = makeSendGridServiceMockValue();
|
||||
const service = await makeUsersServiceMock(
|
||||
cryptoMockValue,
|
||||
usersRepositoryMockValue,
|
||||
adb2cParam,
|
||||
sendGridMockValue,
|
||||
);
|
||||
|
||||
const token =
|
||||
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw';
|
||||
expect(await service.confirmUserAndInitPassword(token)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('トークンの形式が不正な場合、形式不正エラーとなる。', async () => {
|
||||
const cryptoMockValue = makeDefaultCryptoMockValue();
|
||||
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
const sendGridMockValue = makeSendGridServiceMockValue();
|
||||
const service = await makeUsersServiceMock(
|
||||
cryptoMockValue,
|
||||
usersRepositoryMockValue,
|
||||
adb2cParam,
|
||||
sendGridMockValue,
|
||||
);
|
||||
const token = 'invalid.id.token';
|
||||
await expect(service.confirmUser(token)).rejects.toEqual(
|
||||
@ -33,16 +72,47 @@ describe('UsersService', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('トークンの形式が不正な場合、形式不正エラーとなる。(メール認証API)', async () => {
|
||||
const cryptoMockValue = makeDefaultCryptoMockValue();
|
||||
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
|
||||
usersRepositoryMockValue.findUserById = {
|
||||
id: 1,
|
||||
external_id: 'TEST9999',
|
||||
account_id: 1,
|
||||
role: 'None',
|
||||
accepted_terms_version: 'string',
|
||||
email_verified: false,
|
||||
created_by: 'string;',
|
||||
created_at: new Date(),
|
||||
updated_by: 'string;',
|
||||
updated_at: new Date(),
|
||||
};
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
const sendGridMockValue = makeSendGridServiceMockValue();
|
||||
const service = await makeUsersServiceMock(
|
||||
cryptoMockValue,
|
||||
usersRepositoryMockValue,
|
||||
adb2cParam,
|
||||
sendGridMockValue,
|
||||
);
|
||||
const token = 'invalid.id.token';
|
||||
await expect(service.confirmUserAndInitPassword(token)).rejects.toEqual(
|
||||
new HttpException(makeErrorResponse('E000101'), HttpStatus.BAD_REQUEST),
|
||||
);
|
||||
});
|
||||
it('ユーザが既に認証済みだった場合、認証済みユーザエラーとなる。', async () => {
|
||||
const cryptoMockValue = makeDefaultCryptoMockValue();
|
||||
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
|
||||
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
const sendGridMockValue = makeSendGridServiceMockValue();
|
||||
usersRepositoryMockValue.updateUserVerified =
|
||||
new EmailAlreadyVerifiedError();
|
||||
|
||||
const service = await makeUsersServiceMock(
|
||||
cryptoMockValue,
|
||||
usersRepositoryMockValue,
|
||||
adb2cParam,
|
||||
sendGridMockValue,
|
||||
);
|
||||
const token =
|
||||
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw';
|
||||
@ -50,15 +120,50 @@ describe('UsersService', () => {
|
||||
new HttpException(makeErrorResponse('E010202'), HttpStatus.BAD_REQUEST),
|
||||
);
|
||||
});
|
||||
it('ユーザが既に認証済みだった場合、認証済みユーザエラーとなる。(メール認証API)', async () => {
|
||||
const cryptoMockValue = makeDefaultCryptoMockValue();
|
||||
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
|
||||
usersRepositoryMockValue.findUserById = {
|
||||
id: 1,
|
||||
external_id: 'TEST9999',
|
||||
account_id: 1,
|
||||
role: 'None',
|
||||
accepted_terms_version: 'string',
|
||||
email_verified: true,
|
||||
created_by: 'string;',
|
||||
created_at: new Date(),
|
||||
updated_by: 'string;',
|
||||
updated_at: new Date(),
|
||||
};
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
const sendGridMockValue = makeSendGridServiceMockValue();
|
||||
usersRepositoryMockValue.updateUserVerified =
|
||||
new EmailAlreadyVerifiedError();
|
||||
|
||||
const service = await makeUsersServiceMock(
|
||||
cryptoMockValue,
|
||||
usersRepositoryMockValue,
|
||||
adb2cParam,
|
||||
sendGridMockValue,
|
||||
);
|
||||
const token =
|
||||
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw';
|
||||
await expect(service.confirmUserAndInitPassword(token)).rejects.toEqual(
|
||||
new HttpException(makeErrorResponse('E010202'), HttpStatus.BAD_REQUEST),
|
||||
);
|
||||
});
|
||||
it('DBネットワークエラーとなる場合、エラーとなる。', async () => {
|
||||
const cryptoMockValue = makeDefaultCryptoMockValue();
|
||||
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
const sendGridMockValue = makeSendGridServiceMockValue();
|
||||
usersRepositoryMockValue.updateUserVerified = new Error('DB error');
|
||||
|
||||
const service = await makeUsersServiceMock(
|
||||
cryptoMockValue,
|
||||
usersRepositoryMockValue,
|
||||
adb2cParam,
|
||||
sendGridMockValue,
|
||||
);
|
||||
const token =
|
||||
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw';
|
||||
@ -69,4 +174,38 @@ describe('UsersService', () => {
|
||||
),
|
||||
);
|
||||
});
|
||||
it('DBネットワークエラーとなる場合、エラーとなる。(メール認証API)', async () => {
|
||||
const cryptoMockValue = makeDefaultCryptoMockValue();
|
||||
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
|
||||
usersRepositoryMockValue.findUserById = {
|
||||
id: 1,
|
||||
external_id: 'TEST9999',
|
||||
account_id: 1,
|
||||
role: 'None',
|
||||
accepted_terms_version: 'string',
|
||||
email_verified: false,
|
||||
created_by: 'string;',
|
||||
created_at: new Date(),
|
||||
updated_by: 'string;',
|
||||
updated_at: new Date(),
|
||||
};
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
const sendGridMockValue = makeSendGridServiceMockValue();
|
||||
usersRepositoryMockValue.updateUserVerified = new Error('DB error');
|
||||
|
||||
const service = await makeUsersServiceMock(
|
||||
cryptoMockValue,
|
||||
usersRepositoryMockValue,
|
||||
adb2cParam,
|
||||
sendGridMockValue,
|
||||
);
|
||||
const token =
|
||||
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw';
|
||||
await expect(service.confirmUserAndInitPassword(token)).rejects.toEqual(
|
||||
new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -6,12 +6,19 @@ 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 { ConfigService } from '@nestjs/config';
|
||||
import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(
|
||||
private readonly cryptoService: CryptoService,
|
||||
private readonly usersRepository: UsersRepositoryService,
|
||||
private readonly adB2cService: AdB2cService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly sendgridService: SendGridService,
|
||||
) {}
|
||||
private readonly logger = new Logger(UsersService.name);
|
||||
|
||||
@ -64,4 +71,67 @@ export class UsersService {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* confirm User And Init Password
|
||||
* @param token ユーザ仮登録時に払いだされるトークン
|
||||
*/
|
||||
async confirmUserAndInitPassword(token: string): Promise<void> {
|
||||
this.logger.log(`[IN] ${this.confirmUserAndInitPassword.name}`);
|
||||
|
||||
const pubKey = await this.cryptoService.getPublicKey();
|
||||
const decodedToken = verify<{
|
||||
accountId: number;
|
||||
userId: number;
|
||||
email: string;
|
||||
}>(token, pubKey);
|
||||
if (isVerifyError(decodedToken)) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000101'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
// ランダムなパスワードを生成する
|
||||
const ramdomPassword = makePassword();
|
||||
const { userId, email } = decodedToken;
|
||||
|
||||
try {
|
||||
// ユーザー情報からAzure AD B2CのIDを特定する
|
||||
const user = await this.usersRepository.findUserById(userId);
|
||||
const extarnalId = user.external_id;
|
||||
// ユーザを認証済みにする
|
||||
await this.usersRepository.updateUserVerified(userId);
|
||||
// パスワードを変更する
|
||||
await this.adB2cService.changePassword(extarnalId, ramdomPassword);
|
||||
// メールの送信元を取得
|
||||
const from = this.configService.get<string>('MAIL_FROM') ?? '';
|
||||
|
||||
// XXX ODMS側が正式にメッセージを決めるまで仮のメール内容とする
|
||||
const subject = 'A temporary password has been issued.';
|
||||
const text = 'temporary password: ' + ramdomPassword;
|
||||
const domains = this.configService.get<string>('APP_DOMAIN');
|
||||
const path = '/';
|
||||
const html = `<p>OMDS TOP PAGE URL.<p><a href="${domains}${path}">${domains}${path}}"</a>`;
|
||||
|
||||
// メールを送信
|
||||
await this.sendgridService.sendMail(email, from, subject, text, html);
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
if (e instanceof Error) {
|
||||
switch (e.constructor) {
|
||||
case EmailAlreadyVerifiedError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010202'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
default:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,4 +137,32 @@ export class AdB2cService {
|
||||
this.logger.log(`[OUT] ${this.getSignKeySets.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* change Password 特定のADB2Cのユーザのパスワードを変更する
|
||||
* @param externalId ユーザ情報
|
||||
* @param password パスワード
|
||||
*/
|
||||
async changePassword(
|
||||
externalId: string,
|
||||
password: string,
|
||||
): Promise<{ sub: string }> {
|
||||
this.logger.log(`[IN] ${this.changePassword.name}`);
|
||||
try {
|
||||
// ADB2Cのユーザのパスワードを変更する
|
||||
const changeUser = await this.graphClient
|
||||
.api(`/users/${externalId}`)
|
||||
.patch({
|
||||
passwordProfile: {
|
||||
password: password,
|
||||
},
|
||||
});
|
||||
return { sub: changeUser.id };
|
||||
} catch (e) {
|
||||
this.logger.error(e);
|
||||
throw e;
|
||||
} finally {
|
||||
this.logger.log(`[OUT] ${this.changePassword.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user