Merged PR 429: API実装(アカウント削除API:メイン処理)
## 概要 [Task2670: API実装(アカウント削除API:メイン処理)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2670) アカウント削除APIを実装しました。 APIとしてはこれで実装完了ですが、DBに外部キー制約をつけていないので、現時点で削除できるものは以下のみです。 ・アカウントテーブル ・ADB2Cのユーザー ・BLOBストレージ ## レビューポイント 内容が重めの処理なので全体的に見ていただけると嬉しいです。 ## UIの変更 なし ## 動作確認状況 ローカルで以下の動作を確認 ・RDBのアカウントが削除される ・ADB2Cのユーザーが削除される ・RDBのユーザーが退避テーブルに登録される ・BLOBストレージが削除される ## 補足 UTは別タスクに切り出しているので、本タスクでは実装していません。
This commit is contained in:
parent
d942dc73f1
commit
664e815ef9
@ -246,3 +246,9 @@ export const OPTION_ITEM_VALUE_TYPE = {
|
||||
export const ADB2C_SIGN_IN_TYPE = {
|
||||
EAMILADDRESS: 'emailAddress',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* MANUAL_RECOVERY_REQUIRED
|
||||
* @const {string}
|
||||
*/
|
||||
export const MANUAL_RECOVERY_REQUIRED = '[MANUAL_RECOVERY_REQUIRED]';
|
||||
|
||||
@ -1071,7 +1071,7 @@ export class AccountsController {
|
||||
description: 'DBアクセスに失敗しログインできる状態で処理が終了した場合',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiOperation({ operationId: 'deleteAccount' })
|
||||
@ApiOperation({ operationId: 'deleteAccountAndData' })
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(
|
||||
@ -1079,7 +1079,7 @@ export class AccountsController {
|
||||
roles: [ADMIN_ROLES.ADMIN],
|
||||
}),
|
||||
)
|
||||
async deleteAccount(
|
||||
async deleteAccountAndData(
|
||||
@Req() req: Request,
|
||||
@Body() body: DeleteAccountRequest,
|
||||
): Promise<DeleteAccountResponse> {
|
||||
@ -1088,12 +1088,7 @@ export class AccountsController {
|
||||
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
|
||||
const context = makeContext(userId);
|
||||
|
||||
/* TODO 仮実装、別タスクで実装する
|
||||
await this.accountService.deleteAccount(
|
||||
context,
|
||||
accountId
|
||||
);
|
||||
*/
|
||||
await this.accountService.deleteAccountAndData(context, userId, accountId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ import {
|
||||
USER_ROLES,
|
||||
ADB2C_SIGN_IN_TYPE,
|
||||
OPTION_ITEM_VALUE_TYPE,
|
||||
MANUAL_RECOVERY_REQUIRED,
|
||||
} from '../../constants';
|
||||
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
|
||||
import {
|
||||
@ -319,7 +320,7 @@ export class AccountsService {
|
||||
} catch (error) {
|
||||
this.logger.error(`error=${error}`);
|
||||
this.logger.error(
|
||||
`[MANUAL_RECOVERY_REQUIRED] [${context.trackingId}] Failed to delete externalUser: ${externalUserId}`,
|
||||
`${MANUAL_RECOVERY_REQUIRED} [${context.trackingId}] Failed to delete externalUser: ${externalUserId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -338,7 +339,7 @@ export class AccountsService {
|
||||
} catch (error) {
|
||||
this.logger.error(`error=${error}`);
|
||||
this.logger.error(
|
||||
`[MANUAL_RECOVERY_REQUIRED] [${context.trackingId}] Failed to delete account: ${accountId}, user: ${userId}`,
|
||||
`${MANUAL_RECOVERY_REQUIRED} [${context.trackingId}] Failed to delete account: ${accountId}, user: ${userId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -361,7 +362,7 @@ export class AccountsService {
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`[MANUAL_RECOVERY_REQUIRED] [${context.trackingId}] Failed to delete container: ${accountId}, country: ${country}`,
|
||||
`${MANUAL_RECOVERY_REQUIRED} [${context.trackingId}] Failed to delete container: ${accountId}, country: ${country}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1684,4 +1685,99 @@ export class AccountsService {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* アカウントと紐づくデータを削除する
|
||||
* @param context
|
||||
* @param externalId
|
||||
* @param accountId // 削除対象のアカウントID
|
||||
*/
|
||||
async deleteAccountAndData(
|
||||
context: Context,
|
||||
externalId: string,
|
||||
accountId: number,
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.trackingId}] ${this.deleteAccountAndData.name} | params: { ` +
|
||||
`externalId: ${externalId}, ` +
|
||||
`accountId: ${accountId}, };`,
|
||||
);
|
||||
let country: string;
|
||||
let dbUsers: User[];
|
||||
try {
|
||||
// パラメータとトークンから取得したアカウントIDの突き合わせ
|
||||
const { account_id: myAccountId } =
|
||||
await this.usersRepository.findUserByExternalId(externalId);
|
||||
if (myAccountId !== accountId) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000108'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
|
||||
// アカウント削除前に必要な情報を退避する
|
||||
const targetAccount = await this.accountRepository.findAccountById(
|
||||
accountId,
|
||||
);
|
||||
// 削除対象アカウントを削除する
|
||||
dbUsers = await this.accountRepository.deleteAccountAndInsertArchives(
|
||||
accountId,
|
||||
);
|
||||
this.logger.log(`[${context.trackingId}] delete account: ${accountId}`);
|
||||
country = targetAccount.country;
|
||||
} catch (e) {
|
||||
// アカウントの削除に失敗した場合はエラーを返す
|
||||
this.logger.log(`[${context.trackingId}] ${e}`);
|
||||
this.logger.log(
|
||||
`[OUT] [${context.trackingId}] ${this.deleteAccountAndData.name}`,
|
||||
);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// 削除対象アカウント内のADB2Cユーザーをすべて削除する
|
||||
await this.adB2cService.deleteUsers(
|
||||
dbUsers.map((x) => x.external_id),
|
||||
context,
|
||||
);
|
||||
this.logger.log(
|
||||
`[${
|
||||
context.trackingId
|
||||
}] delete ADB2C users: ${accountId}, users_id: ${dbUsers.map(
|
||||
(x) => x.external_id,
|
||||
)}`,
|
||||
);
|
||||
} catch (e) {
|
||||
// ADB2Cユーザーの削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行
|
||||
this.logger.log(`[${context.trackingId}] ${e}`);
|
||||
this.logger.log(
|
||||
`${MANUAL_RECOVERY_REQUIRED} [${
|
||||
context.trackingId
|
||||
}] Failed to delete ADB2C users: ${accountId}, users_id: ${dbUsers.map(
|
||||
(x) => x.external_id,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// blobstorageコンテナを削除する
|
||||
await this.deleteBlobContainer(accountId, country, context);
|
||||
this.logger.log(
|
||||
`[${context.trackingId}] delete blob container: ${accountId}-${country}`,
|
||||
);
|
||||
} catch (e) {
|
||||
// blobstorageコンテナを削除で失敗した場合は、MANUAL_RECOVERY_REQUIRED出して正常終了
|
||||
this.logger.log(`[${context.trackingId}] ${e}`);
|
||||
this.logger.log(
|
||||
`${MANUAL_RECOVERY_REQUIRED}[${context.trackingId}] Failed to delete blob container: ${accountId}, country: ${country}`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`[OUT] [${context.trackingId}] ${this.deleteAccountAndData.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,6 +35,7 @@ import {
|
||||
import {
|
||||
ADB2C_SIGN_IN_TYPE,
|
||||
LICENSE_EXPIRATION_THRESHOLD_DAYS,
|
||||
MANUAL_RECOVERY_REQUIRED,
|
||||
USER_LICENSE_STATUS,
|
||||
USER_ROLES,
|
||||
} from '../../constants';
|
||||
@ -300,7 +301,7 @@ export class UsersService {
|
||||
} catch (error) {
|
||||
this.logger.error(`error=${error}`);
|
||||
this.logger.error(
|
||||
`[MANUAL_RECOVERY_REQUIRED] [${context.trackingId}] Failed to delete externalUser: ${externalUserId}`,
|
||||
`${MANUAL_RECOVERY_REQUIRED} [${context.trackingId}] Failed to delete externalUser: ${externalUserId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -313,7 +314,7 @@ export class UsersService {
|
||||
} catch (error) {
|
||||
this.logger.error(`error=${error}`);
|
||||
this.logger.error(
|
||||
`[MANUAL_RECOVERY_REQUIRED] [${context.trackingId}] Failed to delete user: ${userId}`,
|
||||
`${MANUAL_RECOVERY_REQUIRED} [${context.trackingId}] Failed to delete user: ${userId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -254,6 +254,31 @@ export class AdB2cService {
|
||||
this.logger.log(`[OUT] [${context.trackingId}] ${this.deleteUser.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Azure AD B2Cからユーザ情報を削除する(複数)
|
||||
* @param externalIds 外部ユーザーID
|
||||
* @param context コンテキスト
|
||||
*/
|
||||
async deleteUsers(externalIds: string[], context: Context): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.trackingId}] ${this.deleteUsers.name} | params: { externalIds: ${externalIds} };`,
|
||||
);
|
||||
|
||||
try {
|
||||
// 複数ユーザーを一括削除する方法が不明なため、rate limitの懸念があるのを承知のうえ単一削除の繰り返しで実装
|
||||
// TODO 一括削除する方法が判明したら修正する
|
||||
// https://learn.microsoft.com/en-us/graph/api/user-delete?view=graph-rest-1.0&tabs=javascript#example
|
||||
externalIds.map(
|
||||
async (x) => await this.graphClient.api(`users/${x}`).delete(),
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
throw e;
|
||||
} finally {
|
||||
this.logger.log(`[OUT] [${context.trackingId}] ${this.deleteUsers.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO [Task2002] 文字列の配列を15要素ずつ区切る(この処理も別タスクで削除予定)
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
UpdateResult,
|
||||
EntityManager,
|
||||
} from 'typeorm';
|
||||
import { User } from '../users/entity/user.entity';
|
||||
import { User, UserArchive } from '../users/entity/user.entity';
|
||||
import { Account } from './entity/account.entity';
|
||||
import { License, LicenseOrder } from '../licenses/entity/license.entity';
|
||||
import { SortCriteria } from '../sort_criteria/entity/sort_criteria.entity';
|
||||
@ -902,4 +902,34 @@ export class AccountsRepositoryService {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定されたアカウントを削除する
|
||||
* @param accountId
|
||||
* @returns users 削除対象のユーザー
|
||||
*/
|
||||
async deleteAccountAndInsertArchives(accountId: number): Promise<User[]> {
|
||||
return await this.dataSource.transaction(async (entityManager) => {
|
||||
// 削除対象のユーザーを退避テーブルに退避
|
||||
const users = await this.dataSource.getRepository(User).find({
|
||||
where: {
|
||||
account_id: accountId,
|
||||
},
|
||||
});
|
||||
|
||||
const userArchiveRepo = entityManager.getRepository(UserArchive);
|
||||
await userArchiveRepo
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(UserArchive)
|
||||
.values(users)
|
||||
.execute();
|
||||
|
||||
// アカウントを削除
|
||||
// アカウントを削除することで、外部キー制約がで紐づいている関連テーブルのデータも削除される
|
||||
const accountRepo = entityManager.getRepository(Account);
|
||||
await accountRepo.delete({ id: accountId });
|
||||
return users;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
JoinColumn,
|
||||
OneToOne,
|
||||
OneToMany,
|
||||
PrimaryColumn,
|
||||
} from 'typeorm';
|
||||
import { License } from '../../licenses/entity/license.entity';
|
||||
import { UserGroupMember } from '../../user_groups/entity/user_group_member.entity';
|
||||
@ -80,6 +81,63 @@ export class User {
|
||||
userGroupMembers?: UserGroupMember[];
|
||||
}
|
||||
|
||||
@Entity({ name: 'users_archive' })
|
||||
export class UserArchive {
|
||||
@PrimaryColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
external_id: string;
|
||||
|
||||
@Column()
|
||||
account_id: number;
|
||||
|
||||
@Column()
|
||||
role: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
author_id?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
accepted_terms_version?: string;
|
||||
|
||||
@Column()
|
||||
email_verified: boolean;
|
||||
|
||||
@Column()
|
||||
auto_renew: boolean;
|
||||
|
||||
@Column()
|
||||
license_alert: boolean;
|
||||
|
||||
@Column()
|
||||
notification: boolean;
|
||||
|
||||
@Column()
|
||||
encryption: boolean;
|
||||
|
||||
@Column()
|
||||
prompt: boolean;
|
||||
|
||||
@Column({ nullable: true })
|
||||
deleted_at?: Date;
|
||||
|
||||
@Column({ nullable: true })
|
||||
created_by: string;
|
||||
|
||||
@Column()
|
||||
created_at: Date;
|
||||
|
||||
@Column({ nullable: true })
|
||||
updated_by?: string;
|
||||
|
||||
@Column()
|
||||
updated_at: Date;
|
||||
|
||||
@CreateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
|
||||
archived_at: Date;
|
||||
}
|
||||
|
||||
export type newUser = Omit<
|
||||
User,
|
||||
| 'id'
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { User } from './entity/user.entity';
|
||||
import { User, UserArchive } from './entity/user.entity';
|
||||
import { UsersRepositoryService } from './users.repository.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([User])],
|
||||
imports: [TypeOrmModule.forFeature([User, UserArchive])],
|
||||
providers: [UsersRepositoryService],
|
||||
exports: [UsersRepositoryService],
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user