## 概要 [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の変更 - 特になし ## 動作確認状況 - ローカルでビルド、テストを実行した後に動作を確認済。 ## 補足 - ご不便をおかけしました。よろしくお願いします。
321 lines
10 KiB
TypeScript
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}`);
|
|
}
|
|
}
|
|
}
|