This commit is contained in:
oura.a 2023-10-16 11:00:09 +09:00
commit cdb6931d57
40 changed files with 953 additions and 178 deletions

View File

@ -22,11 +22,11 @@ const App = (): JSX.Element => {
useEffect(() => {
const id = globalAxios.interceptors.response.use(
(response: AxiosResponse) => response,
(e: AxiosError) => {
(e: AxiosError<{ code?: string }>) => {
if (
e?.response?.status === 401 &&
e.code &&
!UNAUTHORIZED_TO_CONTINUE_ERROR_CODES.includes(e.code)
e?.response?.data?.code &&
!UNAUTHORIZED_TO_CONTINUE_ERROR_CODES.includes(e.response.data.code)
) {
dispatch(clearToken());
instance.logoutRedirect({

View File

@ -56,4 +56,5 @@ export const errorCodes = [
"E011002", // ワークタイプ登録上限超過エラー
"E011003", // ワークタイプ不在エラー
"E013001", // ワークフローのAuthorIDとWorktypeIDのペア重複エラー
"E013002", // ワークフロー不在エラー
] as const;

View File

@ -346,6 +346,17 @@ export const deleteWorkflowAsync = createAsyncThunk<
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
// ワークフローが削除済みの場合は成功扱いとする
if (error.code === "E013002") {
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
}
thunkApi.dispatch(
openSnackbar({
level: "error",

View File

@ -34,10 +34,13 @@ import { TemplatesService } from '../../features/templates/templates.service';
import { TemplatesModule } from '../../features/templates/templates.module';
import { WorkflowsService } from '../../features/workflows/workflows.service';
import { WorkflowsModule } from '../../features/workflows/workflows.module';
import { TermsService } from '../../features/terms/terms.service';
import { TermsRepositoryModule } from '../../repositories/terms/terms.repository.module';
import { TermsModule } from '../../features/terms/terms.module';
export const makeTestingModule = async (
datasource: DataSource,
): Promise<TestingModule> => {
): Promise<TestingModule | undefined> => {
try {
const module: TestingModule = await Test.createTestingModule({
imports: [
@ -56,6 +59,7 @@ export const makeTestingModule = async (
LicensesModule,
TemplatesModule,
WorkflowsModule,
TermsModule,
AccountsRepositoryModule,
UsersRepositoryModule,
LicensesRepositoryModule,
@ -71,6 +75,7 @@ export const makeTestingModule = async (
AuthGuardsModule,
SortCriteriaRepositoryModule,
WorktypesRepositoryModule,
TermsRepositoryModule,
],
providers: [
AuthService,
@ -82,6 +87,7 @@ export const makeTestingModule = async (
LicensesService,
TemplatesService,
WorkflowsService,
TermsService,
],
})
.useMocker(async (token) => {

View File

@ -2,10 +2,12 @@ import { Test, TestingModule } from '@nestjs/testing';
import { AccountsController } from './accounts.controller';
import { AccountsService } from './accounts.service';
import { ConfigModule } from '@nestjs/config';
import { AuthService } from '../auth/auth.service';
describe('AccountsController', () => {
let controller: AccountsController;
const mockAccountService = {};
const mockAuthService = {};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
@ -16,10 +18,12 @@ describe('AccountsController', () => {
}),
],
controllers: [AccountsController],
providers: [AccountsService],
providers: [AccountsService, AuthService],
})
.overrideProvider(AccountsService)
.useValue(mockAccountService)
.overrideProvider(AuthService)
.useValue(mockAuthService)
.compile();
controller = module.get<AccountsController>(AccountsController);

View File

@ -8,6 +8,7 @@ import {
UseGuards,
Param,
Query,
HttpException,
} from '@nestjs/common';
import {
ApiOperation,
@ -74,12 +75,15 @@ import { AccessToken } from '../../common/token';
import jwt from 'jsonwebtoken';
import { makeContext } from '../../common/log';
import { v4 as uuidv4 } from 'uuid';
import { AuthService } from '../auth/auth.service';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
@ApiTags('accounts')
@Controller('accounts')
export class AccountsController {
constructor(
private readonly accountService: AccountsService, //private readonly cryptoService: CryptoService,
private readonly authService: AuthService,
) {}
@Post()
@ -1116,11 +1120,22 @@ export class AccountsController {
async getAccountInfoMinimalAccess(
@Body() body: GetAccountInfoMinimalAccessRequest,
): Promise<GetAccountInfoMinimalAccessResponse> {
const context = makeContext(uuidv4());
// IDトークンの検証
const idToken = await this.authService.getVerifiedIdToken(body.idToken);
const isVerified = await this.authService.isVerifiedUser(idToken);
if (!isVerified) {
throw new HttpException(
makeErrorResponse('E010201'),
HttpStatus.BAD_REQUEST,
);
}
// TODO 仮実装。API実装タスクで本実装する。
// const idToken = await this.authService.getVerifiedIdToken(body.idToken);
// await this.accountService.getAccountInfoMinimalAccess(context, idToken);
return;
const context = makeContext(idToken.sub);
const tier = await this.accountService.getAccountInfoMinimalAccess(
context,
idToken.sub,
);
return { tier };
}
}

View File

@ -9,6 +9,7 @@ import { AdB2cModule } from '../../gateways/adb2c/adb2c.module';
import { UserGroupsRepositoryModule } from '../../repositories/user_groups/user_groups.repository.module';
import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module';
import { WorktypesRepositoryModule } from '../../repositories/worktypes/worktypes.repository.module';
import { AuthService } from '../auth/auth.service';
@Module({
imports: [
@ -22,6 +23,6 @@ import { WorktypesRepositoryModule } from '../../repositories/worktypes/worktype
BlobstorageModule,
],
controllers: [AccountsController],
providers: [AccountsService],
providers: [AccountsService, AuthService],
})
export class AccountsModule {}

View File

@ -5595,3 +5595,127 @@ describe('deleteAccountAndData', () => {
expect(userRecord).toBe(null);
});
});
describe('getAccountInfoMinimalAccess', () => {
let source: DataSource = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
database: ':memory:',
logging: false,
entities: [__dirname + '/../../**/*.entity{.ts,.js}'],
synchronize: true, // trueにすると自動的にmigrationが行われるため注意
});
return source.initialize();
});
afterEach(async () => {
await source.destroy();
source = null;
});
it('IDトークンのsub情報からアカウントの階層情報を取得できること(第五階層)', async () => {
const module = await makeTestingModule(source);
const service = module.get<AccountsService>(AccountsService);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, {
tier: 5,
});
const context = makeContext(admin.external_id);
// 作成したデータを確認
{
const tier5Account = await getAccount(source, account.id);
expect(tier5Account.tier).toBe(5);
}
const tier = await service.getAccountInfoMinimalAccess(
context,
admin.external_id,
);
//実行結果を確認
expect(tier).toBe(5);
});
it('IDトークンのSub情報からアカウントの階層情報を取得できること第四階層', async () => {
const module = await makeTestingModule(source);
const service = module.get<AccountsService>(AccountsService);
// 第四階層のアカウント作成
const { account, admin } = await makeTestAccount(source, {
tier: 4,
});
const context = makeContext(admin.external_id);
// 作成したデータを確認
{
const tier5Account = await getAccount(source, account.id);
expect(tier5Account.tier).toBe(4);
}
const tier = await service.getAccountInfoMinimalAccess(
context,
admin.external_id,
);
//実行結果を確認
expect(tier).toBe(4);
});
it('対象のユーザーが存在しない場合、400エラーとなること', async () => {
const module = await makeTestingModule(source);
const service = module.get<AccountsService>(AccountsService);
// 第四階層のアカウント作成
const { account, admin } = await makeTestAccount(source, {
tier: 4,
});
const context = makeContext(admin.external_id);
// 作成したデータを確認
{
const tier5Account = await getAccount(source, account.id);
expect(tier5Account.tier).toBe(4);
}
try {
await service.getAccountInfoMinimalAccess(context, 'fail_external_id');
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST);
expect(e.getResponse()).toEqual(makeErrorResponse('E010204'));
} else {
fail();
}
}
});
it('DBアクセスに失敗した場合、500エラーとなること', async () => {
const module = await makeTestingModule(source);
const service = module.get<AccountsService>(AccountsService);
// 第四階層のアカウント作成
const { account, admin } = await makeTestAccount(source, {
tier: 4,
});
const context = makeContext(admin.external_id);
// 作成したデータを確認
{
const tier5Account = await getAccount(source, account.id);
expect(tier5Account.tier).toBe(4);
}
//DBアクセスに失敗するようにする
const usersRepositoryService = module.get<UsersRepositoryService>(
UsersRepositoryService,
);
usersRepositoryService.findUserByExternalId = jest
.fn()
.mockRejectedValue('DB failed');
try {
await service.getAccountInfoMinimalAccess(context, admin.external_id);
} catch (e) {
if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR);
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
} else {
fail();
}
}
});
});

View File

@ -1843,4 +1843,61 @@ export class AccountsService {
`[OUT] [${context.trackingId}] ${this.deleteAccountAndData.name}`,
);
}
/**
* IDトークンのsubからアカウントの階層情報を取得します
* @param context
* @param externalId
* @returns account info minimal access
*/
async getAccountInfoMinimalAccess(
context: Context,
externalId: string,
): Promise<number> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.getAccountInfoMinimalAccess.name} | params: { externalId: ${externalId} };`,
);
try {
const { account } = await this.usersRepository.findUserByExternalId(
externalId,
);
if (!account) {
throw new AccountNotFoundError(
`Account not found. externalId: ${externalId}`,
);
}
return account.tier;
} catch (e) {
this.logger.error(`[${context.trackingId}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case UserNotFoundError:
throw new HttpException(
makeErrorResponse('E010204'),
HttpStatus.BAD_REQUEST,
);
case AccountNotFoundError:
throw new HttpException(
makeErrorResponse('E010501'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.trackingId}] ${this.getAccountInfoMinimalAccess.name}`,
);
}
}
}

View File

@ -166,7 +166,7 @@ describe('checkIsAcceptedLatestVersion', () => {
await createTermInfo(source, 'EULA', '1.0');
await createTermInfo(source, 'DPA', '1.0');
const result = await service.checkIsAcceptedLatestVersion(context, idToken);
const result = await service.isAcceptedLatestVersion(context, idToken);
expect(result).toBe(true);
});
@ -187,7 +187,7 @@ describe('checkIsAcceptedLatestVersion', () => {
await createTermInfo(source, 'EULA', '1.0');
await createTermInfo(source, 'DPA', '1.0');
const result = await service.checkIsAcceptedLatestVersion(context, idToken);
const result = await service.isAcceptedLatestVersion(context, idToken);
expect(result).toBe(true);
});
@ -208,7 +208,7 @@ describe('checkIsAcceptedLatestVersion', () => {
await createTermInfo(source, 'EULA', '1.1');
await createTermInfo(source, 'DPA', '1.0');
const result = await service.checkIsAcceptedLatestVersion(context, idToken);
const result = await service.isAcceptedLatestVersion(context, idToken);
expect(result).toBe(false);
});
@ -229,7 +229,7 @@ describe('checkIsAcceptedLatestVersion', () => {
await createTermInfo(source, 'EULA', '1.1');
await createTermInfo(source, 'DPA', '1.0');
const result = await service.checkIsAcceptedLatestVersion(context, idToken);
const result = await service.isAcceptedLatestVersion(context, idToken);
expect(result).toBe(false);
});
@ -250,7 +250,7 @@ describe('checkIsAcceptedLatestVersion', () => {
await createTermInfo(source, 'EULA', '1.0');
await createTermInfo(source, 'DPA', '1.1');
const result = await service.checkIsAcceptedLatestVersion(context, idToken);
const result = await service.isAcceptedLatestVersion(context, idToken);
expect(result).toBe(false);
});
});

View File

@ -1,6 +1,7 @@
import {
Body,
Controller,
HttpException,
HttpStatus,
Post,
Req,
@ -21,6 +22,7 @@ import { retrieveAuthorizationToken } from '../../common/http/helper';
import { AccessToken } from '../../common/token';
import jwt from 'jsonwebtoken';
import { makeContext } from '../../common/log';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
@ApiTags('notification')
@Controller('notification')
@ -57,7 +59,20 @@ export class NotificationController {
const { handler, pns } = body;
const accessToken = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(accessToken, { json: true }) as AccessToken;
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(userId);

View File

@ -1,4 +1,11 @@
import { Controller, Get, HttpStatus, Req, UseGuards } from '@nestjs/common';
import {
Controller,
Get,
HttpException,
HttpStatus,
Req,
UseGuards,
} from '@nestjs/common';
import {
ApiBearerAuth,
ApiOperation,
@ -16,6 +23,7 @@ import { retrieveAuthorizationToken } from '../../common/http/helper';
import { Request } from 'express';
import { makeContext } from '../../common/log';
import { TemplatesService } from './templates.service';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
@ApiTags('templates')
@Controller('templates')
@ -46,8 +54,21 @@ export class TemplatesController {
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
@Get()
async getTemplates(@Req() req: Request): Promise<GetTemplatesResponse> {
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req);
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(userId);
const templates = await this.templatesService.getTemplates(context, userId);

View File

@ -9,7 +9,7 @@ import { HttpException, HttpStatus } from '@nestjs/common';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
describe('getTemplates', () => {
let source: DataSource = null;
let source: DataSource | undefined = undefined;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
@ -22,12 +22,16 @@ describe('getTemplates', () => {
});
afterEach(async () => {
if (!source) return;
await source.destroy();
source = null;
source = undefined;
});
it('テンプレートファイル一覧を取得できる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<TemplatesService>(TemplatesService);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
@ -62,10 +66,13 @@ describe('getTemplates', () => {
expect(templates[1].id).toBe(template2.id);
expect(templates[1].name).toBe(template2.file_name);
}
});
}, 6000000);
it('テンプレートファイル一覧を取得できる0件', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<TemplatesService>(TemplatesService);
// 第五階層のアカウント作成
const { admin } = await makeTestAccount(source, { tier: 5 });
@ -80,7 +87,10 @@ describe('getTemplates', () => {
});
it('テンプレートファイル一覧の取得に失敗した場合、エラーとなること', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<TemplatesService>(TemplatesService);
// 第五階層のアカウント作成
const { admin } = await makeTestAccount(source, { tier: 5 });

View File

@ -6,11 +6,14 @@ describe('TermsController', () => {
let controller: TermsController;
beforeEach(async () => {
const mockTermsService = {};
const module: TestingModule = await Test.createTestingModule({
controllers: [TermsController],
providers: [TermsService],
}).compile();
})
.overrideProvider(TermsService)
.useValue(mockTermsService)
.compile();
controller = module.get<TermsController>(TermsController);
});

View File

@ -28,12 +28,7 @@ export class TermsController {
async getTermsInfo(): Promise<GetTermsInfoResponse> {
const context = makeContext(uuidv4());
// TODO 仮実装。API実装タスクで本実装する。
// const termInfo = await this.termsService.getTermsInfo(context);
const termsInfo = [
{ documentType: 'EULA', version: '1.0' },
{ documentType: 'DPA', version: '1.1' },
] as TermInfo[];
const termsInfo = await this.termsService.getTermsInfo(context);
return { termsInfo };
}

View File

@ -1,9 +1,11 @@
import { Module } from '@nestjs/common';
import { TermsController } from './terms.controller';
import { TermsService } from './terms.service';
import { TermsRepositoryModule } from '../../repositories/terms/terms.repository.module';
@Module({
imports: [TermsRepositoryModule],
controllers: [TermsController],
providers: [TermsService]
providers: [TermsService],
})
export class TermsModule {}

View File

@ -1,18 +1,83 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TermsService } from './terms.service';
import { DataSource } from 'typeorm';
import { makeTestingModule } from '../../common/test/modules';
import { createTermInfo } from '../auth/test/utility';
import { makeContext } from '../../common/log';
import { v4 as uuidv4 } from 'uuid';
import { HttpException, HttpStatus } from '@nestjs/common';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
describe('TermsService', () => {
let service: TermsService;
describe('利用規約取得', () => {
let source: DataSource = null;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [TermsService],
}).compile();
service = module.get<TermsService>(TermsService);
source = new DataSource({
type: 'sqlite',
database: ':memory:',
logging: false,
entities: [__dirname + '/../../**/*.entity{.ts,.js}'],
synchronize: true, // trueにすると自動的にmigrationが行われるため注意
});
return source.initialize();
});
it('should be defined', () => {
expect(service).toBeDefined();
afterEach(async () => {
await source.destroy();
source = null;
});
it('最新の利用規約情報が取得できる', async () => {
const module = await makeTestingModule(source);
const service = module.get<TermsService>(TermsService);
await createTermInfo(source, 'EULA', 'v1.0');
await createTermInfo(source, 'EULA', 'v1.1');
await createTermInfo(source, 'DPA', 'v1.0');
await createTermInfo(source, 'DPA', 'v1.2');
const context = makeContext(uuidv4());
const result = await service.getTermsInfo(context);
expect(result[0].documentType).toBe('EULA');
expect(result[0].version).toBe('v1.1');
expect(result[1].documentType).toBe('DPA');
expect(result[1].version).toBe('v1.2');
});
it('利用規約情報(EULA、DPA両方)が存在しない場合エラーとなる', async () => {
const module = await makeTestingModule(source);
const service = module.get<TermsService>(TermsService);
const context = makeContext(uuidv4());
await expect(service.getTermsInfo(context)).rejects.toEqual(
new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
),
);
});
it('利用規約情報(EULAのみ)が存在しない場合エラーとなる', async () => {
const module = await makeTestingModule(source);
const service = module.get<TermsService>(TermsService);
await createTermInfo(source, 'DPA', 'v1.0');
const context = makeContext(uuidv4());
await expect(service.getTermsInfo(context)).rejects.toEqual(
new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
),
);
});
it('利用規約情報(DPAのみ)が存在しない場合エラーとなる', async () => {
const module = await makeTestingModule(source);
const service = module.get<TermsService>(TermsService);
await createTermInfo(source, 'EULA', 'v1.0');
const context = makeContext(uuidv4());
await expect(service.getTermsInfo(context)).rejects.toEqual(
new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
),
);
});
});

View File

@ -1,4 +1,44 @@
import { Injectable } from '@nestjs/common';
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { Context } from '../../common/log';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import { TermInfo } from './types/types';
import { TermsRepositoryService } from '../../repositories/terms/terms.repository.service';
import { TERM_TYPE } from '../../constants';
@Injectable()
export class TermsService {}
export class TermsService {
constructor(private readonly termsRepository: TermsRepositoryService) {}
private readonly logger = new Logger(TermsService.name);
/**
*
* return termsInfo
*/
async getTermsInfo(context: Context): Promise<TermInfo[]> {
this.logger.log(`[IN] [${context.trackingId}] ${this.getTermsInfo.name}`);
try {
const { eulaVersion, dpaVersion } =
await this.termsRepository.getLatestTermsInfo();
return [
{
documentType: TERM_TYPE.EULA,
version: eulaVersion,
},
{
documentType: TERM_TYPE.DPA,
version: dpaVersion,
},
];
} catch (e) {
this.logger.error(`error=${e}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.trackingId}] ${this.getTermsInfo.name}`,
);
}
}
}

View File

@ -10,3 +10,8 @@ export class GetTermsInfoResponse {
@ApiProperty({ type: [TermInfo] })
termsInfo: TermInfo[];
}
export type TermsVersion = {
eulaVersion: string;
dpaVersion: string;
};

View File

@ -2,10 +2,12 @@ import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { ConfigModule } from '@nestjs/config';
import { AuthService } from '../auth/auth.service';
describe('UsersController', () => {
let controller: UsersController;
const mockUserService = {};
const mockAuthService = {};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
@ -16,10 +18,12 @@ describe('UsersController', () => {
}),
],
controllers: [UsersController],
providers: [UsersService],
providers: [UsersService, AuthService],
})
.overrideProvider(UsersService)
.useValue(mockUserService)
.overrideProvider(AuthService)
.useValue(mockAuthService)
.compile();
controller = module.get<UsersController>(UsersController);

View File

@ -41,6 +41,7 @@ import {
UpdateAcceptedVersionResponse,
} from './types/types';
import { UsersService } from './users.service';
import { AuthService } from '../auth/auth.service';
import jwt from 'jsonwebtoken';
import { AuthGuard } from '../../common/guards/auth/authguards';
import {
@ -56,7 +57,10 @@ import { v4 as uuidv4 } from 'uuid';
@ApiTags('users')
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
constructor(
private readonly usersService: UsersService,
private readonly authService: AuthService,
) {}
@ApiResponse({
status: HttpStatus.OK,
@ -495,11 +499,24 @@ export class UsersController {
async updateAcceptedVersion(
@Body() body: UpdateAcceptedVersionRequest,
): Promise<UpdateAcceptedVersionResponse> {
const context = makeContext(uuidv4());
const { idToken, acceptedEULAVersion, acceptedDPAVersion } = body;
// TODO 仮実装。API実装タスクで本実装する。
// const idToken = await this.authService.getVerifiedIdToken(body.idToken);
// await this.usersService.updateAcceptedVersion(context, idToken);
const verifiedIdToken = await this.authService.getVerifiedIdToken(idToken);
const context = makeContext(verifiedIdToken.sub);
const isVerified = await this.authService.isVerifiedUser(verifiedIdToken);
if (!isVerified) {
throw new HttpException(
makeErrorResponse('E010201'),
HttpStatus.BAD_REQUEST,
);
}
await this.usersService.updateAcceptedVersion(
context,
verifiedIdToken.sub,
acceptedEULAVersion,
acceptedDPAVersion,
);
return {};
}
}

View File

@ -7,6 +7,7 @@ import { UsersRepositoryModule } from '../../repositories/users/users.repository
import { LicensesRepositoryModule } from '../../repositories/licenses/licenses.repository.module';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { AuthService } from '../auth/auth.service';
@Module({
imports: [
@ -18,6 +19,6 @@ import { UsersService } from './users.service';
ConfigModule,
],
controllers: [UsersController],
providers: [UsersService],
providers: [UsersService, AuthService],
})
export class UsersModule {}

View File

@ -43,6 +43,7 @@ import {
makeTestSimpleAccount,
makeTestUser,
} from '../../common/test/utility';
import { v4 as uuidv4 } from 'uuid';
describe('UsersService.confirmUser', () => {
let source: DataSource = null;
@ -2480,3 +2481,91 @@ describe('UsersService.updateUser', () => {
);
});
});
describe('UsersService.updateAcceptedVersion', () => {
let source: DataSource = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
database: ':memory:',
logging: false,
entities: [__dirname + '/../../**/*.entity{.ts,.js}'],
synchronize: true, // trueにすると自動的にmigrationが行われるため注意
});
return source.initialize();
});
afterEach(async () => {
await source.destroy();
source = null;
});
it('同意済み利用規約バージョンを更新できる(第五)', async () => {
const module = await makeTestingModule(source);
const { admin } = await makeTestAccount(source, {
tier: 5,
});
const context = makeContext(uuidv4());
const service = module.get<UsersService>(UsersService);
await service.updateAcceptedVersion(context, admin.external_id, 'v2.0');
const user = await getUser(source, admin.id);
expect(user.accepted_eula_version).toBe('v2.0');
});
it('同意済み利用規約バージョンを更新できる(第一~第四)', async () => {
const module = await makeTestingModule(source);
const { admin } = await makeTestAccount(source, {
tier: 4,
});
const context = makeContext(uuidv4());
const service = module.get<UsersService>(UsersService);
await service.updateAcceptedVersion(
context,
admin.external_id,
'v2.0',
'v3.0',
);
const user = await getUser(source, admin.id);
expect(user.accepted_eula_version).toBe('v2.0');
expect(user.accepted_dpa_version).toBe('v3.0');
});
it('パラメータが不在のときエラーとなる(第五)', async () => {
const module = await makeTestingModule(source);
const { admin } = await makeTestAccount(source, {
tier: 5,
});
const context = makeContext(uuidv4());
const service = module.get<UsersService>(UsersService);
await expect(
service.updateAcceptedVersion(context, admin.external_id, undefined),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010001'), HttpStatus.BAD_REQUEST),
);
});
it('パラメータが不在のときエラーとなる(第一~第四)', async () => {
const module = await makeTestingModule(source);
const { admin } = await makeTestAccount(source, {
tier: 4,
});
const context = makeContext(uuidv4());
const service = module.get<UsersService>(UsersService);
await expect(
service.updateAcceptedVersion(
context,
admin.external_id,
'v2.0',
undefined,
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010001'), HttpStatus.BAD_REQUEST),
);
});
});

View File

@ -4,7 +4,7 @@ import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import { isVerifyError, verify } from '../../common/jwt';
import { getPublicKey } from '../../common/jwt/jwt';
import { makePassword } from '../../common/password/password';
import { AccessToken } from '../../common/token';
import { AccessToken, IDToken } from '../../common/token';
import {
SortDirection,
TaskListSortableAttribute,
@ -30,6 +30,7 @@ import {
EmailAlreadyVerifiedError,
EncryptionPasswordNeedError,
InvalidRoleChangeError,
UpdateTermsVersionNotSetError,
UserNotFoundError,
} from '../../repositories/users/errors/types';
import {
@ -967,4 +968,58 @@ export class UsersService {
);
}
}
/**
*
* @param context
* @param idToken
* @param eulaVersion
* @param dpaVersion
*/
async updateAcceptedVersion(
context: Context,
externalId: string,
eulaVersion: string,
dpaVersion?: string,
): Promise<void> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.updateAcceptedVersion.name} | params: { ` +
`externalId: ${externalId}, ` +
`eulaVersion: ${eulaVersion}, ` +
`dpaVersion: ${dpaVersion}, };`,
);
try {
await this.usersRepository.updateAcceptedTermsVersion(
externalId,
eulaVersion,
dpaVersion,
);
} catch (e) {
this.logger.error(`[${context.trackingId}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case UserNotFoundError:
throw new HttpException(
makeErrorResponse('E010204'),
HttpStatus.BAD_REQUEST,
);
case UpdateTermsVersionNotSetError:
throw new HttpException(
makeErrorResponse('E010001'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
} finally {
this.logger.log(
`[OUT] [${context.trackingId}] ${this.updateAcceptedVersion.name}`,
);
}
}
}

View File

@ -2,6 +2,7 @@ import {
Body,
Controller,
Get,
HttpException,
HttpStatus,
Param,
Post,
@ -34,6 +35,7 @@ import { retrieveAuthorizationToken } from '../../common/http/helper';
import { Request } from 'express';
import { makeContext } from '../../common/log';
import { WorkflowsService } from './workflows.service';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
@ApiTags('workflows')
@Controller('workflows')
@ -64,8 +66,22 @@ export class WorkflowsController {
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
@Get()
async getWorkflows(@Req() req: Request): Promise<GetWorkflowsResponse> {
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
// TODO strictNullChecks対応
const accessToken = retrieveAuthorizationToken(req);
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(userId);
@ -107,17 +123,31 @@ export class WorkflowsController {
@Body() body: CreateWorkflowsRequest,
): Promise<CreateWorkflowsResponse> {
const { authorId, worktypeId, templateId, typists } = body;
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
// TODO strictNullChecks対応
const accessToken = retrieveAuthorizationToken(req);
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(userId);
await this.workflowsService.createWorkflow(
context,
userId,
authorId,
typists,
worktypeId,
templateId,
typists,
);
return {};
@ -158,8 +188,22 @@ export class WorkflowsController {
): Promise<UpdateWorkflowResponse> {
const { authorId, worktypeId, templateId, typists } = body;
const { workflowId } = param;
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
// TODO strictNullChecks対応
const accessToken = retrieveAuthorizationToken(req);
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(userId);
await this.workflowsService.updateWorkflow(
@ -167,9 +211,9 @@ export class WorkflowsController {
userId,
workflowId,
authorId,
typists,
worktypeId,
templateId,
typists,
);
return {};
@ -208,8 +252,22 @@ export class WorkflowsController {
@Param() param: DeleteWorkflowRequestParam,
): Promise<DeleteWorkflowResponse> {
const { workflowId } = param;
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
// TODO strictNullChecks対応
const accessToken = retrieveAuthorizationToken(req);
if (!accessToken) {
throw new HttpException(
makeErrorResponse('E000107'),
HttpStatus.UNAUTHORIZED,
);
}
const decodedAccessToken = jwt.decode(accessToken, { json: true });
if (!decodedAccessToken) {
throw new HttpException(
makeErrorResponse('E000101'),
HttpStatus.UNAUTHORIZED,
);
}
const { userId } = decodedAccessToken as AccessToken;
const context = makeContext(userId);
await this.workflowsService.deleteWorkflow(context, userId, workflowId);

View File

@ -21,7 +21,7 @@ import { HttpException, HttpStatus } from '@nestjs/common';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
describe('getWorkflows', () => {
let source: DataSource = null;
let source: DataSource | null = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
@ -34,12 +34,15 @@ describe('getWorkflows', () => {
});
afterEach(async () => {
if (!source) return;
await source.destroy();
source = null;
});
it('アカウント内のWorkflow一覧を取得できる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const { id: authorId1 } = await makeTestUser(source, {
@ -149,10 +152,10 @@ describe('getWorkflows', () => {
expect(resWorkflows[0].id).toBe(workflow1.id);
expect(resWorkflows[0].author.id).toBe(authorId1);
expect(resWorkflows[0].author.authorId).toBe('AUTHOR1');
expect(resWorkflows[0].worktype.id).toBe(worktypeId1);
expect(resWorkflows[0].worktype.worktypeId).toBe('worktype1');
expect(resWorkflows[0].template.id).toBe(templateId1);
expect(resWorkflows[0].template.fileName).toBe('fileName1');
expect(resWorkflows[0].worktype?.id).toBe(worktypeId1);
expect(resWorkflows[0].worktype?.worktypeId).toBe('worktype1');
expect(resWorkflows[0].template?.id).toBe(templateId1);
expect(resWorkflows[0].template?.fileName).toBe('fileName1');
expect(resWorkflows[0].typists.length).toBe(1);
expect(resWorkflows[0].typists[0].typistUserId).toBe(typistId);
expect(resWorkflows[0].typists[0].typistName).toBe('typist1');
@ -161,8 +164,8 @@ describe('getWorkflows', () => {
expect(resWorkflows[1].author.id).toBe(authorId2);
expect(resWorkflows[1].author.authorId).toBe('AUTHOR2');
expect(resWorkflows[1].worktype).toBe(undefined);
expect(resWorkflows[1].template.id).toBe(templateId1);
expect(resWorkflows[1].template.fileName).toBe('fileName1');
expect(resWorkflows[1].template?.id).toBe(templateId1);
expect(resWorkflows[1].template?.fileName).toBe('fileName1');
expect(resWorkflows[1].typists.length).toBe(1);
expect(resWorkflows[1].typists[0].typistGroupId).toBe(userGroupId);
expect(resWorkflows[1].typists[0].typistName).toBe('group1');
@ -170,8 +173,8 @@ describe('getWorkflows', () => {
expect(resWorkflows[2].id).toBe(workflow3.id);
expect(resWorkflows[2].author.id).toBe(authorId3);
expect(resWorkflows[2].author.authorId).toBe('AUTHOR3');
expect(resWorkflows[2].worktype.id).toBe(worktypeId1);
expect(resWorkflows[2].worktype.worktypeId).toBe('worktype1');
expect(resWorkflows[2].worktype?.id).toBe(worktypeId1);
expect(resWorkflows[2].worktype?.worktypeId).toBe('worktype1');
expect(resWorkflows[2].template).toBe(undefined);
expect(resWorkflows[2].typists.length).toBe(1);
expect(resWorkflows[2].typists[0].typistGroupId).toBe(userGroupId);
@ -180,7 +183,9 @@ describe('getWorkflows', () => {
});
it('アカウント内のWorkflow一覧を取得できる0件', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { admin } = await makeTestAccount(source, { tier: 5 });
@ -200,7 +205,9 @@ describe('getWorkflows', () => {
});
it('DBアクセスに失敗した場合、500エラーを返却する', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
@ -228,7 +235,7 @@ describe('getWorkflows', () => {
});
describe('createWorkflows', () => {
let source: DataSource = null;
let source: DataSource | null = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
@ -240,12 +247,15 @@ describe('createWorkflows', () => {
return source.initialize();
});
afterEach(async () => {
if (!source) return;
await source.destroy();
source = null;
});
it('アカウント内にWorkflowを作成できるWorktypeIDあり、テンプレートファイルあり', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const { id: authorId } = await makeTestUser(source, {
@ -288,13 +298,13 @@ describe('createWorkflows', () => {
context,
admin.external_id,
authorId,
worktypeId,
templateId,
[
{
typistId: typistId,
},
],
worktypeId,
templateId,
);
//実行結果を確認
@ -314,7 +324,9 @@ describe('createWorkflows', () => {
});
it('アカウント内にWorkflowを作成できるWorktypeIDなし、テンプレートファイルあり', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const { id: authorId } = await makeTestUser(source, {
@ -351,13 +363,13 @@ describe('createWorkflows', () => {
context,
admin.external_id,
authorId,
undefined,
templateId,
[
{
typistId: typistId,
},
],
undefined,
templateId,
);
//実行結果を確認
@ -377,7 +389,9 @@ describe('createWorkflows', () => {
});
it('アカウント内にWorkflowを作成できるWorktypeIDあり、テンプレートファイルなし', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const { id: authorId } = await makeTestUser(source, {
@ -413,13 +427,13 @@ describe('createWorkflows', () => {
context,
admin.external_id,
authorId,
worktypeId,
undefined,
[
{
typistId: typistId,
},
],
worktypeId,
undefined,
);
//実行結果を確認
@ -439,7 +453,9 @@ describe('createWorkflows', () => {
});
it('アカウント内にWorkflowを作成できるWorktypeIDなし、テンプレートファイルなし', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const { id: authorId } = await makeTestUser(source, {
@ -469,13 +485,13 @@ describe('createWorkflows', () => {
context,
admin.external_id,
authorId,
undefined,
undefined,
[
{
typistId: typistId,
},
],
undefined,
undefined,
);
//実行結果を確認
@ -495,7 +511,9 @@ describe('createWorkflows', () => {
});
it('アカウント内にWorkflowを作成できるWorktypeIDなし、テンプレートファイルなし、同一AuthorIDのワークフローあり', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const { id: authorId } = await makeTestUser(source, {
@ -532,26 +550,26 @@ describe('createWorkflows', () => {
context,
admin.external_id,
authorId,
worktypeId,
undefined,
[
{
typistId: typistId,
},
],
worktypeId,
undefined,
);
await service.createWorkflow(
context,
admin.external_id,
authorId,
undefined,
undefined,
[
{
typistId: typistId,
},
],
undefined,
undefined,
);
//実行結果を確認
@ -571,7 +589,9 @@ describe('createWorkflows', () => {
});
it('DBにAuthorが存在しない場合、400エラーとなること', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
@ -611,13 +631,13 @@ describe('createWorkflows', () => {
context,
admin.external_id,
0,
worktypeId,
templateId,
[
{
typistId: typistId,
},
],
worktypeId,
templateId,
);
} catch (e) {
if (e instanceof HttpException) {
@ -630,7 +650,9 @@ describe('createWorkflows', () => {
});
it('DBにWorktypeIDが存在しない場合、400エラーとなること', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const { id: authorId } = await makeTestUser(source, {
@ -669,13 +691,13 @@ describe('createWorkflows', () => {
context,
admin.external_id,
authorId,
9999,
templateId,
[
{
typistId: typistId,
},
],
9999,
templateId,
);
} catch (e) {
if (e instanceof HttpException) {
@ -688,7 +710,9 @@ describe('createWorkflows', () => {
});
it('DBにテンプレートファイルが存在しない場合、400エラーとなること', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const { id: authorId } = await makeTestUser(source, {
@ -726,13 +750,13 @@ describe('createWorkflows', () => {
context,
admin.external_id,
authorId,
worktypeId,
9999,
[
{
typistId: typistId,
},
],
worktypeId,
9999,
);
} catch (e) {
if (e instanceof HttpException) {
@ -745,7 +769,9 @@ describe('createWorkflows', () => {
});
it('DBにルーティング候補ユーザーが存在しない場合、400エラーとなること', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const { id: authorId } = await makeTestUser(source, {
@ -785,13 +811,13 @@ describe('createWorkflows', () => {
context,
admin.external_id,
authorId,
worktypeId,
templateId,
[
{
typistId: 9999,
},
],
worktypeId,
templateId,
);
} catch (e) {
if (e instanceof HttpException) {
@ -804,7 +830,9 @@ describe('createWorkflows', () => {
});
it('DBにルーティング候補グループが存在しない場合、400エラーとなること', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const { id: authorId } = await makeTestUser(source, {
@ -844,13 +872,13 @@ describe('createWorkflows', () => {
context,
admin.external_id,
authorId,
worktypeId,
templateId,
[
{
typistGroupId: 9999,
},
],
worktypeId,
templateId,
);
} catch (e) {
if (e instanceof HttpException) {
@ -863,7 +891,9 @@ describe('createWorkflows', () => {
});
it('DBにAuthorIDとWorktypeIDのペアがすでに存在する場合、400エラーとなること', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const { id: authorId } = await makeTestUser(source, {
@ -912,13 +942,13 @@ describe('createWorkflows', () => {
context,
admin.external_id,
authorId,
worktypeId,
templateId,
[
{
typistId: typistId,
},
],
worktypeId,
templateId,
);
} catch (e) {
if (e instanceof HttpException) {
@ -931,7 +961,9 @@ describe('createWorkflows', () => {
});
it('DBアクセスに失敗した場合、500エラーを返却する', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const { id: authorId } = await makeTestUser(source, {
@ -984,13 +1016,13 @@ describe('createWorkflows', () => {
context,
admin.external_id,
authorId,
worktypeId,
templateId,
[
{
typistId: typistId,
},
],
worktypeId,
templateId,
);
} catch (e) {
if (e instanceof HttpException) {
@ -1004,7 +1036,7 @@ describe('createWorkflows', () => {
});
describe('updateWorkflow', () => {
let source: DataSource = null;
let source: DataSource | null = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
@ -1016,12 +1048,15 @@ describe('updateWorkflow', () => {
return source.initialize();
});
afterEach(async () => {
if (!source) return;
await source.destroy();
source = null;
});
it('アカウント内のWorkflowを更新できるWorktypeIDあり、テンプレートファイルあり', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const { id: authorId1 } = await makeTestUser(source, {
@ -1090,13 +1125,13 @@ describe('updateWorkflow', () => {
admin.external_id,
preWorkflow.id,
authorId2,
worktypeId,
templateId,
[
{
typistId: typistId2,
},
],
worktypeId,
templateId,
);
//実行結果を確認
@ -1115,7 +1150,9 @@ describe('updateWorkflow', () => {
});
it('アカウント内にWorkflowを作成できるWorktypeIDなし、テンプレートファイルあり', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const { id: authorId1 } = await makeTestUser(source, {
@ -1178,13 +1215,13 @@ describe('updateWorkflow', () => {
admin.external_id,
preWorkflow.id,
authorId2,
undefined,
templateId,
[
{
typistId: typistId2,
},
],
undefined,
templateId,
);
//実行結果を確認
@ -1203,7 +1240,9 @@ describe('updateWorkflow', () => {
});
it('アカウント内にWorkflowを作成できるWorktypeIDあり、テンプレートファイルなし', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const { id: authorId1 } = await makeTestUser(source, {
@ -1265,13 +1304,13 @@ describe('updateWorkflow', () => {
admin.external_id,
preWorkflow.id,
authorId2,
worktypeId,
undefined,
[
{
typistId: typistId2,
},
],
worktypeId,
undefined,
);
//実行結果を確認
@ -1290,7 +1329,9 @@ describe('updateWorkflow', () => {
});
it('アカウント内にWorkflowを作成できるWorktypeIDなし、テンプレートファイルなし', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const { id: authorId1 } = await makeTestUser(source, {
@ -1346,13 +1387,13 @@ describe('updateWorkflow', () => {
admin.external_id,
preWorkflow.id,
authorId2,
undefined,
undefined,
[
{
typistId: typistId2,
},
],
undefined,
undefined,
);
//実行結果を確認
@ -1371,7 +1412,9 @@ describe('updateWorkflow', () => {
});
it('アカウント内にWorkflowを作成できるWorktypeIDなし、テンプレートファイルなし、同一AuthorIDのワークフローあり', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const { id: authorId1 } = await makeTestUser(source, {
@ -1447,13 +1490,13 @@ describe('updateWorkflow', () => {
admin.external_id,
preWorkflow1.id,
authorId2,
undefined,
undefined,
[
{
typistId: typistId2,
},
],
undefined,
undefined,
);
//実行結果を確認
@ -1472,7 +1515,9 @@ describe('updateWorkflow', () => {
});
it('DBにWorkflowが存在しない場合、400エラーとなること', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const { id: authorId1 } = await makeTestUser(source, {
@ -1497,13 +1542,13 @@ describe('updateWorkflow', () => {
admin.external_id,
9999,
authorId1,
undefined,
undefined,
[
{
typistId: typistId1,
},
],
undefined,
undefined,
);
} catch (e) {
if (e instanceof HttpException) {
@ -1515,7 +1560,9 @@ describe('updateWorkflow', () => {
}
});
it('DBにAuthorが存在しない場合、400エラーとなること', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const { id: authorId1 } = await makeTestUser(source, {
@ -1568,13 +1615,13 @@ describe('updateWorkflow', () => {
admin.external_id,
preWorkflow.id,
9999,
worktypeId,
templateId,
[
{
typistId: typistId1,
},
],
worktypeId,
templateId,
);
} catch (e) {
if (e instanceof HttpException) {
@ -1587,7 +1634,9 @@ describe('updateWorkflow', () => {
});
it('DBにWorktypeIDが存在しない場合、400エラーとなること', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const { id: authorId1 } = await makeTestUser(source, {
@ -1634,13 +1683,13 @@ describe('updateWorkflow', () => {
admin.external_id,
preWorkflow.id,
authorId1,
9999,
templateId,
[
{
typistId: typistId1,
},
],
9999,
templateId,
);
} catch (e) {
if (e instanceof HttpException) {
@ -1653,7 +1702,9 @@ describe('updateWorkflow', () => {
});
it('DBにテンプレートファイルが存在しない場合、400エラーとなること', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const { id: authorId1 } = await makeTestUser(source, {
@ -1699,13 +1750,13 @@ describe('updateWorkflow', () => {
admin.external_id,
preWorkflow.id,
authorId1,
worktypeId,
9999,
[
{
typistId: typistId1,
},
],
worktypeId,
9999,
);
} catch (e) {
if (e instanceof HttpException) {
@ -1718,7 +1769,9 @@ describe('updateWorkflow', () => {
});
it('DBにルーティング候補ユーザーが存在しない場合、400エラーとなること', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const { id: authorId1 } = await makeTestUser(source, {
@ -1771,13 +1824,13 @@ describe('updateWorkflow', () => {
admin.external_id,
preWorkflow.id,
authorId1,
worktypeId,
templateId,
[
{
typistId: 9999,
},
],
worktypeId,
templateId,
);
} catch (e) {
if (e instanceof HttpException) {
@ -1790,7 +1843,9 @@ describe('updateWorkflow', () => {
});
it('DBにルーティング候補グループが存在しない場合、400エラーとなること', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const { id: authorId1 } = await makeTestUser(source, {
@ -1843,13 +1898,13 @@ describe('updateWorkflow', () => {
admin.external_id,
preWorkflow.id,
authorId1,
worktypeId,
templateId,
[
{
typistGroupId: 9999,
},
],
worktypeId,
templateId,
);
} catch (e) {
if (e instanceof HttpException) {
@ -1862,7 +1917,9 @@ describe('updateWorkflow', () => {
});
it('DBにAuthorIDとWorktypeIDのペアがすでに存在する場合、400エラーとなること', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const { id: authorId1 } = await makeTestUser(source, {
@ -1909,13 +1966,13 @@ describe('updateWorkflow', () => {
admin.external_id,
preWorkflow.id,
authorId1,
worktypeId1,
undefined,
[
{
typistId: typistId1,
},
],
worktypeId1,
undefined,
);
} catch (e) {
if (e instanceof HttpException) {
@ -1928,7 +1985,9 @@ describe('updateWorkflow', () => {
});
it('DBアクセスに失敗した場合、500エラーを返却する', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
const { id: authorId1 } = await makeTestUser(source, {
@ -1983,13 +2042,13 @@ describe('updateWorkflow', () => {
admin.external_id,
preWorkflow.id,
authorId1,
undefined,
undefined,
[
{
typistId: typistId1,
},
],
undefined,
undefined,
);
} catch (e) {
if (e instanceof HttpException) {

View File

@ -14,6 +14,7 @@ import {
WorkflowNotFoundError,
} from '../../repositories/workflows/errors/types';
import { AccountNotFoundError } from '../../repositories/accounts/errors/types';
import { Assignee } from '../tasks/types/types';
@Injectable()
export class WorkflowsService {
@ -47,15 +48,20 @@ export class WorkflowsService {
// ワークフロー一覧からtypistのexternalIdを取得
const externalIds = workflowRecords.flatMap((workflow) => {
const workflowTypists = workflow.workflowTypists.flatMap(
const workflowTypists = workflow.workflowTypists?.flatMap(
(workflowTypist) => {
const { typist } = workflowTypist;
return typist ? [typist?.external_id] : [];
return typist ? [typist.external_id] : [];
},
);
return workflowTypists;
});
const distinctedExternalIds = [...new Set(externalIds)];
// externalIdsからundefinedを除外
const filteredExternalIds = externalIds.filter(
(externalId): externalId is string => externalId !== undefined,
);
// externalIdsから重複を除外
const distinctedExternalIds = [...new Set(filteredExternalIds)];
// ADB2Cからユーザー一覧を取得
const adb2cUsers = await this.adB2cService.getUsers(
@ -64,8 +70,11 @@ export class WorkflowsService {
);
// DBから取得したワークフロー一覧を整形
const workflows = workflowRecords.map((workflow) => {
const workflows = workflowRecords.map((workflow): Workflow => {
const { id, author, worktype, template, workflowTypists } = workflow;
if (!author || !author.id || !author.author_id) {
throw new Error('author is undefined');
}
const authorId = { id: author.id, authorId: author.author_id };
const worktypeId = worktype
@ -75,16 +84,24 @@ export class WorkflowsService {
? { id: template.id, fileName: template.file_name }
: undefined;
if (!workflowTypists) {
throw new Error('workflowTypists is undefined');
}
// ルーティング候補を整形
const typists = workflowTypists.map((workflowTypist) => {
const typists = workflowTypists.map((workflowTypist): Assignee => {
const { typist, typistGroup } = workflowTypist;
// typistがユーザーの場合はADB2Cからユーザー名を取得
const typistName = typist
? adb2cUsers.find(
(adb2cUser) => adb2cUser.id === typist.external_id,
).displayName
: typistGroup.name;
)?.displayName
: typistGroup?.name;
if (!typistName) {
throw new Error('typistName is undefined');
}
return {
typistUserId: typist?.id,
@ -130,9 +147,9 @@ export class WorkflowsService {
context: Context,
externalId: string,
authorId: number,
typists: WorkflowTypist[],
worktypeId?: number | undefined,
templateId?: number | undefined,
typists?: WorkflowTypist[],
): Promise<void> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.createWorkflow.name} | | params: { ` +
@ -149,9 +166,9 @@ export class WorkflowsService {
await this.workflowsRepository.createtWorkflows(
accountId,
authorId,
typists,
worktypeId,
templateId,
typists,
);
} catch (e) {
this.logger.error(`[${context.trackingId}] error=${e}`);
@ -215,9 +232,9 @@ export class WorkflowsService {
externalId: string,
workflowId: number,
authorId: number,
typists: WorkflowTypist[],
worktypeId?: number | undefined,
templateId?: number | undefined,
typists?: WorkflowTypist[],
): Promise<void> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.updateWorkflow.name} | params: { ` +
@ -236,9 +253,9 @@ export class WorkflowsService {
accountId,
workflowId,
authorId,
typists,
worktypeId,
templateId,
typists,
);
} catch (e) {
this.logger.error(`[${context.trackingId}] error=${e}`);

View File

@ -30,17 +30,18 @@ export const isConflictError = (arg: unknown): arg is ConflictError => {
@Injectable()
export class AdB2cService {
private readonly logger = new Logger(AdB2cService.name);
private readonly tenantName = this.configService.get<string>('TENANT_NAME');
private readonly tenantName =
this.configService.getOrThrow<string>('TENANT_NAME');
private readonly flowName =
this.configService.get<string>('SIGNIN_FLOW_NAME');
this.configService.getOrThrow<string>('SIGNIN_FLOW_NAME');
private graphClient: Client;
constructor(private readonly configService: ConfigService) {
// ADB2Cへの認証情報
const credential = new ClientSecretCredential(
this.configService.get('ADB2C_TENANT_ID'),
this.configService.get('ADB2C_CLIENT_ID'),
this.configService.get('ADB2C_CLIENT_SECRET'),
this.configService.getOrThrow<string>('ADB2C_TENANT_ID'),
this.configService.getOrThrow('ADB2C_CLIENT_ID'),
this.configService.getOrThrow('ADB2C_CLIENT_SECRET'),
);
const authProvider = new TokenCredentialAuthenticationProvider(credential, {
scopes: ['https://graph.microsoft.com/.default'],

View File

@ -28,31 +28,31 @@ export class BlobstorageService {
private readonly sasTokenExpireHour: number;
constructor(private readonly configService: ConfigService) {
this.sharedKeyCredentialUS = new StorageSharedKeyCredential(
this.configService.get('STORAGE_ACCOUNT_NAME_US'),
this.configService.get('STORAGE_ACCOUNT_KEY_US'),
this.configService.getOrThrow<string>('STORAGE_ACCOUNT_NAME_US'),
this.configService.getOrThrow<string>('STORAGE_ACCOUNT_KEY_US'),
);
this.sharedKeyCredentialAU = new StorageSharedKeyCredential(
this.configService.get('STORAGE_ACCOUNT_NAME_AU'),
this.configService.get('STORAGE_ACCOUNT_KEY_AU'),
this.configService.getOrThrow<string>('STORAGE_ACCOUNT_NAME_AU'),
this.configService.getOrThrow<string>('STORAGE_ACCOUNT_KEY_AU'),
);
this.sharedKeyCredentialEU = new StorageSharedKeyCredential(
this.configService.get('STORAGE_ACCOUNT_NAME_EU'),
this.configService.get('STORAGE_ACCOUNT_KEY_EU'),
this.configService.getOrThrow<string>('STORAGE_ACCOUNT_NAME_EU'),
this.configService.getOrThrow<string>('STORAGE_ACCOUNT_KEY_EU'),
);
this.blobServiceClientUS = new BlobServiceClient(
this.configService.get('STORAGE_ACCOUNT_ENDPOINT_US'),
this.configService.getOrThrow<string>('STORAGE_ACCOUNT_ENDPOINT_US'),
this.sharedKeyCredentialUS,
);
this.blobServiceClientAU = new BlobServiceClient(
this.configService.get('STORAGE_ACCOUNT_ENDPOINT_AU'),
this.configService.getOrThrow<string>('STORAGE_ACCOUNT_ENDPOINT_AU'),
this.sharedKeyCredentialAU,
);
this.blobServiceClientEU = new BlobServiceClient(
this.configService.get('STORAGE_ACCOUNT_ENDPOINT_EU'),
this.configService.getOrThrow<string>('STORAGE_ACCOUNT_ENDPOINT_EU'),
this.sharedKeyCredentialEU,
);
this.sasTokenExpireHour = Number(
this.configService.get('STORAGE_TOKEN_EXPIRE_TIME'),
this.sasTokenExpireHour = this.configService.getOrThrow<number>(
'STORAGE_TOKEN_EXPIRE_TIME',
);
}

View File

@ -18,8 +18,8 @@ export class NotificationhubService {
private readonly client: NotificationHubsClient;
constructor(private readonly configService: ConfigService) {
this.client = new NotificationHubsClient(
this.configService.get<string>('NOTIFICATION_HUB_CONNECT_STRING'),
this.configService.get<string>('NOTIFICATION_HUB_NAME'),
this.configService.getOrThrow<string>('NOTIFICATION_HUB_CONNECT_STRING'),
this.configService.getOrThrow<string>('NOTIFICATION_HUB_NAME'),
);
}

View File

@ -9,7 +9,7 @@ import { Context } from '../../common/log';
export class SendGridService {
private readonly logger = new Logger(SendGridService.name);
constructor(private readonly configService: ConfigService) {
const key = this.configService.get<string>('SENDGRID_API_KEY');
const key = this.configService.getOrThrow<string>('SENDGRID_API_KEY');
sendgrid.setApiKey(key);
}

View File

@ -5,14 +5,15 @@ import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import helmet from 'helmet';
const helmetDirectives = helmet.contentSecurityPolicy.getDefaultDirectives();
helmetDirectives['connect-src'] =
process.env.STAGE === 'local'
? [
"'self'",
process.env.ADB2C_ORIGIN,
process.env.STORAGE_ACCOUNT_ENDPOINT_US,
process.env.STORAGE_ACCOUNT_ENDPOINT_AU,
process.env.STORAGE_ACCOUNT_ENDPOINT_EU,
process.env.ADB2C_ORIGIN ?? '',
process.env.STORAGE_ACCOUNT_ENDPOINT_US ?? '',
process.env.STORAGE_ACCOUNT_ENDPOINT_AU ?? '',
process.env.STORAGE_ACCOUNT_ENDPOINT_EU ?? '',
]
: ["'self'"];

View File

@ -336,7 +336,7 @@ export class TasksRepositoryService {
await taskRepo.update(
{ audio_file_id: audio_file_id },
{
typist_user: null,
typist_user_id: null,
status: TASK_STATUS.UPLOADED,
},
);

View File

@ -19,13 +19,13 @@ export class TemplateFile {
@Column()
file_name: string;
@Column({ nullable: true })
created_by?: string;
created_by: string | null;
@CreateDateColumn()
created_at: Date;
@Column({ nullable: true })
updated_by?: string;
updated_by: string | null;
@UpdateDateColumn()
updated_at: Date;
@OneToMany(() => Task, (task) => task.template_file)
tasks?: Task[];
tasks: Task[] | null;
}

View File

@ -1,8 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Term } from './entity/term.entity';
import { TermsRepositoryService } from './terms.repository.service';
@Module({
imports: [TypeOrmModule.forFeature([Term])],
providers: [TermsRepositoryService],
exports: [TermsRepositoryService],
})
export class TermsRepositoryModule {}

View File

@ -0,0 +1,47 @@
import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { TermsVersion } from '../../features/terms/types/types';
import { Term } from './entity/term.entity';
import { TERM_TYPE } from '../../constants';
import { TermInfoNotFoundError } from '../users/errors/types';
@Injectable()
export class TermsRepositoryService {
constructor(private dataSource: DataSource) {}
/*
*
* @returns Term[]
*/
async getLatestTermsInfo(): Promise<TermsVersion> {
return await this.dataSource.transaction(async (entityManager) => {
const termRepo = entityManager.getRepository(Term);
const latestEulaInfo = await termRepo.findOne({
where: {
document_type: TERM_TYPE.EULA,
},
order: {
id: 'DESC',
},
});
const latestDpaInfo = await termRepo.findOne({
where: {
document_type: TERM_TYPE.DPA,
},
order: {
id: 'DESC',
},
});
if (!latestEulaInfo || !latestDpaInfo) {
throw new TermInfoNotFoundError(
`Terms info is not found. latestEulaInfo: ${latestEulaInfo}, latestDpaInfo: ${latestDpaInfo}`,
);
}
return {
eulaVersion: latestEulaInfo.version,
dpaVersion: latestDpaInfo.version,
};
});
}
}

View File

@ -10,3 +10,5 @@ export class InvalidRoleChangeError extends Error {}
export class EncryptionPasswordNeedError extends Error {}
// 利用規約バージョン情報不在エラー
export class TermInfoNotFoundError extends Error {}
// 利用規約バージョンパラメータ不在エラー
export class UpdateTermsVersionNotSetError extends Error {}

View File

@ -13,6 +13,7 @@ import {
InvalidRoleChangeError,
EncryptionPasswordNeedError,
TermInfoNotFoundError,
UpdateTermsVersionNotSetError,
} from './errors/types';
import {
LICENSE_ALLOCATED_STATUS,
@ -475,4 +476,49 @@ export class UsersRepositoryService {
};
});
}
/**
*
* @param externalId
* @param eulaVersion
* @param dpaVersion
* @returns update
*/
async updateAcceptedTermsVersion(
externalId: string,
eulaVersion: string,
dpaVersion: string | undefined,
): Promise<void> {
await this.dataSource.transaction(async (entityManager) => {
const userRepo = entityManager.getRepository(User);
const user = await userRepo.findOne({
where: {
external_id: externalId,
},
relations: {
account: true,
},
});
if (!user) {
throw new UserNotFoundError(
`User not found. externalId: ${externalId}`,
);
}
// パラメータが不在の場合はエラーを返却
if (!eulaVersion) {
throw new UpdateTermsVersionNotSetError(`EULA version param not set.`);
}
if (user.account.tier !== TIERS.TIER5 && !dpaVersion) {
throw new UpdateTermsVersionNotSetError(
`DPA version param not set. User's tier: ${user.account.tier}`,
);
}
user.accepted_eula_version = eulaVersion;
user.accepted_dpa_version = dpaVersion ?? user.accepted_dpa_version;
await userRepo.update({ id: user.id }, user);
});
}
}

View File

@ -25,35 +25,35 @@ export class Workflow {
author_id: number;
@Column({ nullable: true })
worktype_id?: number;
worktype_id: number | null;
@Column({ nullable: true })
template_id?: number;
template_id: number | null;
@Column({ nullable: true })
created_by: string;
created_by: string | null;
@CreateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
created_at: Date;
@Column({ nullable: true })
updated_by?: string;
updated_by: string | null;
@UpdateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
updated_at: Date;
@ManyToOne(() => User, (user) => user.id)
@JoinColumn({ name: 'author_id' })
author?: User;
author: User | null;
@ManyToOne(() => Worktype, (worktype) => worktype.id)
@JoinColumn({ name: 'worktype_id' })
worktype?: Worktype;
worktype: Worktype | null;
@ManyToOne(() => TemplateFile, (templateFile) => templateFile.id)
@JoinColumn({ name: 'template_id' })
template?: TemplateFile;
template: TemplateFile | null;
@OneToMany(() => WorkflowTypist, (workflowTypist) => workflowTypist.workflow)
workflowTypists?: WorkflowTypist[];
workflowTypists: WorkflowTypist[] | null;
}

View File

@ -61,9 +61,9 @@ export class WorkflowsRepositoryService {
async createtWorkflows(
accountId: number,
authorId: number,
typists: WorkflowTypist[],
worktypeId?: number | undefined,
templateId?: number | undefined,
typists?: WorkflowTypist[],
): Promise<void> {
return await this.dataSource.transaction(async (entityManager) => {
// authorの存在確認
@ -178,9 +178,9 @@ export class WorkflowsRepositoryService {
accountId: number,
workflowId: number,
authorId: number,
typists: WorkflowTypist[],
worktypeId?: number | undefined,
templateId?: number | undefined,
typists?: WorkflowTypist[],
): Promise<void> {
return await this.dataSource.transaction(async (entityManager) => {
const workflowRepo = entityManager.getRepository(Workflow);
@ -343,8 +343,8 @@ export class WorkflowsRepositoryService {
const workflow = new Workflow();
workflow.account_id = accountId;
workflow.author_id = authorId;
workflow.worktype_id = worktypeId;
workflow.template_id = templateId;
workflow.worktype_id = worktypeId ?? null;
workflow.template_id = templateId ?? null;
return workflow;
}
@ -358,8 +358,8 @@ export class WorkflowsRepositoryService {
*/
private makeWorkflowTypist(
workflowId: number,
typistId: number,
typistGroupId: number,
typistId?: number,
typistGroupId?: number,
): DbWorkflowTypist {
const workflowTypist = new DbWorkflowTypist();
workflowTypist.workflow_id = workflowId;