diff --git a/dictation_client/src/app/store.ts b/dictation_client/src/app/store.ts index 4cc9a10..fd28168 100644 --- a/dictation_client/src/app/store.ts +++ b/dictation_client/src/app/store.ts @@ -7,7 +7,6 @@ import ui from "features/ui/uiSlice"; import user from "features/user/userSlice"; import license from "features/license/licenseOrder/licenseSlice"; import licenseCardIssue from "features/license/licenseCardIssue/licenseCardIssueSlice"; -import licenseCardActivate from "features/license/licenseCardActivate/licenseCardActivateSlice"; import licenseSummary from "features/license/licenseSummary/licenseSummarySlice"; import dictation from "features/dictation/dictationSlice"; @@ -21,7 +20,6 @@ export const store = configureStore({ user, license, licenseCardIssue, - licenseCardActivate, licenseSummary, dictation, }, diff --git a/dictation_client/src/features/license/licenseCardActivate/index.ts b/dictation_client/src/features/license/licenseCardActivate/index.ts deleted file mode 100644 index 8698973..0000000 --- a/dictation_client/src/features/license/licenseCardActivate/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./state"; -export * from "./operations"; -export * from "./selectors"; -export * from "./licenseCardActivateSlice"; diff --git a/dictation_client/src/features/license/licenseCardActivate/licenseCardActivateSlice.ts b/dictation_client/src/features/license/licenseCardActivate/licenseCardActivateSlice.ts deleted file mode 100644 index e06838c..0000000 --- a/dictation_client/src/features/license/licenseCardActivate/licenseCardActivateSlice.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { PayloadAction, createSlice } from "@reduxjs/toolkit"; -import { LicenseCardActivateState } from "./state"; -import { activateCardLicenseAsync } from "./operations"; - -const initialState: LicenseCardActivateState = { - apps: { - keyLicense: "", - isLoading: false, - }, -}; -export const licenseCardActivateSlice = createSlice({ - name: "licenseCardActivate", - initialState, - reducers: { - changeKeyLicense: ( - state, - action: PayloadAction<{ keyLicense: string }> - ) => { - const { keyLicense } = action.payload; - state.apps.keyLicense = keyLicense.toUpperCase(); - }, - cleanupApps: (state) => { - state.apps = initialState.apps; - }, - }, -}); - -export const { changeKeyLicense, cleanupApps } = - licenseCardActivateSlice.actions; - -export default licenseCardActivateSlice.reducer; diff --git a/dictation_client/src/features/license/licenseCardActivate/operations.ts b/dictation_client/src/features/license/licenseCardActivate/operations.ts deleted file mode 100644 index 103da22..0000000 --- a/dictation_client/src/features/license/licenseCardActivate/operations.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { createAsyncThunk } from "@reduxjs/toolkit"; -import { openSnackbar } from "../../ui/uiSlice"; -import type { RootState } from "../../../app/store"; -import { getTranslationID } from "../../../translation"; -import { LicensesApi } from "../../../api/api"; -import { Configuration } from "../../../api/configuration"; -import { ErrorObject, createErrorObject } from "../../../common/errors"; - -export const activateCardLicenseAsync = createAsyncThunk< - { - /* Empty Object */ - }, - { - // パラメータ - cardLicenseKey: string; - }, - { - // rejectした時の返却値の型s - rejectValue: { - error: ErrorObject; - }; - } ->("licenses/activateCardLicenseAsync", async (args, thunkApi) => { - const { cardLicenseKey } = args; - - // apiのConfigurationを取得する - const { getState } = thunkApi; - const state = getState() as RootState; - const { configuration, accessToken } = state.auth; - const config = new Configuration(configuration); - const licensesApi = new LicensesApi(config); - - try { - await licensesApi.activateCardLicenses( - { - cardLicenseKey, - }, - { - headers: { authorization: `Bearer ${accessToken}` }, - } - ); - thunkApi.dispatch( - openSnackbar({ - level: "info", - message: getTranslationID("common.message.success"), - }) - ); - return {}; - } catch (e) { - // e ⇒ errorObjectに変換" - const error = createErrorObject(e); - - const errorMessage = getTranslationID("common.message.internalServerError"); - - thunkApi.dispatch( - openSnackbar({ - level: "error", - message: errorMessage, - }) - ); - - return thunkApi.rejectWithValue({ error }); - } -}); diff --git a/dictation_client/src/features/license/licenseCardActivate/selectors.ts b/dictation_client/src/features/license/licenseCardActivate/selectors.ts deleted file mode 100644 index 8ff2f38..0000000 --- a/dictation_client/src/features/license/licenseCardActivate/selectors.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { RootState } from "../../../app/store"; - -export const selectInputValidationErrors = (state: RootState) => { - const { keyLicense } = state.licenseCardActivate.apps; - const hasErrorIncorrectKeyNumber = checkErrorIncorrectKeyNumber(keyLicense); - return { - hasErrorIncorrectKeyNumber, - }; -}; -export const checkErrorIncorrectKeyNumber = (keyLicense: string): boolean => - // // 20+4(20文字+space4個)以外の場合はエラー - keyLicense.length !== 24; - -export const selectKeyLicense = (state: RootState) => - state.licenseCardActivate.apps.keyLicense; - -export const selectIsLoading = (state: RootState) => - state.licenseCardActivate.apps.isLoading; diff --git a/dictation_client/src/features/license/licenseCardActivate/state.ts b/dictation_client/src/features/license/licenseCardActivate/state.ts deleted file mode 100644 index 4ade669..0000000 --- a/dictation_client/src/features/license/licenseCardActivate/state.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface LicenseCardActivateState { - apps: Apps; -} - -export interface Apps { - keyLicense: string; - isLoading: boolean; -} diff --git a/dictation_client/src/pages/LicensePage/cardLicenseActivatePopup.tsx b/dictation_client/src/pages/LicensePage/cardLicenseActivatePopup.tsx index 5bc654b..71a1d6d 100644 --- a/dictation_client/src/pages/LicensePage/cardLicenseActivatePopup.tsx +++ b/dictation_client/src/pages/LicensePage/cardLicenseActivatePopup.tsx @@ -1,20 +1,6 @@ -import React, { useState, useCallback, useEffect, useRef } from "react"; -import { useTranslation } from "react-i18next"; -import { AppDispatch } from "app/store"; -import { useDispatch, useSelector } from "react-redux"; -import _ from "lodash"; -import styles from "../../styles/app.module.scss"; -import { getTranslationID } from "../../translation"; +import React, { useCallback } from "react"; +import styles from "styles/app.module.scss"; import close from "../../assets/images/close.svg"; -import { - activateCardLicenseAsync, - selectKeyLicense, - cleanupApps, - selectIsLoading, - changeKeyLicense, - selectInputValidationErrors, -} from "../../features/license/licenseCardActivate/index"; -import progress_activit from "../../assets/images/progress_activit.svg"; interface CardLicenseActivatePopupProps { onClose: () => void; @@ -24,92 +10,18 @@ export const CardLicenseActivatePopup: React.FC< CardLicenseActivatePopupProps > = (props) => { const { onClose } = props; - const { t } = useTranslation(); - const dispatch: AppDispatch = useDispatch(); - const cardLicenseKey = useSelector(selectKeyLicense); - const [keyNumber, setKeyNumber] = useState(cardLicenseKey); - const isLoading = useSelector(selectIsLoading); // ポップアップを閉じる処理 const closePopup = useCallback(() => { - if (isLoading) { - return; - } onClose(); - }, [isLoading, onClose]); - - // ブラウザのウィンドウが閉じられようとしている場合に発火するイベントハンドラ - const handleBeforeUnload = (e: BeforeUnloadEvent) => { - // isLoadingがtrueの場合は確認ダイアログを表示する - if (isLoading) { - e.preventDefault(); - // ChromeではreturnValueが必要 - e.returnValue = ""; - } - }; - // コンポーネントがマウントされた時にイベントハンドラを登録する - useEffect(() => { - window.addEventListener("beforeunload", handleBeforeUnload); - // コンポーネントがアンマウントされるときにイベントハンドラを解除する - return () => { - window.removeEventListener("beforeunload", handleBeforeUnload); - }; - }); - - useEffect( - () => () => { - // useEffectのreturnとしてcleanupAppsを実行することで、ポップアップのアンマウント時に初期化を行う - dispatch(cleanupApps()); - }, - [dispatch] - ); - - const [isPushActivateButton, setIsPushActivateButton] = - useState(false); - - // エラー宣言 - const { hasErrorIncorrectKeyNumber } = useSelector( - selectInputValidationErrors - ); - - // activateボタン押下時 - const onActivateLicense = useCallback(async () => { - setIsPushActivateButton(true); - - if (keyNumber.length !== 24) { - const inputBox = document.getElementById("inputBox"); - // カーソルをテキストボックスに戻す - if (inputBox) { - inputBox.focus(); - } - return; - } - - // activateAPIの呼び出し - const cardLicenseKeyWithoutSpaces = keyNumber.replace(/\s/g, ""); - const { meta } = await dispatch( - activateCardLicenseAsync({ cardLicenseKey: cardLicenseKeyWithoutSpaces }) - ); - setIsPushActivateButton(false); - - // カーソルをテキストボックスに戻す - const inputBox = document.getElementById("inputBox"); - if (inputBox) { - inputBox.focus(); - } - - if (meta.requestStatus === "fulfilled") { - dispatch(cleanupApps()); - setKeyNumber(""); - } - }, [keyNumber, dispatch]); + }, [onClose]); // HTML return (

- {t(getTranslationID("cardLicenseActivatePopupPage.label.title"))} + Activate License Key @@ -118,93 +30,24 @@ export const CardLicenseActivatePopup: React.FC<

-
- -
+
Key number
{ - let input = e.target.value.toUpperCase(); - input = input.replace(/[^A-Z0-9]/g, ""); - // _.chunk関数で、配列の要素を一定の要素数ごとに分ける - input = _.chunk(input, 4) - .map((a) => a.join("")) - .join(" "); - setKeyNumber(input); - if (input.includes("\n")) { - dispatch( - changeKeyLicense({ - keyLicense: e.target.value.toUpperCase(), - }) - ); - onActivateLicense(); - } - }} - onBlur={(e) => { - dispatch( - changeKeyLicense({ - keyLicense: e.target.value.toUpperCase(), - }) - ); - }} - onKeyDown={(e) => { - if (e.key === "Enter") { - e.preventDefault(); - const input = e.target.value.toUpperCase(); - setKeyNumber(input); - const button = document.getElementById("button"); - if (button) { - button.focus(); - button.click(); - } - } - }} /> - {isPushActivateButton && hasErrorIncorrectKeyNumber && ( - - {t( - getTranslationID( - "cardLicenseActivatePopupPage.label.keyNumberIncorrectError" - ) - )} - - )}
- Loading
diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index 06e3583..42dc3b5 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -238,17 +238,5 @@ "label": { "cardLicenseButton": "(de)License Card" } - }, - "cardLicenseActivatePopupPage": { - "label": { - "title": "(de)Activate License Key", - "keyNumber": "(de)Key number", - "activateButton": "(de)activate", - "keyNumberIncorrectError": "(de)Key Numberには20桁の半角大文字英数字を入力してください。" - }, - "message": { - "LicenseKeyNotExistError": "(de)入力されたライセンスキーは存在しません。ライセンスキーを再度お確かめください。", - "LicenseKeyAlreadyActivatedError": "(de)入力されたライセンスキーは、既に有効化されています。ライセンスキーを再度お確かめください。" - } } } diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index b2b4e35..a7f3f7b 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -238,17 +238,5 @@ "label": { "cardLicenseButton": "License Card" } - }, - "cardLicenseActivatePopupPage": { - "label": { - "title": "Activate License Key", - "keyNumber": "Key number", - "activateButton": "activate", - "keyNumberIncorrectError": "Key Numberには20桁の半角大文字英数字を入力してください。" - }, - "message": { - "LicenseKeyNotExistError": "入力されたライセンスキーは存在しません。ライセンスキーを再度お確かめください。", - "LicenseKeyAlreadyActivatedError": "入力されたライセンスキーは、既に有効化されています。ライセンスキーを再度お確かめください。" - } } } diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index a575daf..85e871c 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -238,17 +238,5 @@ "label": { "cardLicenseButton": "(es)License Card" } - }, - "cardLicenseActivatePopupPage": { - "label": { - "title": "(es)Activate License Key", - "keyNumber": "(es)Key number", - "activateButton": "(es)activate", - "keyNumberIncorrectError": "(es)Key Numberには20桁の半角大文字英数字を入力してください。" - }, - "message": { - "LicenseKeyNotExistError": "(es)入力されたライセンスキーは存在しません。ライセンスキーを再度お確かめください。", - "LicenseKeyAlreadyActivatedError": "(es)入力されたライセンスキーは、既に有効化されています。ライセンスキーを再度お確かめください。" - } } } diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index 9a19b52..4dfaae3 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -238,17 +238,5 @@ "label": { "cardLicenseButton": "(fr)License Card" } - }, - "cardLicenseActivatePopupPage": { - "label": { - "title": "(fr)Activate License Key", - "keyNumber": "(fr)Key number", - "activateButton": "(fr)activate", - "keyNumberIncorrectError": "(fr)Key Numberには20桁の半角大文字英数字を入力してください。" - }, - "message": { - "LicenseKeyNotExistError": "(fr)入力されたライセンスキーは存在しません。ライセンスキーを再度お確かめください。", - "LicenseKeyAlreadyActivatedError": "(fr)入力されたライセンスキーは、既に有効化されています。ライセンスキーを再度お確かめください。" - } } } diff --git a/dictation_server/.env b/dictation_server/.env index cfcf9f8..eba8c6a 100644 --- a/dictation_server/.env +++ b/dictation_server/.env @@ -13,3 +13,4 @@ TENANT_NAME=adb2codmsdev SIGNIN_FLOW_NAME=b2c_1_signin_dev EMAIL_CONFIRM_LIFETIME=86400000 APP_DOMAIN=https://10.1.0.10:4443/ +STORAGE_TOKEN_EXPIRE_TIME=2 \ No newline at end of file diff --git a/dictation_server/.env.local.example b/dictation_server/.env.local.example index 64e244c..c4deccc 100644 --- a/dictation_server/.env.local.example +++ b/dictation_server/.env.local.example @@ -16,7 +16,6 @@ MAIL_FROM=xxxxx@xxxxx.xxxx NOTIFICATION_HUB_NAME=ntf-odms-shared NOTIFICATION_HUB_CONNECT_STRING=XXXXXXXXXXXXXXXXXX APP_DOMAIN=http://localhost:8081/ -STORAGE_TOKEN_EXPIRE_TIME=30 STORAGE_ACCOUNT_NAME_US=saodmsusdev STORAGE_ACCOUNT_NAME_AU=saodmsaudev STORAGE_ACCOUNT_NAME_EU=saodmseudev diff --git a/dictation_server/src/common/error/code.ts b/dictation_server/src/common/error/code.ts index 08823d3..4bee6e5 100644 --- a/dictation_server/src/common/error/code.ts +++ b/dictation_server/src/common/error/code.ts @@ -36,4 +36,8 @@ export const ErrorCodes = [ 'E010501', // アカウント不在エラー 'E010601', // タスク変更不可エラー(タスクが変更できる状態でない、またはタスクが存在しない) 'E010602', // タスク変更権限不足エラー + 'E010603', // タスク不在エラー + 'E010701', // Blobファイル不在エラー + 'E010801', // ライセンス不在エラー + 'E010802', // ライセンス取り込み済みエラー ] as const; diff --git a/dictation_server/src/common/error/message.ts b/dictation_server/src/common/error/message.ts index 4e74380..30ab3c2 100644 --- a/dictation_server/src/common/error/message.ts +++ b/dictation_server/src/common/error/message.ts @@ -25,4 +25,8 @@ export const errors: Errors = { E010501: 'Account not Found Error.', E010601: 'Task is not Editable Error', E010602: 'No task edit permissions Error', + E010603: 'Task not found Error.', + E010701: 'File not found in Blob Storage Error.', + E010801: 'License not exist Error', + E010802: 'License already activated Error', }; diff --git a/dictation_server/src/features/files/files.controller.ts b/dictation_server/src/features/files/files.controller.ts index c091664..5aeb394 100644 --- a/dictation_server/src/features/files/files.controller.ts +++ b/dictation_server/src/features/files/files.controller.ts @@ -177,16 +177,24 @@ export class FilesController { '指定した音声ファイルのBlob Storage上のダウンロード先アクセスURLを取得します', }) @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards( + RoleGuard.requireds({ roles: [USER_ROLES.AUTHOR, USER_ROLES.TYPIST] }), + ) async downloadLocation( - @Headers() headers, + @Req() req: Request, @Query() body: AudioDownloadLocationRequest, ): Promise { - // eslint-disable-next-line @typescript-eslint/no-unused-vars const { audioFileId } = body; - // コンテナ作成処理の前にアクセストークンの認証を行う - // - return { url: '' }; + const token = retrieveAuthorizationToken(req); + const accessToken = jwt.decode(token, { json: true }) as AccessToken; + const url = await this.filesService.publishAudioFileDownloadSas( + accessToken.userId, + audioFileId, + ); + + return { url }; } @Get('template/download-location') diff --git a/dictation_server/src/features/files/files.service.spec.ts b/dictation_server/src/features/files/files.service.spec.ts index bda834b..7b677bf 100644 --- a/dictation_server/src/features/files/files.service.spec.ts +++ b/dictation_server/src/features/files/files.service.spec.ts @@ -6,8 +6,16 @@ import { makeDefaultUsersRepositoryMockValue, makeFilesServiceMock, } from './test/files.service.mock'; +import { DataSource } from 'typeorm'; +import { + createAccount, + createTask, + createUser, + makeTestingModuleWithBlob, +} from './test/utility'; +import { FilesService } from './files.service'; -describe('FilesService', () => { +describe('音声ファイルアップロードURL取得', () => { it('アップロードSASトークンが乗っているURLを返却する', async () => { const blobParam = makeBlobstorageServiceMockValue(); const userRepoParam = makeDefaultUsersRepositoryMockValue(); @@ -95,7 +103,9 @@ describe('FilesService', () => { new HttpException(makeErrorResponse('E009999'), HttpStatus.UNAUTHORIZED), ); }); +}); +describe('タスク作成', () => { it('文字起こしタスクを作成できる', async () => { const blobParam = makeBlobstorageServiceMockValue(); const userRepoParam = makeDefaultUsersRepositoryMockValue(); diff --git a/dictation_server/src/features/files/files.service.ts b/dictation_server/src/features/files/files.service.ts index 260f12a..94b196e 100644 --- a/dictation_server/src/features/files/files.service.ts +++ b/dictation_server/src/features/files/files.service.ts @@ -28,6 +28,7 @@ export class FilesService { private readonly logger = new Logger(FilesService.name); constructor( private readonly usersRepository: UsersRepositoryService, + private readonly tasksRepository: TasksRepositoryService, private readonly tasksRepositoryService: TasksRepositoryService, private readonly blobStorageService: BlobstorageService, ) {} @@ -127,13 +128,19 @@ export class FilesService { } try { + // URLにSASトークンがついている場合は取り除く + const urlObj = new URL(url); + urlObj.search = ''; + const fileUrl = urlObj.toString(); + this.logger.log(`Request URL: ${url}, Without param URL${fileUrl}`); + // 文字起こしタスク追加(音声ファイルとオプションアイテムも同時に追加) // 追加時に末尾のJOBナンバーにインクリメントする const task = await this.tasksRepositoryService.create( user.account_id, user.id, priority, - url, + fileUrl, fileName, authorId, workType, diff --git a/dictation_server/src/features/files/test/files.service.mock.ts b/dictation_server/src/features/files/test/files.service.mock.ts index c6b87cb..a841f05 100644 --- a/dictation_server/src/features/files/test/files.service.mock.ts +++ b/dictation_server/src/features/files/test/files.service.mock.ts @@ -9,7 +9,9 @@ import { Task } from '../../../repositories/tasks/entity/task.entity'; export type BlobstorageServiceMockValue = { createContainer: void | Error; containerExists: boolean | Error; + fileExists: boolean | Error; publishUploadSas: string | Error; + publishDownloadSas: string | Error; }; export type UsersRepositoryMockValue = { @@ -47,13 +49,23 @@ export const makeFilesServiceMock = async ( export const makeBlobstorageServiceMock = ( value: BlobstorageServiceMockValue, ) => { - const { containerExists, createContainer, publishUploadSas } = value; + const { + containerExists, + fileExists, + createContainer, + publishUploadSas, + publishDownloadSas, + } = value; return { containerExists: containerExists instanceof Error ? jest.fn, []>().mockRejectedValue(containerExists) : jest.fn, []>().mockResolvedValue(containerExists), + fileExists: + fileExists instanceof Error + ? jest.fn, []>().mockRejectedValue(fileExists) + : jest.fn, []>().mockResolvedValue(fileExists), createContainer: createContainer instanceof Error ? jest.fn, []>().mockRejectedValue(createContainer) @@ -62,6 +74,10 @@ export const makeBlobstorageServiceMock = ( publishUploadSas instanceof Error ? jest.fn, []>().mockRejectedValue(publishUploadSas) : jest.fn, []>().mockResolvedValue(publishUploadSas), + publishDownloadSas: + publishDownloadSas instanceof Error + ? jest.fn, []>().mockRejectedValue(publishDownloadSas) + : jest.fn, []>().mockResolvedValue(publishDownloadSas), }; }; @@ -80,7 +96,9 @@ export const makeBlobstorageServiceMockValue = (): BlobstorageServiceMockValue => { return { containerExists: true, + fileExists: true, publishUploadSas: 'https://blob-storage?sas-token', + publishDownloadSas: 'https://blob-storage?sas-token', createContainer: undefined, }; }; diff --git a/dictation_server/src/features/licenses/licenses.controller.ts b/dictation_server/src/features/licenses/licenses.controller.ts index 2a70711..8925c49 100644 --- a/dictation_server/src/features/licenses/licenses.controller.ts +++ b/dictation_server/src/features/licenses/licenses.controller.ts @@ -28,7 +28,7 @@ import { retrieveAuthorizationToken } from '../../common/http/helper'; import { AccessToken } from '../../common/token'; import { AuthGuard } from '../../common/guards/auth/authguards'; import { RoleGuard } from '../../common/guards/role/roleguards'; -import { ADMIN_ROLES } from '../../constants'; +import { ADMIN_ROLES, TIER_1, TIER_5 } from '../../constants'; import jwt from 'jsonwebtoken'; import { makeErrorResponse } from '../../common/error/makeErrorResponse'; @@ -112,7 +112,7 @@ export class LicensesController { const payload = jwt.decode(accessToken, { json: true }) as AccessToken; // 第一階層以外は401を返す(後々UseGuardsで弾く) - if (payload.tier != 1) { + if (payload.tier != TIER_1) { throw new HttpException( makeErrorResponse('E000108'), HttpStatus.UNAUTHORIZED, @@ -159,6 +159,22 @@ export class LicensesController { console.log(req.header('Authorization')); console.log(body); + const accessToken = retrieveAuthorizationToken(req); + const payload = jwt.decode(accessToken, { json: true }) as AccessToken; + + // TODO 第五階層以外は401を返す(後々UseGuardsで弾く) + if (payload.tier != TIER_5) { + throw new HttpException( + makeErrorResponse('E000108'), + HttpStatus.UNAUTHORIZED, + ); + } + + await this.licensesService.activateCardLicenseKey( + payload.userId, + body.cardLicenseKey, + ); + return {}; } } diff --git a/dictation_server/src/features/licenses/licenses.service.spec.ts b/dictation_server/src/features/licenses/licenses.service.spec.ts index 446326e..553f2cb 100644 --- a/dictation_server/src/features/licenses/licenses.service.spec.ts +++ b/dictation_server/src/features/licenses/licenses.service.spec.ts @@ -3,6 +3,7 @@ import { CreateOrdersRequest, IssueCardLicensesRequest, IssueCardLicensesResponse, + ActivateCardLicensesRequest, } from './types/types'; import { makeDefaultAccountsRepositoryMockValue, @@ -12,14 +13,23 @@ import { } from './test/liscense.service.mock'; import { makeErrorResponse } from '../../common/error/makeErrorResponse'; import { HttpException, HttpStatus } from '@nestjs/common'; -import { PoNumberAlreadyExistError } from '../../repositories/licenses/errors/types'; +import { + PoNumberAlreadyExistError, + LicenseKeyAlreadyActivatedError, + LicenseNotExistError, +} from '../../repositories/licenses/errors/types'; import { LicensesService } from './licenses.service'; import { makeTestingModule } from '../../common/test/modules'; import { DataSource } from 'typeorm'; import { createAccount, createUser, + createCardLicense, + createLicense, + createCardLicenseIssue, selectCardLicensesCount, + selectCardLicense, + selectLicense, } from './test/utility'; describe('LicensesService', () => { @@ -177,6 +187,92 @@ describe('LicensesService', () => { ), ); }); + it('カードライセンス取り込みが完了する', async () => { + const lisencesRepositoryMockValue = + makeDefaultLicensesRepositoryMockValue(); + const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const accountsRepositoryMockValue = + makeDefaultAccountsRepositoryMockValue(); + const service = await makeLicensesServiceMock( + lisencesRepositoryMockValue, + usersRepositoryMockValue, + accountsRepositoryMockValue, + ); + const body = new ActivateCardLicensesRequest(); + const token: AccessToken = { userId: '0001', role: '', tier: 5 }; + body.cardLicenseKey = 'WZCETXC0Z9PQZ9GKRGGY'; + expect( + await service.activateCardLicenseKey(token.userId, body.cardLicenseKey), + ).toEqual(undefined); + }); + it('カードライセンス取り込みに失敗した場合、エラーになる(DBエラー)', async () => { + const lisencesRepositoryMockValue = + makeDefaultLicensesRepositoryMockValue(); + lisencesRepositoryMockValue.activateCardLicense = new Error('DB failed'); + const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const accountsRepositoryMockValue = + makeDefaultAccountsRepositoryMockValue(); + const service = await makeLicensesServiceMock( + lisencesRepositoryMockValue, + usersRepositoryMockValue, + accountsRepositoryMockValue, + ); + const body = new ActivateCardLicensesRequest(); + const token: AccessToken = { userId: '0001', role: '', tier: 5 }; + body.cardLicenseKey = 'WZCETXC0Z9PQZ9GKRGGY'; + await expect( + service.activateCardLicenseKey(token.userId, body.cardLicenseKey), + ).rejects.toEqual( + new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ), + ); + }); + it('カードライセンス取り込みに失敗した場合、エラーになる(ライセンスが存在しないエラー)', async () => { + const lisencesRepositoryMockValue = + makeDefaultLicensesRepositoryMockValue(); + lisencesRepositoryMockValue.activateCardLicense = + new LicenseNotExistError(); + const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const accountsRepositoryMockValue = + makeDefaultAccountsRepositoryMockValue(); + const service = await makeLicensesServiceMock( + lisencesRepositoryMockValue, + usersRepositoryMockValue, + accountsRepositoryMockValue, + ); + const body = new ActivateCardLicensesRequest(); + const token: AccessToken = { userId: '0001', role: '', tier: 5 }; + body.cardLicenseKey = 'WZCETXC0Z9PQZ9GKRGGY'; + await expect( + service.activateCardLicenseKey(token.userId, body.cardLicenseKey), + ).rejects.toEqual( + new HttpException(makeErrorResponse('E010801'), HttpStatus.BAD_REQUEST), + ); + }); + it('カードライセンス取り込みに失敗した場合、エラーになる(ライセンスが既に取り込まれているエラー)', async () => { + const lisencesRepositoryMockValue = + makeDefaultLicensesRepositoryMockValue(); + lisencesRepositoryMockValue.activateCardLicense = + new LicenseKeyAlreadyActivatedError(); + const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const accountsRepositoryMockValue = + makeDefaultAccountsRepositoryMockValue(); + const service = await makeLicensesServiceMock( + lisencesRepositoryMockValue, + usersRepositoryMockValue, + accountsRepositoryMockValue, + ); + const body = new ActivateCardLicensesRequest(); + const token: AccessToken = { userId: '0001', role: '', tier: 5 }; + body.cardLicenseKey = 'WZCETXC0Z9PQZ9GKRGGY'; + await expect( + service.activateCardLicenseKey(token.userId, body.cardLicenseKey), + ).rejects.toEqual( + new HttpException(makeErrorResponse('E010802'), HttpStatus.BAD_REQUEST), + ); + }); }); describe('DBテスト', () => { @@ -214,4 +310,38 @@ describe('DBテスト', () => { const dbSelectResult = await selectCardLicensesCount(source); expect(dbSelectResult.count).toEqual(issueCount); }); + + it('カードライセンス取り込みが完了する', async () => { + const module = await makeTestingModule(source); + + const { accountId } = await createAccount(source); + const { externalId } = await createUser( + source, + accountId, + 'userId', + 'admin', + ); + + const cardLicenseKey = 'WZCETXC0Z9PQZ9GKRGGY'; + const defaultAccountId = 150; + const licenseId = 50; + const issueId = 100; + + await createLicense(source, licenseId, defaultAccountId); + await createCardLicense(source, licenseId, issueId, cardLicenseKey); + await createCardLicenseIssue(source, issueId); + + const service = module.get(LicensesService); + + await service.activateCardLicenseKey(externalId, cardLicenseKey); + const dbSelectResultFromCardLicense = await selectCardLicense( + source, + cardLicenseKey, + ); + const dbSelectResultFromLicense = await selectLicense(source, licenseId); + expect( + dbSelectResultFromCardLicense.cardLicense.activated_at, + ).toBeDefined(); + expect(dbSelectResultFromLicense.license.account_id).toEqual(accountId); + }); }); diff --git a/dictation_server/src/features/licenses/licenses.service.ts b/dictation_server/src/features/licenses/licenses.service.ts index 3767636..2c13742 100644 --- a/dictation_server/src/features/licenses/licenses.service.ts +++ b/dictation_server/src/features/licenses/licenses.service.ts @@ -4,7 +4,11 @@ import { AccessToken } from '../../common/token'; import { UsersRepositoryService } from '../../repositories/users/users.repository.service'; import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service'; import { AccountNotFoundError } from '../../repositories/accounts/errors/types'; -import { PoNumberAlreadyExistError } from '../../repositories/licenses/errors/types'; +import { + PoNumberAlreadyExistError, + LicenseNotExistError, + LicenseKeyAlreadyActivatedError, +} from '../../repositories/licenses/errors/types'; import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service'; import { UserNotFoundError } from '../../repositories/users/errors/types'; import { IssueCardLicensesResponse } from './types/types'; @@ -140,4 +144,71 @@ export class LicensesService { ); } } + + /** + * card license activate + * @param externalId + * @param cardLicenseKey + */ + async activateCardLicenseKey( + externalId: string, + cardLicenseKey: string, + ): Promise { + this.logger.log( + `[IN] ${this.activateCardLicenseKey.name}, argCardLicenseKey: ${cardLicenseKey}`, + ); + let myAccountId: number; + + // ユーザIDからアカウントIDを取得する + try { + myAccountId = ( + await this.usersRepository.findUserByExternalId(externalId) + ).account_id; + } catch (e) { + this.logger.error(`error=${e}`); + switch (e.constructor) { + case UserNotFoundError: + throw new HttpException( + makeErrorResponse('E010204'), + HttpStatus.BAD_REQUEST, + ); + default: + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + // カードライセンスを取り込む + try { + await this.licensesRepository.activateCardLicense( + myAccountId, + cardLicenseKey, + ); + } catch (e) { + this.logger.error(`error=${e}`); + this.logger.error('cardLicenseKey activate failed'); + + switch (e.constructor) { + case LicenseNotExistError: + throw new HttpException( + makeErrorResponse('E010801'), + HttpStatus.BAD_REQUEST, + ); + case LicenseKeyAlreadyActivatedError: + throw new HttpException( + makeErrorResponse('E010802'), + HttpStatus.BAD_REQUEST, + ); + default: + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + this.logger.log(`[OUT] ${this.activateCardLicenseKey.name}`); + return; + } } diff --git a/dictation_server/src/features/licenses/test/liscense.service.mock.ts b/dictation_server/src/features/licenses/test/liscense.service.mock.ts index bf00c7c..58eba66 100644 --- a/dictation_server/src/features/licenses/test/liscense.service.mock.ts +++ b/dictation_server/src/features/licenses/test/liscense.service.mock.ts @@ -9,6 +9,7 @@ import { AccountsRepositoryService } from '../../../repositories/accounts/accoun export type LicensesRepositoryMockValue = { order: undefined | Error; createCardLicenses: string[] | Error; + activateCardLicense: undefined | Error; }; export type AccountsRepositoryMockValue = { @@ -45,7 +46,7 @@ export const makeLicensesServiceMock = async ( export const makeLicensesRepositoryMock = ( value: LicensesRepositoryMockValue, ) => { - const { order, createCardLicenses } = value; + const { order, createCardLicenses, activateCardLicense } = value; return { order: order instanceof Error @@ -57,6 +58,10 @@ export const makeLicensesRepositoryMock = ( : jest .fn, []>() .mockResolvedValue(createCardLicenses), + activateCardLicense: + activateCardLicense instanceof Error + ? jest.fn, []>().mockRejectedValue(activateCardLicense) + : jest.fn, []>().mockResolvedValue(activateCardLicense), }; }; @@ -100,6 +105,7 @@ export const makeDefaultLicensesRepositoryMockValue = 'XYLEWNY2LR6Q657CZE41', 'AEJWRFFSWRQYQQJ6WVLV', ], + activateCardLicense: undefined, }; }; export const makeDefaultUsersRepositoryMockValue = diff --git a/dictation_server/src/features/licenses/test/utility.ts b/dictation_server/src/features/licenses/test/utility.ts index 740896d..70cc546 100644 --- a/dictation_server/src/features/licenses/test/utility.ts +++ b/dictation_server/src/features/licenses/test/utility.ts @@ -1,7 +1,11 @@ import { DataSource } from 'typeorm'; import { User } from '../../../repositories/users/entity/user.entity'; import { Account } from '../../../repositories/accounts/entity/account.entity'; -import { CardLicense } from '../../../repositories/licenses/entity/license.entity'; +import { + License, + CardLicense, + CardLicenseIssue, +} from '../../../repositories/licenses/entity/license.entity'; export const createAccount = async ( datasource: DataSource, @@ -49,9 +53,92 @@ export const createUser = async ( return { userId: user.id, externalId: external_id }; }; +export const createLicense = async ( + datasource: DataSource, + licenseId: number, + accountId: number, +): Promise => { + const { identifiers } = await datasource.getRepository(License).insert({ + id: licenseId, + expiry_date: null, + account_id: accountId, + type: 'card', + status: 'Unallocated', + allocated_user_id: null, + order_id: null, + deleted_at: null, + delete_order_id: null, + created_by: 'test_runner', + created_at: new Date(), + updated_by: 'updater', + updated_at: new Date(), + }); + identifiers.pop() as License; +}; + +export const createCardLicense = async ( + datasource: DataSource, + licenseId: number, + issueId: number, + cardLicenseKey: string, +): Promise => { + const { identifiers } = await datasource.getRepository(CardLicense).insert({ + license_id: licenseId, + issue_id: issueId, + card_license_key: cardLicenseKey, + activated_at: null, + created_by: 'test_runner', + created_at: new Date(), + updated_by: 'updater', + updated_at: new Date(), + }); + identifiers.pop() as CardLicense; +}; + +export const createCardLicenseIssue = async ( + datasource: DataSource, + issueId: number, +): Promise => { + const { identifiers } = await datasource + .getRepository(CardLicenseIssue) + .insert({ + id: issueId, + issued_at: new Date(), + created_by: 'test_runner', + created_at: new Date(), + updated_by: 'updater', + updated_at: new Date(), + }); + identifiers.pop() as CardLicenseIssue; +}; + export const selectCardLicensesCount = async ( datasource: DataSource, ): Promise<{ count: number }> => { const count = await datasource.getRepository(CardLicense).count(); return { count: count }; }; + +export const selectCardLicense = async ( + datasource: DataSource, + cardLicenseKey: string, +): Promise<{ cardLicense: CardLicense }> => { + const cardLicense = await datasource.getRepository(CardLicense).findOne({ + where: { + card_license_key: cardLicenseKey, + }, + }); + return { cardLicense }; +}; + +export const selectLicense = async ( + datasource: DataSource, + id: number, +): Promise<{ license: License }> => { + const license = await datasource.getRepository(License).findOne({ + where: { + id: id, + }, + }); + return { license }; +}; diff --git a/dictation_server/src/features/tasks/tasks.service.spec.ts b/dictation_server/src/features/tasks/tasks.service.spec.ts index 06adcbd..67c6d70 100644 --- a/dictation_server/src/features/tasks/tasks.service.spec.ts +++ b/dictation_server/src/features/tasks/tasks.service.spec.ts @@ -19,7 +19,6 @@ import { } from './test/utility'; import { Adb2cTooManyRequestsError } from '../../gateways/adb2c/adb2c.service'; import { makeTestingModule } from '../../common/test/modules'; -import { TasksNotFoundError } from '../../repositories/tasks/errors/types'; describe('TasksService', () => { it('タスク一覧を取得できる(admin)', async () => { diff --git a/dictation_server/src/gateways/blobstorage/blobstorage.service.ts b/dictation_server/src/gateways/blobstorage/blobstorage.service.ts index 824ce13..b7e92b6 100644 --- a/dictation_server/src/gateways/blobstorage/blobstorage.service.ts +++ b/dictation_server/src/gateways/blobstorage/blobstorage.service.ts @@ -4,7 +4,9 @@ import { StorageSharedKeyCredential, ContainerClient, ContainerSASPermissions, + BlobSASPermissions, generateBlobSASQueryParameters, + BlobClient, } from '@azure/storage-blob'; import { ConfigService } from '@nestjs/config'; import { @@ -83,6 +85,27 @@ export class BlobstorageService { return exists; } + /** + * Files exists + * @param accountId + * @param country + * @param path + * @param fileName + * @param containerUrl + * @returns exists + */ + async fileExists( + accountId: number, + country: string, + filePath: string, + ): Promise { + const containerClient = this.getContainerClient(accountId, country); + const blob = containerClient.getBlobClient(`${filePath}`); + const exists = await blob.exists(); + + return exists; + } + /** * SASトークン付きのBlobStorageアップロードURLを生成し返却します * @param accountId @@ -109,10 +132,9 @@ export class BlobstorageService { } //SASの有効期限を設定 - //TODO 有効期限は仮で30分 const expiryDate = new Date(); - expiryDate.setMinutes( - expiryDate.getMinutes() + + expiryDate.setHours( + expiryDate.getHours() + this.configService.get('STORAGE_TOKEN_EXPIRE_TIME'), ); @@ -137,6 +159,63 @@ export class BlobstorageService { return url.toString(); } + /** + * SASトークン付きのBlobStorageダウンロードURLを生成し返却します + * @param accountId + * @param country + * @param path + * @param fileName + * @returns download sas + */ + async publishDownloadSas( + accountId: number, + country: string, + filePath: string, + ): Promise { + this.logger.log(`[IN] ${this.publishDownloadSas.name}`); + let containerClient: ContainerClient; + let blobClient: BlobClient; + let sharedKeyCredential: StorageSharedKeyCredential; + try { + // コンテナ名を指定してClientを取得 + containerClient = this.getContainerClient(accountId, country); + // コンテナ内のBlobパス名を指定してClientを取得 + blobClient = containerClient.getBlobClient(`${filePath}`); + // 国に対応したリージョンの接続情報を取得する + sharedKeyCredential = this.getSharedKeyCredential(country); + } catch (e) { + this.logger.error(`error=${e}`); + throw e; + } + + //SASの有効期限を設定 + const expiryDate = new Date(); + expiryDate.setHours( + expiryDate.getHours() + + this.configService.get('STORAGE_TOKEN_EXPIRE_TIME'), + ); + + //SASの権限を設定(ダウンロードのため読み取り許可) + const permissions = new BlobSASPermissions(); + permissions.read = true; + + //SASを発行 + const sasToken = generateBlobSASQueryParameters( + { + containerName: containerClient.containerName, + blobName: blobClient.name, + permissions: permissions, + startsOn: new Date(), + expiresOn: expiryDate, + }, + sharedKeyCredential, + ); + + const url = new URL(blobClient.url); + url.search = `${sasToken}`; + return url.toString(); + } + /** * Gets container client * @param companyName diff --git a/dictation_server/src/repositories/licenses/entity/license.entity.ts b/dictation_server/src/repositories/licenses/entity/license.entity.ts index 77f467d..f253eb8 100644 --- a/dictation_server/src/repositories/licenses/entity/license.entity.ts +++ b/dictation_server/src/repositories/licenses/entity/license.entity.ts @@ -3,6 +3,7 @@ import { Column, PrimaryGeneratedColumn, CreateDateColumn, + UpdateDateColumn, } from 'typeorm'; @Entity({ name: 'license_orders' }) @@ -63,6 +64,18 @@ export class License { @Column({ nullable: true }) delete_order_id: number; + + @Column({ nullable: true }) + created_by: string; + + @CreateDateColumn() + created_at: Date; + + @Column({ nullable: true }) + updated_by: string; + + @UpdateDateColumn() + updated_at: Date; } @Entity({ name: 'licenses_history' }) export class LicenseHistory { @@ -92,6 +105,18 @@ export class CardLicenseIssue { @Column() issued_at: Date; + + @Column({ nullable: true }) + created_by: string; + + @CreateDateColumn() + created_at: Date; + + @Column({ nullable: true }) + updated_by: string; + + @UpdateDateColumn() + updated_at: Date; } @Entity({ name: 'card_licenses' }) @@ -107,4 +132,16 @@ export class CardLicense { @Column({ nullable: true }) activated_at: Date; + + @Column({ nullable: true }) + created_by: string; + + @CreateDateColumn() + created_at: Date; + + @Column({ nullable: true }) + updated_by: string; + + @UpdateDateColumn({}) + updated_at: Date; } diff --git a/dictation_server/src/repositories/licenses/errors/types.ts b/dictation_server/src/repositories/licenses/errors/types.ts index eca9967..972f850 100644 --- a/dictation_server/src/repositories/licenses/errors/types.ts +++ b/dictation_server/src/repositories/licenses/errors/types.ts @@ -1,2 +1,8 @@ // POナンバーがすでに存在するエラー export class PoNumberAlreadyExistError extends Error {} + +// 取り込むカードライセンスが存在しないエラー +export class LicenseNotExistError extends Error {} + +// 取り込むライセンスが既に取り込み済みのエラー +export class LicenseKeyAlreadyActivatedError extends Error {} diff --git a/dictation_server/src/repositories/licenses/licenses.repository.module.ts b/dictation_server/src/repositories/licenses/licenses.repository.module.ts index a1ec5bf..a142c1f 100644 --- a/dictation_server/src/repositories/licenses/licenses.repository.module.ts +++ b/dictation_server/src/repositories/licenses/licenses.repository.module.ts @@ -1,10 +1,22 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { CardLicense, CardLicenseIssue, License, LicenseOrder } from './entity/license.entity'; +import { + CardLicense, + CardLicenseIssue, + License, + LicenseOrder, +} from './entity/license.entity'; import { LicensesRepositoryService } from './licenses.repository.service'; @Module({ - imports: [TypeOrmModule.forFeature([LicenseOrder,License,CardLicense,CardLicenseIssue])], + imports: [ + TypeOrmModule.forFeature([ + LicenseOrder, + License, + CardLicense, + CardLicenseIssue, + ]), + ], providers: [LicensesRepositoryService], exports: [LicensesRepositoryService], }) diff --git a/dictation_server/src/repositories/licenses/licenses.repository.service.ts b/dictation_server/src/repositories/licenses/licenses.repository.service.ts index 16e0685..04fc010 100644 --- a/dictation_server/src/repositories/licenses/licenses.repository.service.ts +++ b/dictation_server/src/repositories/licenses/licenses.repository.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { DataSource, In } from 'typeorm'; import { LicenseOrder, @@ -13,11 +13,16 @@ import { LICENSE_STATUS_ISSUED, LICENSE_TYPE, } from '../../constants'; -import { PoNumberAlreadyExistError } from './errors/types'; +import { + PoNumberAlreadyExistError, + LicenseNotExistError, + LicenseKeyAlreadyActivatedError, +} from './errors/types'; @Injectable() export class LicensesRepositoryService { constructor(private dataSource: DataSource) {} + private readonly logger = new Logger(LicensesRepositoryService.name); async order( poNumber: string, @@ -185,4 +190,69 @@ export class LicensesRepositoryService { return licenseKeys; } + + /** + * カードライセンスを取り込む + * @param accountId + * @param licenseKey + * @returns void + */ + async activateCardLicense( + accountId: number, + licenseKey: string, + ): Promise { + await this.dataSource.transaction(async (entityManager) => { + const cardLicenseRepo = entityManager.getRepository(CardLicense); + + // カードライセンステーブルを検索 + const targetCardLicense = await cardLicenseRepo.findOne({ + where: { + card_license_key: licenseKey, + }, + }); + // カードライセンスが存在しなければエラー + if (!targetCardLicense) { + this.logger.error( + `card license key not exist. card_licence_key: ${licenseKey}`, + ); + throw new LicenseNotExistError(); + } + // 既に取り込み済みならエラー + if (targetCardLicense.activated_at) { + this.logger.error( + `card license already activated. card_licence_key: ${licenseKey}`, + ); + throw new LicenseKeyAlreadyActivatedError(); + } + + const licensesRepo = entityManager.getRepository(License); + + // ライセンステーブルを検索 + const targetLicense = await licensesRepo.findOne({ + where: { + id: targetCardLicense.license_id, + }, + }); + // ライセンスが存在しなければエラー + if (!targetLicense) { + this.logger.error( + `license not exist. licence_id: ${targetCardLicense.license_id}`, + ); + throw new LicenseNotExistError(); + } + + // ライセンステーブルを更新する + targetLicense.account_id = accountId; + await licensesRepo.save(targetLicense); + + // カードライセンステーブルを更新する + targetCardLicense.activated_at = new Date(); + await cardLicenseRepo.save(targetCardLicense); + + this.logger.log( + `activate success. licence_id: ${targetCardLicense.license_id}`, + ); + }); + return; + } } diff --git a/dictation_server/src/repositories/template_files/template_files.repository.module.ts b/dictation_server/src/repositories/template_files/template_files.repository.module.ts new file mode 100644 index 0000000..000232a --- /dev/null +++ b/dictation_server/src/repositories/template_files/template_files.repository.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { TemplateFilesRepositoryService } from './template_files.repository.service'; +import { TemplateFile } from './entity/template_file.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([TemplateFile])], + providers: [TemplateFilesRepositoryService], + exports: [TemplateFilesRepositoryService], +}) +export class TemplateFilesRepositoryModule {} diff --git a/dictation_server/src/repositories/template_files/template_files.repository.service.ts b/dictation_server/src/repositories/template_files/template_files.repository.service.ts new file mode 100644 index 0000000..b35c191 --- /dev/null +++ b/dictation_server/src/repositories/template_files/template_files.repository.service.ts @@ -0,0 +1,7 @@ +import { Injectable } from '@nestjs/common'; +import { DataSource } from 'typeorm'; + +@Injectable() +export class TemplateFilesRepositoryService { + constructor(private dataSource: DataSource) {} +}