x.sunamoto.k 3f7a9ed11a Merged PR 75: API実装(ユーザー一覧取得)
## 概要
[Task1592: API実装(ユーザー一覧取得)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/1592)

- ユーザ一覧取得のAPIを実装
- アクセストークンにより権限を確認する
 - src/common/jwt/jwt.ts verifyAuthority([Task1593: API実装(ユーザー登録)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/1593)で作成)を呼び出すため追って再修正します。(レビュー対象外です)
 - src/features/users/users.controller.ts getUsersから
  src/features/users/users.service.ts getUsersへ
- DBから同一アカウントのユーザ一覧を取得する
 - findSameAccountUsersを新規作成

- Azure AD B2Cからユーザーを取得してマージ
 - src/gateways/adb2c/adb2c.service.ts getUserを新規作成
 - マージはfor文でまわしています(力技)
- マージした結果を返却

- 影響範囲
 - usersテーブルの変更が入るときにマージ部分の手直しが要ります。(TODOを添えています)

## レビューポイント
- 新規に作成したfindSameAccountUsersの妥当性
- 新規に作成したgetUserの妥当性
→Azureからの返り値はsrc/common/token/types.tsに定義済。
 (Azure AD B2Cから取得できた項目で再定義)

## UIの変更
- 特になし

## 動作確認状況
- ローカルでビルド、テストを実行した後に動作を確認済。

## 補足
- ご不便をおかけしました。よろしくお願いします。
2023-05-12 01:27:19 +00:00

321 lines
10 KiB
TypeScript

import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import { isVerifyError, verify } from '../../common/jwt';
import { makePassword } from '../../common/password/password';
import { AccessToken } from '../../common/token';
import {
AdB2cService,
ConflictError,
isConflictError,
} from '../../gateways/adb2c/adb2c.service';
import { CryptoService } from '../../gateways/crypto/crypto.service';
import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
import { User as EntityUser } from '../../repositories/users/entity/user.entity';
import {
EmailAlreadyVerifiedError,
UsersRepositoryService,
} from '../../repositories/users/users.repository.service';
import { User } from './types/types';
@Injectable()
export class UsersService {
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);
/**
* Confirms user
* @param token ユーザ仮登録時に払いだされるトークン
*/
async confirmUser(token: string): Promise<void> {
this.logger.log(`[IN] ${this.confirmUser.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,
);
}
try {
// トランザクションで取得と更新をまとめる
const userId = decodedToken.userId;
await this.usersRepository.updateUserVerified(userId);
} catch (e) {
console.log(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,
);
}
}
this.logger.error(`error=${e}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
/**
* ユーザを作成する
* @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: EntityUser;
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: EntityUser;
// 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 ユーザ仮登録時に払いだされるトークン
*/
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,
);
}
}
}
}
/**
* Get Users
* @param accessToken
* @returns users
*/
async getUsers(accessToken: string): Promise<User[]> {
this.logger.log(`[IN] ${this.getUsers.name}`);
try {
// DBよりアクセス者の所属するアカウントを取得する
const pubKey = await this.cryptoService.getPublicKey();
const payload = verify<AccessToken>(accessToken, pubKey);
if (isVerifyError(payload)) {
throw new Error(`${payload.reason} | ${payload.message}`);
}
// DBから同一アカウントのユーザ一覧を取得する
const dbUsers = await this.usersRepository.findSameAccountUsers(
Number(payload.userId),
);
// 値をマージして定義されたレスポンス通りに返す
const users: User[] = [];
// TODO 膨大なループが発生することが見込まれ商用には耐えないので、本実装時に修正予定
for (let i = 0; i < dbUsers.length; i++) {
// Azure AD B2Cからユーザーを取得する
const aadb2cUser = await this.adB2cService.getUser(
dbUsers[i].external_id,
);
const user = new User();
user.name = aadb2cUser.displayName;
user.role = dbUsers[i].role;
user.authorId = dbUsers[i].author_id;
// TODO DBから取得できるようになるため暫定
user.typistGroupName = '';
user.email = aadb2cUser.mail;
user.emailVerified = dbUsers[i].email_verified;
user.autoRenew = dbUsers[i].auto_renew;
user.licenseAlert = dbUsers[i].license_alert;
user.notification = dbUsers[i].notification;
users.push(user);
}
return users;
} catch (e) {
this.logger.error(`error=${e}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.NOT_FOUND,
);
} finally {
this.logger.log(`[OUT] ${this.getUsers.name}`);
}
}
}