This commit is contained in:
oura.a 2023-10-19 16:33:11 +09:00
commit 215ccbee0c
112 changed files with 4398 additions and 1416 deletions

View File

@ -58,6 +58,7 @@ jobs:
npm run test
env:
JWT_PUBLIC_KEY: $(token-public-key)
JWT_PRIVATE_KEY: $(token-private-key)
SENDGRID_API_KEY: $(sendgrid-api-key)
NOTIFICATION_HUB_NAME: $(notification-hub-name)
NOTIFICATION_HUB_CONNECT_STRING: $(notification-hub-connect-string)
@ -73,6 +74,12 @@ jobs:
ADB2C_TENANT_ID: $(adb2c-tenant-id)
ADB2C_CLIENT_ID: $(adb2c-client-id)
ADB2C_CLIENT_SECRET: $(adb2c-client-secret)
MAIL_FROM: xxxxxx
APP_DOMAIN: xxxxxxxxx
EMAIL_CONFIRM_LIFETIME : 0
TENANT_NAME : xxxxxxxxxxxx
SIGNIN_FLOW_NAME : xxxxxxxxxxxx
STORAGE_TOKEN_EXPIRE_TIME : 0
- task: Docker@0
displayName: build
inputs:

View File

@ -1,5 +1,6 @@
import { Route, Routes } from "react-router-dom";
import TopPage from "pages/TopPage";
import AuthPage from "pages/AuthPage";
import LoginPage from "pages/LoginPage";
import SamplePage from "pages/SamplePage";
import { AuthErrorPage } from "pages/ErrorPage";
@ -20,18 +21,21 @@ import WorkflowPage from "pages/WorkflowPage";
import TypistGroupSettingPage from "pages/TypistGroupSettingPage";
import WorktypeIdSettingPage from "pages/WorkTypeIdSettingPage";
import AccountPage from "pages/AccountPage";
import AcceptToUsePage from "pages/TermsPage";
import { TemplateFilePage } from "pages/TemplateFilePage";
import { AccountDeleteSuccess } from "pages/AccountPage/accountDeleteSuccess";
const AppRouter: React.FC = () => (
<Routes>
<Route path="/" element={<TopPage />} />
<Route path="/auth" element={<AuthPage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/authError" element={<AuthErrorPage />} />
<Route
path="/signup"
element={<SignupPage completeTo="/signup/complete" />}
/>
<Route path="/terms" element={<AcceptToUsePage />} />
<Route path="/signup/complete" element={<SignupCompletePage />} />
<Route path="/mail-confirm/" element={<VerifyPage />} />
<Route path="/mail-confirm/user" element={<UserVerifyPage />} />

View File

@ -127,7 +127,7 @@ export interface AllocatableLicenseInfo {
* @type {string}
* @memberof AllocatableLicenseInfo
*/
'expiryDate': string;
'expiryDate'?: string;
}
/**
*
@ -2561,6 +2561,44 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat
options: localVarRequestOptions,
};
},
/**
*
* @summary
* @param {number} id Worktypeの内部ID
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
deleteWorktype: async (id: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('deleteWorktype', 'id', id)
const localVarPath = `/accounts/worktypes/{id}/delete`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @summary
@ -3340,6 +3378,17 @@ export const AccountsApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.deleteAccountAndData(deleteAccountRequest, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @summary
* @param {number} id Worktypeの内部ID
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async deleteWorktype(id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.deleteWorktype(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @summary
@ -3616,6 +3665,16 @@ export const AccountsApiFactory = function (configuration?: Configuration, baseP
deleteAccountAndData(deleteAccountRequest: DeleteAccountRequest, options?: any): AxiosPromise<object> {
return localVarFp.deleteAccountAndData(deleteAccountRequest, options).then((request) => request(axios, basePath));
},
/**
*
* @summary
* @param {number} id Worktypeの内部ID
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
deleteWorktype(id: number, options?: any): AxiosPromise<object> {
return localVarFp.deleteWorktype(id, options).then((request) => request(axios, basePath));
},
/**
*
* @summary
@ -3888,6 +3947,18 @@ export class AccountsApi extends BaseAPI {
return AccountsApiFp(this.configuration).deleteAccountAndData(deleteAccountRequest, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary
* @param {number} id Worktypeの内部ID
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AccountsApi
*/
public deleteWorktype(id: number, options?: AxiosRequestConfig) {
return AccountsApiFp(this.configuration).deleteWorktype(id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary

View File

@ -18,6 +18,7 @@ import worktype from "features/workflow/worktype/worktypeSlice";
import account from "features/account/accountSlice";
import template from "features/workflow/template/templateSlice";
import workflow from "features/workflow/workflowSlice";
import terms from "features/terms/termsSlice";
export const store = configureStore({
reducer: {
@ -40,6 +41,7 @@ export const store = configureStore({
account,
template,
workflow,
terms,
},
});

View File

@ -32,6 +32,7 @@ export const errorCodes = [
"E010206", // DBのTierが想定外の値エラー
"E010207", // ユーザーのRole変更不可エラー
"E010208", // ユーザーの暗号化パスワード不足エラー
"E010209", // ユーザーの同意済み利用規約バージョンが最新でないエラー
"E010301", // メールアドレス登録済みエラー
"E010302", // authorId重複エラー
"E010401", // PONumber重複エラー
@ -55,6 +56,7 @@ export const errorCodes = [
"E011001", // ワークタイプ重複エラー
"E011002", // ワークタイプ登録上限超過エラー
"E011003", // ワークタイプ不在エラー
"E011004", // ワークタイプ使用中エラー
"E013001", // ワークフローのAuthorIDとWorktypeIDのペア重複エラー
"E013002", // ワークフロー不在エラー
] as const;

View File

@ -81,3 +81,21 @@ const isErrorResponse = (error: unknown): error is ErrorResponse => {
const isErrorCode = (errorCode: string): errorCode is ErrorCodeType =>
errorCodes.includes(errorCode as ErrorCodeType);
export const isErrorObject = (
data: unknown
): data is { error: ErrorObject } => {
if (
data &&
typeof data === "object" &&
"error" in data &&
typeof (data as { error: ErrorObject }).error === "object" &&
typeof (data as { error: ErrorObject }).error.message === "string" &&
typeof (data as { error: ErrorObject }).error.code === "string" &&
(typeof (data as { error: ErrorObject }).error.statusCode === "number" ||
(data as { error: ErrorObject }).error.statusCode === undefined)
) {
return true;
}
return false;
};

View File

@ -5,7 +5,7 @@ export const msalConfig: Configuration = {
clientId: import.meta.env.VITE_B2C_CLIENTID,
authority: import.meta.env.VITE_B2C_AUTHORITY,
knownAuthorities: [import.meta.env.VITE_B2C_KNOWNAUTHORITIES],
redirectUri: `${globalThis.location.origin}/login`,
redirectUri: `${globalThis.location.origin}/auth`,
navigateToLoginRequestUrl: false,
},
cache: {

View File

@ -62,3 +62,16 @@ export const isIdToken = (arg: any): arg is IdToken => {
return true;
};
export const getIdTokenFromLocalStorage = (
localStorageKeyforIdToken: string
): string | null => {
const idTokenString = localStorage.getItem(localStorageKeyforIdToken);
if (idTokenString) {
const idTokenObject = JSON.parse(idTokenString);
if (isIdToken(idTokenObject)) {
return idTokenObject.secret;
}
}
return null;
};

View File

@ -1,17 +1,26 @@
import { createSlice } from "@reduxjs/toolkit";
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { LoginState } from "./state";
import { loginAsync } from "./operations";
const initialState: LoginState = {
apps: {
LoginApiCallStatus: "none",
localStorageKeyforIdToken: null,
},
};
export const loginSlice = createSlice({
name: "login",
initialState,
reducers: {},
reducers: {
changeLocalStorageKeyforIdToken: (
state,
action: PayloadAction<{ localStorageKeyforIdToken: string }>
) => {
const { localStorageKeyforIdToken } = action.payload;
state.apps.localStorageKeyforIdToken = localStorageKeyforIdToken;
},
},
extraReducers: (builder) => {
builder.addCase(loginAsync.pending, (state) => {
state.apps.LoginApiCallStatus = "pending";
@ -25,4 +34,5 @@ export const loginSlice = createSlice({
},
});
export const { changeLocalStorageKeyforIdToken } = loginSlice.actions;
export default loginSlice.reducer;

View File

@ -3,6 +3,7 @@ import type { RootState } from "app/store";
import { setToken } from "features/auth/authSlice";
import { AuthApi } from "../../api/api";
import { Configuration } from "../../api/configuration";
import { ErrorObject, createErrorObject } from "../../common/errors";
export const loginAsync = createAsyncThunk<
{
@ -14,7 +15,7 @@ export const loginAsync = createAsyncThunk<
{
// rejectした時の返却値の型
rejectValue: {
/* Empty Object */
error: ErrorObject;
};
}
>("login/loginAsync", async (args, thunkApi) => {
@ -41,6 +42,8 @@ export const loginAsync = createAsyncThunk<
return {};
} catch (e) {
return thunkApi.rejectWithValue({});
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -4,3 +4,7 @@ export const selectLoginApiCallStatus = (
state: RootState
): "fulfilled" | "rejected" | "none" | "pending" =>
state.login.apps.LoginApiCallStatus;
export const selectLocalStorageKeyforIdToken = (
state: RootState
): string | null => state.login.apps.localStorageKeyforIdToken;

View File

@ -4,4 +4,5 @@ export interface LoginState {
export interface Apps {
LoginApiCallStatus: "fulfilled" | "rejected" | "none" | "pending";
localStorageKeyforIdToken: string | null;
}

View File

@ -3,10 +3,12 @@ import type { RootState } from "app/store";
import { ErrorObject, createErrorObject } from "common/errors";
import { getTranslationID } from "translation";
import { closeSnackbar, openSnackbar } from "features/ui/uiSlice";
import { TERMS_DOCUMENT_TYPE } from "features/terms/constants";
import {
AccountsApi,
CreateAccountRequest,
GetDealersResponse,
TermsApi,
} from "../../api/api";
import { Configuration } from "../../api/configuration";
@ -93,3 +95,42 @@ export const getDealersAsync = createAsyncThunk<
return thunkApi.rejectWithValue({ error });
}
});
export const getLatestEulaVersionAsync = createAsyncThunk<
string,
void,
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("login/getLatestEulaVersionAsync", async (args, thunkApi) => {
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const config = new Configuration(configuration);
const termsApi = new TermsApi(config);
try {
const termsInfo = await termsApi.getTermsInfo();
const latestEulaVersion = termsInfo.data.termsInfo.find(
(val) => val.documentType === TERMS_DOCUMENT_TYPE.EULA
);
if (!latestEulaVersion) {
throw new Error("EULA info is not found");
}
return latestEulaVersion.version;
} catch (e) {
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -72,3 +72,6 @@ export const selectSelectedDealer = (state: RootState) => {
const { dealer } = state.signup.apps;
return dealers.find((x: Dealer) => x.id === dealer);
};
export const selectEulaVersion = (state: RootState) =>
state.signup.domain.eulaVersion;

View File

@ -1,6 +1,10 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { SignupState } from "./state";
import { getDealersAsync, signupAsync } from "./operations";
import {
getDealersAsync,
getLatestEulaVersionAsync,
signupAsync,
} from "./operations";
const initialState: SignupState = {
apps: {
@ -15,6 +19,7 @@ const initialState: SignupState = {
},
domain: {
dealers: [],
eulaVersion: "",
},
};
@ -74,6 +79,15 @@ export const signupSlice = createSlice({
builder.addCase(getDealersAsync.rejected, () => {
//
});
builder.addCase(getLatestEulaVersionAsync.pending, () => {
//
});
builder.addCase(getLatestEulaVersionAsync.fulfilled, (state, action) => {
state.domain.eulaVersion = action.payload;
});
builder.addCase(getLatestEulaVersionAsync.rejected, () => {
//
});
},
});
export const {

View File

@ -18,4 +18,5 @@ export interface Apps {
export interface Domain {
dealers: Dealer[];
eulaVersion: string;
}

View File

@ -0,0 +1,8 @@
/**
*
* @const {string[]}
*/
export const TERMS_DOCUMENT_TYPE = {
DPA: "DPA",
EULA: "EULA",
} as const;

View File

@ -0,0 +1,4 @@
export * from "./termsSlice";
export * from "./state";
export * from "./operations";
export * from "./selectors";

View File

@ -0,0 +1,158 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import type { RootState } from "app/store";
import { ErrorObject, createErrorObject } from "common/errors";
import { getTranslationID } from "translation";
import { openSnackbar } from "features/ui/uiSlice";
import { getIdTokenFromLocalStorage } from "common/token";
import { TIERS } from "components/auth/constants";
import {
UsersApi,
GetAccountInfoMinimalAccessResponse,
AccountsApi,
TermsApi,
GetTermsInfoResponse,
} from "../../api/api";
import { Configuration } from "../../api/configuration";
export const getAccountInfoMinimalAccessAsync = createAsyncThunk<
GetAccountInfoMinimalAccessResponse,
{
localStorageKeyforIdToken: string;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("accept/getAccountInfoMinimalAccessAsync", async (args, thunkApi) => {
const { localStorageKeyforIdToken } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration, accessToken } = state.auth;
const config = new Configuration(configuration);
const accountApi = new AccountsApi(config);
try {
// IDトークンの取得
const idToken = getIdTokenFromLocalStorage(localStorageKeyforIdToken);
// IDトークンが取得できない場合エラーとする
if (!idToken) {
throw new Error("Unable to retrieve the ID token.");
}
const res = await accountApi.getAccountInfoMinimalAccess(
{ idToken },
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
return res.data;
} catch (e) {
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const getTermsInfoAsync = createAsyncThunk<
GetTermsInfoResponse,
void,
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("accept/getTermsInfoAsync", async (_args, thunkApi) => {
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration, accessToken } = state.auth;
const config = new Configuration(configuration);
const termsApi = new TermsApi(config);
try {
const termsInfo = await termsApi.getTermsInfo({
headers: { authorization: `Bearer ${accessToken}` },
});
return termsInfo.data;
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const updateAcceptedVersionAsync = createAsyncThunk<
{
/* Empty Object */
},
{
tier: number;
localStorageKeyforIdToken: string;
updateAccceptVersions: {
acceptedVerDPA: string;
acceptedVerEULA: string;
};
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("accept/UpdateAcceptedVersionAsync", async (args, thunkApi) => {
const { tier, localStorageKeyforIdToken, updateAccceptVersions } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration, accessToken } = state.auth;
const config = new Configuration(configuration);
const userApi = new UsersApi(config);
try {
// IDトークンの取得
const idToken = getIdTokenFromLocalStorage(localStorageKeyforIdToken);
// IDトークンが取得できない場合エラーとする
if (!idToken) {
throw new Error("Unable to retrieve the ID token.");
}
await userApi.updateAcceptedVersion(
{
idToken,
acceptedEULAVersion: updateAccceptVersions.acceptedVerEULA,
acceptedDPAVersion: !(TIERS.TIER5 === tier.toString())
? updateAccceptVersions.acceptedVerDPA
: undefined,
},
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
return {};
} catch (e) {
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -0,0 +1,20 @@
import { RootState } from "app/store";
import { TERMS_DOCUMENT_TYPE } from "features/terms/constants";
export const selectTermVersions = (state: RootState) => {
const { termsInfo } = state.terms.domain;
const acceptedVerDPA =
termsInfo.find(
(termInfo) => termInfo.documentType === TERMS_DOCUMENT_TYPE.DPA
)?.version || "";
const acceptedVerEULA =
termsInfo.find(
(termInfo) => termInfo.documentType === TERMS_DOCUMENT_TYPE.EULA
)?.version || "";
return { acceptedVerDPA, acceptedVerEULA };
};
export const selectTier = (state: RootState) => state.terms.domain.tier;

View File

@ -0,0 +1,15 @@
import { TermInfo } from "../../api/api";
export interface AcceptState {
domain: Domain;
apps: Apps;
}
export interface Domain {
tier: number;
termsInfo: TermInfo[];
}
export interface Apps {
isLoading: boolean;
}

View File

@ -0,0 +1,64 @@
import { createSlice } from "@reduxjs/toolkit";
import { AcceptState } from "./state";
import {
getAccountInfoMinimalAccessAsync,
getTermsInfoAsync,
updateAcceptedVersionAsync,
} from "./operations";
const initialState: AcceptState = {
domain: {
tier: 0,
termsInfo: [
{
documentType: "",
version: "",
},
],
},
apps: {
isLoading: false,
},
};
export const termsSlice = createSlice({
name: "terms",
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(getAccountInfoMinimalAccessAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(
getAccountInfoMinimalAccessAsync.fulfilled,
(state, actions) => {
state.apps.isLoading = false;
state.domain.tier = actions.payload.tier;
}
);
builder.addCase(getAccountInfoMinimalAccessAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(getTermsInfoAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(getTermsInfoAsync.fulfilled, (state, actions) => {
state.apps.isLoading = false;
state.domain.termsInfo = actions.payload.termsInfo;
});
builder.addCase(getTermsInfoAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(updateAcceptedVersionAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(updateAcceptedVersionAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(updateAcceptedVersionAsync.rejected, (state) => {
state.apps.isLoading = false;
});
},
});
export default termsSlice.reducer;

View File

@ -87,7 +87,7 @@ export const userSlice = createSlice({
action: PayloadAction<{ authorId: string | undefined }>
) => {
const { authorId } = action.payload;
state.apps.addUser.authorId = authorId;
state.apps.addUser.authorId = authorId?.toUpperCase();
},
changeAutoRenew: (state, action: PayloadAction<{ autoRenew: boolean }>) => {
const { autoRenew } = action.payload;
@ -144,7 +144,7 @@ export const userSlice = createSlice({
state.apps.updateUser.name = user.name;
state.apps.updateUser.email = user.email;
state.apps.updateUser.role = user.role as RoleType;
state.apps.updateUser.authorId = user.authorId;
state.apps.updateUser.authorId = user.authorId?.toUpperCase();
state.apps.updateUser.encryption = user.encryption;
state.apps.updateUser.encryptionPassword = undefined;
state.apps.updateUser.prompt = user.prompt;
@ -156,7 +156,7 @@ export const userSlice = createSlice({
state.apps.selectedUser.name = user.name;
state.apps.selectedUser.email = user.email;
state.apps.selectedUser.role = user.role as RoleType;
state.apps.selectedUser.authorId = user.authorId;
state.apps.selectedUser.authorId = user.authorId?.toUpperCase();
state.apps.selectedUser.encryption = user.encryption;
state.apps.selectedUser.encryptionPassword = undefined;
state.apps.selectedUser.prompt = user.prompt;
@ -175,7 +175,7 @@ export const userSlice = createSlice({
action: PayloadAction<{ authorId: string }>
) => {
const { authorId } = action.payload;
state.apps.updateUser.authorId = authorId;
state.apps.updateUser.authorId = authorId.toUpperCase();
},
changeUpdateEncryption: (
state,
@ -243,7 +243,8 @@ export const userSlice = createSlice({
state.apps.licenseAllocateUser.id = selectedUser.id;
state.apps.licenseAllocateUser.name = selectedUser.name;
state.apps.licenseAllocateUser.email = selectedUser.email;
state.apps.licenseAllocateUser.authorId = selectedUser.authorId;
state.apps.licenseAllocateUser.authorId =
selectedUser.authorId.toUpperCase();
state.apps.licenseAllocateUser.licenseStatus = selectedUser.licenseStatus;
state.apps.licenseAllocateUser.expiration = selectedUser.expiration;
state.apps.licenseAllocateUser.remaining = selectedUser.remaining;

View File

@ -342,3 +342,75 @@ export const updateActiveWorktypeAsync = createAsyncThunk<
return thunkApi.rejectWithValue({ error });
}
});
export const deleteWorktypeAsync = createAsyncThunk<
{
/* Empty Object */
},
{ worktypeId: number },
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("workflow/deleteWorktypeAsync", async (args, thunkApi) => {
const { worktypeId } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration, accessToken } = state.auth;
const config = new Configuration(configuration);
const accountsApi = new AccountsApi(config);
try {
await accountsApi.deleteWorktype(worktypeId, {
headers: { authorization: `Bearer ${accessToken}` },
});
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
if (error.statusCode === 400) {
if (error.code === "E011003") {
// ワークタイプが削除済みの場合は成功扱いとする
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
}
if (error.code === "E011004") {
// ワークタイプがワークフローで使用中の場合は削除できない
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID(
"worktypeIdSetting.message.worktypeInUseError"
),
})
);
return {};
}
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -0,0 +1,78 @@
import { useMsal } from "@azure/msal-react";
import { AuthError } from "@azure/msal-browser";
import { AppDispatch } from "app/store";
import Footer from "components/footer";
import Header from "components/header";
import {
selectLoginApiCallStatus,
changeLocalStorageKeyforIdToken,
} from "features/login";
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
const AuthPage: React.FC = (): JSX.Element => {
const { instance } = useMsal();
const dispatch: AppDispatch = useDispatch();
const navigate = useNavigate();
const status = useSelector(selectLoginApiCallStatus);
// TODO 将来的にトークンの取得処理をoperations.ts側に移動させたい。useEffect内で非同期処理を行いたくない。
useEffect(() => {
if (status !== "none") {
// ログイン処理で、何回か本画面が描画される契機があるが、認証処理は一度だけ実施すればよいため認証処理実行済みであれば何もしない
return;
}
(async () => {
try {
const loginResult = await instance.handleRedirectPromise();
// eslint-disable-next-line
console.log({ loginResult }); // TODO:loading画面から遷移できない事象の調査用ログ。事象解消後削除eslint-disable含めてする。
if (loginResult && loginResult.account) {
const { homeAccountId, idTokenClaims } = loginResult.account;
if (idTokenClaims && idTokenClaims.aud) {
const localStorageKeyforIdToken = `${homeAccountId}-${
import.meta.env.VITE_B2C_KNOWNAUTHORITIES
}-idtoken-${idTokenClaims.aud}----`;
// AADB2Cログイン画面以外から本画面に遷移した場合用にIDトークン取得用キーをstateに保存
dispatch(
changeLocalStorageKeyforIdToken({
localStorageKeyforIdToken,
})
);
// トークン取得と設定を行う
navigate("/login");
}
}
} catch (e) {
// eslint-disable-next-line
console.log({ e }); // TODO:loading画面から遷移できない事象の調査用ログ。事象解消後削除eslint-disable含めてする。
// AAD B2Cの多要素認証画面やパスワードリセット画面で「cancel」をクリックすると、handleRedirectPromise()にてエラーが発生するため、
// それをハンドリングして適切な画面遷移処理を行う。
if (e instanceof AuthError) {
// エラーコードはerrorMessageの中の一部として埋め込まれており完全一致で取得するのは筋が悪いため、部分一致で取得する。
// TODO 他にもAADB2Cのエラーコードを使用する箇所が出てきた場合、定数化すること
if (e.errorMessage.startsWith("AADB2C90091")) {
navigate("/");
}
}
}
})();
}, [instance, navigate, status, dispatch]);
return (
<>
<Header />
<h3>loading ...</h3>
<Footer />
</>
);
};
export default AuthPage;

View File

@ -1,30 +1,39 @@
import { useMsal } from "@azure/msal-react";
import { AuthError } from "@azure/msal-browser";
import { AppDispatch } from "app/store";
import { isIdToken } from "common/token";
import { loadAccessToken, loadRefreshToken } from "features/auth/utils";
import { loginAsync, selectLocalStorageKeyforIdToken } from "features/login";
import React, { useCallback, useEffect } from "react";
import Footer from "components/footer";
import Header from "components/header";
import { loadAccessToken, loadRefreshToken } from "features/auth/utils";
import { loginAsync, selectLoginApiCallStatus } from "features/login";
import React, { useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import { isErrorObject } from "common/errors";
const LoginPage: React.FC = (): JSX.Element => {
const { instance } = useMsal();
const dispatch: AppDispatch = useDispatch();
const navigate = useNavigate();
const [, i18n] = useTranslation();
const status = useSelector(selectLoginApiCallStatus);
const localStorageKeyforIdToken = useSelector(
selectLocalStorageKeyforIdToken
);
const login = useCallback(
const tokenSet = useCallback(
async (idToken: string) => {
// ログイン処理呼び出し
const { meta } = await dispatch(loginAsync({ idToken }));
const { meta, payload } = await dispatch(loginAsync({ idToken }));
// ログイン失敗した場合、B2Cをログアウトしてからエラーページに遷移する
if (meta.requestStatus === "rejected") {
if (isErrorObject(payload)) {
// 未同意の規約がある場合は利用規約同意画面に遷移する
if (payload.error.code === "E010209") {
navigate("/terms");
return;
}
}
instance.logoutRedirect({
postLogoutRedirectUri: "/AuthError",
});
@ -48,53 +57,26 @@ const LoginPage: React.FC = (): JSX.Element => {
[dispatch, i18n.language, instance, navigate]
);
// TODO 将来的にトークンの取得処理をoperations.ts側に移動させたい。useEffect内で非同期処理を行いたくない。
useEffect(() => {
if (status !== "none") {
// ログイン処理で、何回か本画面が描画される契機があるが、認証処理は一度だけ実施すればよいため認証処理実行済みであれば何もしない
// AADB2Cのログイン画面とLoginPageを経由していない場合はトップページに遷移する
if (!localStorageKeyforIdToken) {
navigate("/");
return;
}
(async () => {
try {
const loginResult = await instance.handleRedirectPromise();
// eslint-disable-next-line
console.log({ loginResult }); // TODO:loading画面から遷移できない事象の調査用ログ。事象解消後削除eslint-disable含めてする。
if (loginResult && loginResult.account) {
const { homeAccountId, idTokenClaims } = loginResult.account;
if (idTokenClaims && idTokenClaims.aud) {
// IDトークンの取得
const idTokenString = localStorage.getItem(
`${homeAccountId}-${
import.meta.env.VITE_B2C_KNOWNAUTHORITIES
}-idtoken-${idTokenClaims.aud}----`
);
const idTokenString = localStorage.getItem(localStorageKeyforIdToken);
if (idTokenString) {
const idTokenObject = JSON.parse(idTokenString);
if (isIdToken(idTokenObject)) {
await login(idTokenObject.secret);
}
}
}
}
} catch (e) {
// eslint-disable-next-line
console.log({ e }); // TODO:loading画面から遷移できない事象の調査用ログ。事象解消後削除eslint-disable含めてする。
// AAD B2Cの多要素認証画面やパスワードリセット画面で「cancel」をクリックすると、handleRedirectPromise()にてエラーが発生するため、
// それをハンドリングして適切な画面遷移処理を行う。
if (e instanceof AuthError) {
// エラーコードはerrorMessageの中の一部として埋め込まれており完全一致で取得するのは筋が悪いため、部分一致で取得する。
// TODO 他にもAADB2Cのエラーコードを使用する箇所が出てきた場合、定数化すること
if (e.errorMessage.startsWith("AADB2C90091")) {
navigate("/");
}
await tokenSet(idTokenObject.secret);
}
}
})();
}, [instance, login, navigate, status]);
// 画面描画後のみ実行するため引数を設定しない
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<>

View File

@ -14,6 +14,7 @@ import {
selectEmail,
selectPassword,
selectSelectedDealer,
selectEulaVersion,
} from "../../features/signup/selectors";
import { signupAsync } from "../../features/signup/operations";
@ -27,6 +28,7 @@ const SignupConfirm: React.FC = (): JSX.Element => {
const adminMail = useSelector(selectEmail);
const adminPassword = useSelector(selectPassword);
const dealer = useSelector(selectSelectedDealer);
const acceptedEulaVersion = useSelector(selectEulaVersion);
const onSubmit = useCallback(() => {
dispatch(
@ -37,7 +39,7 @@ const SignupConfirm: React.FC = (): JSX.Element => {
adminName,
adminMail,
adminPassword,
acceptedEulaVersion: "",
acceptedEulaVersion,
acceptedDpaVersion: "",
token: "",
})
@ -50,6 +52,7 @@ const SignupConfirm: React.FC = (): JSX.Element => {
adminName,
adminMail,
adminPassword,
acceptedEulaVersion,
]);
return (

View File

@ -24,7 +24,10 @@ import { useDispatch, useSelector } from "react-redux";
import { useLocation, useNavigate } from "react-router-dom";
import { getTranslationID } from "translation";
import styles from "styles/app.module.scss";
import { getDealersAsync } from "features/signup/operations";
import {
getDealersAsync,
getLatestEulaVersionAsync,
} from "features/signup/operations";
import { LANGUAGE_LIST } from "features/top/constants";
import { openSnackbar } from "features/ui";
import { COUNTRY_LIST } from "./constants";
@ -84,6 +87,7 @@ const SignupInput: React.FC = (): JSX.Element => {
// 入力画面の初期化時の処理
useEffect(() => {
dispatch(getDealersAsync());
dispatch(getLatestEulaVersionAsync());
}, [dispatch]);
useEffect(() => {
@ -264,10 +268,9 @@ const SignupInput: React.FC = (): JSX.Element => {
/>
{isPushCreateButton && hasErrorEmptyAdminName && (
<span className={styles.formError}>
{" "}
{t(
{` ${t(
getTranslationID("signupPage.message.inputEmptyError")
)}
)}`}
</span>
)}
</dd>
@ -369,8 +372,9 @@ const SignupInput: React.FC = (): JSX.Element => {
}}
>
{t(getTranslationID("signupPage.label.termsLink"))}
</a>{" "}
{t(getTranslationID("signupPage.label.termsLinkFor"))} <br />
</a>
{` ${t(getTranslationID("signupPage.label.termsLinkFor"))} `}
<br />
<label htmlFor="check-box">
<input
id="check-box"

View File

@ -0,0 +1,188 @@
/* eslint-disable jsx-a11y/label-has-associated-control */
import { useTranslation } from "react-i18next";
import { getTranslationID } from "translation";
import Header from "components/header";
import { AppDispatch } from "app/store";
import { useDispatch, useSelector } from "react-redux";
import styles from "styles/app.module.scss";
import { TIERS } from "components/auth/constants";
import Footer from "components/footer";
import { useCallback, useEffect, useState } from "react";
import {
getAccountInfoMinimalAccessAsync,
getTermsInfoAsync,
updateAcceptedVersionAsync,
selectTier,
selectTermVersions,
} from "features//terms";
import { selectLocalStorageKeyforIdToken } from "features/login";
import { useNavigate } from "react-router-dom";
const TermsPage: React.FC = (): JSX.Element => {
const [t] = useTranslation();
const dispatch: AppDispatch = useDispatch();
const navigate = useNavigate();
const updateAccceptVersions = useSelector(selectTermVersions);
const localStorageKeyforIdToken = useSelector(
selectLocalStorageKeyforIdToken
);
const tier = useSelector(selectTier);
const [isCheckedEula, setIsCheckedEula] = useState(false);
const [isCheckedDpa, setIsCheckedDpa] = useState(false);
const [isClickedEulaLink, setIsClickedEulaLink] = useState(false);
const [isClickedDpaLink, setIsClickedDpaLink] = useState(false);
// 画面起動時
useEffect(() => {
dispatch(getTermsInfoAsync());
if (localStorageKeyforIdToken) {
dispatch(getAccountInfoMinimalAccessAsync({ localStorageKeyforIdToken }));
} else {
// ログイン画面を経由していないため、トップページに遷移する
navigate("/");
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// ユーザーが第5階層であるかどうかを判定する(アクセストークンから自分の階層を取得できないので自前で作成)
const isTier5 = () => TIERS.TIER5.includes(tier.toString());
// ボタン押下可否判定ロジック
const canClickButton = () => {
if (isTier5()) {
return isCheckedEula;
}
return isCheckedEula && isCheckedDpa;
};
// ボタン押下時処理
const onAcceptTermsOfUse = useCallback(async () => {
if (
localStorageKeyforIdToken &&
updateAccceptVersions.acceptedVerDPA !== "" &&
updateAccceptVersions.acceptedVerEULA !== ""
) {
const { meta } = await dispatch(
updateAcceptedVersionAsync({
tier,
localStorageKeyforIdToken,
updateAccceptVersions,
})
);
// 同意済バージョンが更新できたら、再度トークン生成を行う
if (meta.requestStatus === "fulfilled") {
navigate("/login");
}
}
}, [
navigate,
localStorageKeyforIdToken,
updateAccceptVersions,
tier,
dispatch,
]);
return (
<div className={styles.wrap}>
<Header />
<main className={styles.main}>
<div className={styles.mainSmall}>
<div>
<h1 className={`${styles.marginBtm1} ${styles.alignCenter}`}>
{t(getTranslationID("termsPage.label.title"))}
</h1>
</div>
<section className={styles.form}>
<form action="" name="" method="">
<dl className={`${styles.formList} ${styles.hasbg}`}>
<dt className={styles.formTitle} />
<dd className={`${styles.full} ${styles.alignCenter}`}>
<p>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<a
href="/" /* TODO Eula用の利用規約リンクが決定したら設定を行う */
target="_blank"
className={styles.linkTx}
onClick={() => setIsClickedEulaLink(true)}
>
{t(getTranslationID("termsPage.label.linkOfEula"))}
</a>
{` ${t(getTranslationID("termsPage.label.forOdds"))}`}
</p>
<p>
<label>
<input
type="checkbox"
checked={isCheckedEula}
className={styles.formCheck}
value=""
onChange={(e) => setIsCheckedEula(e.target.checked)}
disabled={!isClickedEulaLink}
/>
{t(
getTranslationID("termsPage.label.checkBoxForConsent")
)}
</label>
</p>
</dd>
{/* 第五階層以外の場合はEulaのリンクをあわせて表示する */}
{!isTier5() && (
<dd className={`${styles.full} ${styles.alignCenter}`}>
<p>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<a
href="/" /* TODO Dpa用の利用規約リンクが決定したら設定を行う */
target="_blank"
className={styles.linkTx}
onClick={() => setIsClickedDpaLink(true)}
>
{t(getTranslationID("termsPage.label.linkOfDpa"))}
</a>
{` ${t(getTranslationID("termsPage.label.forOdds"))}`}
</p>
<p>
<label>
<input
type="checkbox"
checked={isCheckedDpa}
className={styles.formCheck}
value=""
onChange={(e) => setIsCheckedDpa(e.target.checked)}
disabled={!isClickedDpaLink}
/>
{t(
getTranslationID("termsPage.label.checkBoxForConsent")
)}
</label>
</p>
</dd>
)}
<dd className={`${styles.full} ${styles.alignCenter}`}>
<p>
<input
type="button"
name="submit"
value={t(getTranslationID("termsPage.label.button"))}
className={`${styles.formSubmit} ${styles.marginBtm1} ${
canClickButton() ? styles.isActive : ""
}`}
onClick={onAcceptTermsOfUse}
/>
</p>
</dd>
</dl>
</form>
</section>
</div>
</main>
<Footer />
</div>
);
};
export default TermsPage;

View File

@ -200,7 +200,11 @@ export const UserAddPopup: React.FC<UserAddPopupProps> = (props) => {
className={styles.formInput}
value={addUser.authorId ?? undefined}
onChange={(e) => {
dispatch(changeAuthorId({ authorId: e.target.value }));
dispatch(
changeAuthorId({
authorId: e.target.value.toUpperCase(),
})
);
}}
/>
{isPushCreateButton && hasErrorEmptyAuthorId && (

View File

@ -184,7 +184,9 @@ export const UserUpdatePopup: React.FC<UserUpdatePopupProps> = (props) => {
className={styles.formInput}
onChange={(e) => {
dispatch(
changeUpdateAuthorId({ authorId: e.target.value })
changeUpdateAuthorId({
authorId: e.target.value.toUpperCase(),
})
);
}}
/>

View File

@ -1,7 +1,7 @@
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
import Footer from "components/footer";
import Header from "components/header";
import React, { useEffect, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import { getTranslationID } from "translation";
import styles from "styles/app.module.scss";
import undo from "assets/images/undo.svg";
@ -18,6 +18,7 @@ import {
selectIsLoading,
selectWorktypes,
selectActiveWorktypeId,
deleteWorktypeAsync,
} from "features/workflow/worktype";
import { AppDispatch } from "app/store";
import { AddWorktypeIdPopup } from "./addWorktypeIdPopup";
@ -86,6 +87,23 @@ const WorktypeIdSettingPage: React.FC = (): JSX.Element => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedActiveWorktypeId]);
// 削除ボタン押下時の処理
const onDeleteWoktype = useCallback(
async (worktypeId: number) => {
if (
/* eslint-disable-next-line no-alert */
!window.confirm(t(getTranslationID("common.message.dialogConfirm")))
) {
return;
}
const { meta } = await dispatch(deleteWorktypeAsync({ worktypeId }));
if (meta.requestStatus === "fulfilled") {
dispatch(listWorktypesAsync());
}
},
[dispatch, t]
);
return (
<>
<AddWorktypeIdPopup
@ -253,9 +271,10 @@ const WorktypeIdSettingPage: React.FC = (): JSX.Element => {
</a>
</li>
<li>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<a
className={`${styles.menuLink} ${styles.isActive}`}
// onClick={}
onClick={() => onDeleteWoktype(worktype.id)}
>
{t(getTranslationID("common.label.delete"))}
</a>

View File

@ -2266,8 +2266,7 @@ tr.isSelected .menuInTable li a.isDisable {
}
.formChange ul.chooseMember li input + label:hover,
.formChange ul.holdMember li input + label:hover {
background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left
center;
background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left center;
background-size: 1.3rem;
}
.formChange ul.chooseMember li input:checked + label,
@ -2278,8 +2277,8 @@ tr.isSelected .menuInTable li a.isDisable {
}
.formChange ul.chooseMember li input:checked + label:hover,
.formChange ul.holdMember li input:checked + label:hover {
background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat
right center;
background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat right
center;
background-size: 1.3rem;
}
.formChange > p {
@ -2432,8 +2431,7 @@ tr.isSelected .menuInTable li a.isDisable {
}
.formChange ul.chooseMember li input + label:hover,
.formChange ul.holdMember li input + label:hover {
background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left
center;
background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left center;
background-size: 1.3rem;
}
.formChange ul.chooseMember li input:checked + label,
@ -2444,8 +2442,8 @@ tr.isSelected .menuInTable li a.isDisable {
}
.formChange ul.chooseMember li input:checked + label:hover,
.formChange ul.holdMember li input:checked + label:hover {
background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat
right center;
background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat right
center;
background-size: 1.3rem;
}
.formChange > p {

View File

@ -429,7 +429,8 @@
"optionItemInvalidError": "(de)Default valueがDefaultに設定されている場合、Initial valueは入力が必須です。",
"optionItemSaveFailedError": "(de)オプションアイテムの保存に失敗しました。画面を更新し、再度実行してください",
"optionItemIncorrectError": "(de)入力されたItem labelまたはInitial valueがルールを満たしていません。下記のルールを満たす値を入力してください",
"updateActiveWorktypeFailedError": "(de)Active WorktypeIDの保存に失敗しました。画面を更新し、再度実行してください"
"updateActiveWorktypeFailedError": "(de)Active WorktypeIDの保存に失敗しました。画面を更新し、再度実行してください",
"worktypeInUseError": "(de)このWorktype IDはルーティングルールで使用されているため削除できません。"
}
},
"templateFilePage": {
@ -499,13 +500,14 @@
"backToTopPageLink": "(de)Back to TOP Page"
}
},
"AgreeToUsePage": {
"termsPage": {
"label": {
"title": "(de)Terms of Use has updated. Please confirm again.",
"linkOfEula": "(de)Click here to read the terms of use.",
"linkOfDpa": "(de)Click here to read the terms of use.",
"checkBoxForConsent": "(de)Yes, I agree to the terms of use.",
"forOdds": "(de)for ODDS."
"forOdds": "(de)for ODDS.",
"button": "(de)Continue"
}
}
}

View File

@ -429,7 +429,8 @@
"optionItemInvalidError": "Default valueがDefaultに設定されている場合、Initial valueは入力が必須です。",
"optionItemSaveFailedError": "オプションアイテムの保存に失敗しました。画面を更新し、再度実行してください",
"optionItemIncorrectError": "入力されたItem labelまたはInitial valueがルールを満たしていません。下記のルールを満たす値を入力してください",
"updateActiveWorktypeFailedError": "Active WorktypeIDの保存に失敗しました。画面を更新し、再度実行してください"
"updateActiveWorktypeFailedError": "Active WorktypeIDの保存に失敗しました。画面を更新し、再度実行してください",
"worktypeInUseError": "このWorktype IDはルーティングルールで使用されているため削除できません。"
}
},
"templateFilePage": {
@ -499,13 +500,14 @@
"backToTopPageLink": "Back to TOP Page"
}
},
"AgreeToUsePage": {
"termsPage": {
"label": {
"title": "Terms of Use has updated. Please confirm again.",
"linkOfEula": "Click here to read the terms of use.",
"linkOfDpa": "Click here to read the terms of use.",
"checkBoxForConsent": "Yes, I agree to the terms of use.",
"forOdds": "for ODDS."
"forOdds": "for ODDS.",
"button": "Continue"
}
}
}

View File

@ -429,7 +429,8 @@
"optionItemInvalidError": "(es)Default valueがDefaultに設定されている場合、Initial valueは入力が必須です。",
"optionItemSaveFailedError": "(es)オプションアイテムの保存に失敗しました。画面を更新し、再度実行してください",
"optionItemIncorrectError": "(es)入力されたItem labelまたはInitial valueがルールを満たしていません。下記のルールを満たす値を入力してください",
"updateActiveWorktypeFailedError": "(es)Active WorktypeIDの保存に失敗しました。画面を更新し、再度実行してください"
"updateActiveWorktypeFailedError": "(es)Active WorktypeIDの保存に失敗しました。画面を更新し、再度実行してください",
"worktypeInUseError": "(es)このWorktype IDはルーティングルールで使用されているため削除できません。"
}
},
"templateFilePage": {
@ -499,13 +500,14 @@
"backToTopPageLink": "(es)Back to TOP Page"
}
},
"AgreeToUsePage": {
"termsPage": {
"label": {
"title": "(es)Terms of Use has updated. Please confirm again.",
"linkOfEula": "(es)Click here to read the terms of use.",
"linkOfDpa": "(es)Click here to read the terms of use.",
"checkBoxForConsent": "(es)Yes, I agree to the terms of use.",
"forOdds": "(es)for ODDS."
"forOdds": "(es)for ODDS.",
"button": "(es)Continue"
}
}
}

View File

@ -429,7 +429,8 @@
"optionItemInvalidError": "(fr)Default valueがDefaultに設定されている場合、Initial valueは入力が必須です。",
"optionItemSaveFailedError": "(fr)オプションアイテムの保存に失敗しました。画面を更新し、再度実行してください",
"optionItemIncorrectError": "(fr)入力されたItem labelまたはInitial valueがルールを満たしていません。下記のルールを満たす値を入力してください",
"updateActiveWorktypeFailedError": "(fr)Active WorktypeIDの保存に失敗しました。画面を更新し、再度実行してください"
"updateActiveWorktypeFailedError": "(fr)Active WorktypeIDの保存に失敗しました。画面を更新し、再度実行してください",
"worktypeInUseError": "(fr)このWorktype IDはルーティングルールで使用されているため削除できません。"
}
},
"templateFilePage": {
@ -499,13 +500,14 @@
"backToTopPageLink": "(fr)Back to TOP Page"
}
},
"AgreeToUsePage": {
"termsPage": {
"label": {
"title": "(fr)Terms of Use has updated. Please confirm again.",
"linkOfEula": "(fr)Click here to read the terms of use.",
"linkOfDpa": "(fr)Click here to read the terms of use.",
"checkBoxForConsent": "(fr)Yes, I agree to the terms of use.",
"forOdds": "(fr)for ODDS."
"forOdds": "(fr)for ODDS.",
"button": "(fr)Continue"
}
}
}

View File

@ -0,0 +1,33 @@
-- +migrate Up
ALTER TABLE `checkout_permission` DROP FOREIGN KEY `checkout_permission_fk_task_id`;
ALTER TABLE `tasks` DROP FOREIGN KEY `tasks_fk_account_id`;
ALTER TABLE `template_files` DROP FOREIGN KEY `template_files_fk_account_id`;
ALTER TABLE `option_items` DROP FOREIGN KEY `option_items_fk_worktype_id`;
ALTER TABLE `worktypes` DROP FOREIGN KEY `worktypes_fk_account_id`;
ALTER TABLE `audio_option_items` DROP FOREIGN KEY `audio_option_items_fk_audio_file_id`;
ALTER TABLE `audio_files` DROP FOREIGN KEY `audio_files_fk_account_id`;
ALTER TABLE `user_group_member` DROP FOREIGN KEY `user_group_member_fk_user_group_id`;
ALTER TABLE `user_group` DROP FOREIGN KEY `user_group_fk_account_id`;
ALTER TABLE `license_allocation_history` DROP FOREIGN KEY `license_allocation_history_fk_account_id`;
ALTER TABLE `card_licenses` DROP FOREIGN KEY `card_licenses_fk_license_id`;
ALTER TABLE `licenses` DROP FOREIGN KEY `licenses_fk_account_id`;
ALTER TABLE `license_orders` DROP FOREIGN KEY `license_orders_fk_from_account_id`;
ALTER TABLE `sort_criteria` DROP FOREIGN KEY `sort_criteria_fk_user_id`;
ALTER TABLE `users` DROP FOREIGN KEY `users_fk_account_id`;
-- +migrate Down
ALTER TABLE `checkout_permission` ADD CONSTRAINT `checkout_permission_fk_task_id` FOREIGN KEY (`task_id`) REFERENCES `tasks` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `tasks` ADD CONSTRAINT `tasks_fk_account_id` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `template_files` ADD CONSTRAINT `template_files_fk_account_id` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `option_items` ADD CONSTRAINT `option_items_fk_worktype_id` FOREIGN KEY (`worktype_id`) REFERENCES `worktypes` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `worktypes` ADD CONSTRAINT `worktypes_fk_account_id` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `audio_option_items` ADD CONSTRAINT `audio_option_items_fk_audio_file_id` FOREIGN KEY (`audio_file_id`) REFERENCES `audio_files` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `audio_files` ADD CONSTRAINT `audio_files_fk_account_id` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `user_group_member` ADD CONSTRAINT `user_group_member_fk_user_group_id` FOREIGN KEY (`user_group_id`) REFERENCES `user_group` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `user_group` ADD CONSTRAINT `user_group_fk_account_id` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `license_allocation_history` ADD CONSTRAINT `license_allocation_history_fk_account_id` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `card_licenses` ADD CONSTRAINT `card_licenses_fk_license_id` FOREIGN KEY (`license_id`) REFERENCES `licenses` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `licenses` ADD CONSTRAINT `licenses_fk_account_id` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `license_orders` ADD CONSTRAINT `license_orders_fk_from_account_id` FOREIGN KEY (`from_account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `sort_criteria` ADD CONSTRAINT `sort_criteria_fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `users` ADD CONSTRAINT `users_fk_account_id` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,9 @@
-- +migrate Up
insert into terms(terms.document_type, terms.version) values('EURA', 'V0.1');
insert into terms(terms.document_type, terms.version) values('DPA', 'V0.1');
commit;
-- +migrate Down
delete from terms where terms.document_type = 'EURA' and terms.version = 'V0.1';
delete from terms where terms.document_type = 'DPA' and terms.version = 'V0.1';
commit;

View File

@ -990,6 +990,59 @@
"security": [{ "bearer": [] }]
}
},
"/accounts/worktypes/{id}/delete": {
"post": {
"operationId": "deleteWorktype",
"summary": "",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"description": "Worktypeの内部ID",
"schema": { "type": "number" }
}
],
"responses": {
"200": {
"description": "成功時のレスポンス",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeleteWorktypeResponse"
}
}
}
},
"400": {
"description": "指定WorktypeIDが削除済み / 指定WorktypeIDがWorkflowで使用中",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
},
"401": {
"description": "認証エラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
},
"500": {
"description": "想定外のサーバーエラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
}
},
"tags": ["accounts"],
"security": [{ "bearer": [] }]
}
},
"/accounts/worktypes/{id}/option-items": {
"get": {
"operationId": "getOptionItems",
@ -3706,6 +3759,7 @@
"required": ["worktypeId"]
},
"UpdateWorktypeResponse": { "type": "object", "properties": {} },
"DeleteWorktypeResponse": { "type": "object", "properties": {} },
"GetWorktypeOptionItem": {
"type": "object",
"properties": {
@ -4411,7 +4465,7 @@
"licenseId": { "type": "number" },
"expiryDate": { "format": "date-time", "type": "string" }
},
"required": ["licenseId", "expiryDate"]
"required": ["licenseId"]
},
"GetAllocatableLicensesResponse": {
"type": "object",

View File

@ -57,6 +57,7 @@ export const ErrorCodes = [
'E011001', // ワークタイプ重複エラー
'E011002', // ワークタイプ登録上限超過エラー
'E011003', // ワークタイプ不在エラー
'E011004', // ワークタイプ使用中エラー
'E012001', // テンプレートファイル不在エラー
'E013001', // ワークフローのAuthorIDとWorktypeIDのペア重複エラー
'E013002', // ワークフロー不在エラー

View File

@ -46,6 +46,7 @@ export const errors: Errors = {
E011001: 'This WorkTypeID already used Error',
E011002: 'WorkTypeID create limit exceeded Error',
E011003: 'WorkTypeID not found Error',
E011004: 'WorkTypeID is in use Error',
E012001: 'Template file not found Error',
E013001: 'AuthorId and WorktypeId pair already exists Error',
E013002: 'Workflow not found Error',

View File

@ -92,13 +92,15 @@ export class RoleGuard implements CanActivate {
* @returns true/false
*/
checkRole(role: string): boolean {
const { roles } = this.settings;
const settings = this.settings;
if (!settings || !settings.roles) {
return true;
}
const userRoles = role.split(' ');
// Role毎にAccessTokenの権限チェックを行う
for (let i = 0; i < roles.length; i++) {
const role = roles[i];
for (let i = 0; i < settings.roles.length; i++) {
const role = settings.roles[i];
let isValid = false;
if (Array.isArray(role)) {
isValid = role.every((x) => userRoles.includes(x));
@ -172,9 +174,12 @@ export class RoleGuard implements CanActivate {
* @returns true/false
*/
checkTier(tier: number): boolean {
const { tiers } = this.settings;
const settings = this.settings;
if (!settings || !settings.tiers) {
return true;
}
// 宣言された階層中にパラメータの内容が含まれていればtrue
return tiers.includes(tier as (typeof TIERS)[keyof typeof TIERS]);
return settings.tiers.includes(tier as (typeof TIERS)[keyof typeof TIERS]);
}
}

View File

@ -132,7 +132,7 @@ export const getPrivateKey = (configService: ConfigService): string => {
return (
// 開発環境用に改行コードを置換する
// 本番環境では\\nが含まれないため、置換が行われない想定
configService.get<string>('JWT_PRIVATE_KEY')?.replace(/\\n/g, '\n') ?? ''
configService.getOrThrow<string>('JWT_PRIVATE_KEY').replace(/\\n/g, '\n')
);
};
@ -140,6 +140,6 @@ export const getPublicKey = (configService: ConfigService): string => {
return (
// 開発環境用に改行コードを置換する
// 本番環境では\\nが含まれないため、置換が行われない想定
configService.get<string>('JWT_PUBLIC_KEY')?.replace(/\\n/g, '\n') ?? ''
configService.getOrThrow<string>('JWT_PUBLIC_KEY').replace(/\\n/g, '\n')
);
};

View File

@ -15,10 +15,9 @@ export const makePassword = (): string => {
// autoGeneratedPasswordが以上の条件を満たせばvalidがtrueになる
let valid = false;
let autoGeneratedPassword: string;
let autoGeneratedPassword: string = '';
while (!valid) {
autoGeneratedPassword = '';
// パスワードをランダムに決定
while (autoGeneratedPassword.length < passLength) {
// 上で決定したcharsの中からランダムに1文字ずつ追加

View File

@ -3,6 +3,7 @@ import { DataSource } from 'typeorm';
import { User, UserArchive } from '../../repositories/users/entity/user.entity';
import { Account } from '../../repositories/accounts/entity/account.entity';
import { ADMIN_ROLES, USER_ROLES } from '../../constants';
import { License } from '../../repositories/licenses/entity/license.entity';
type InitialTestDBState = {
tier1Accounts: { account: Account; users: User[] }[];
@ -57,11 +58,11 @@ export const makeHierarchicalAccounts = async (
}
// 第2階層を作成
{
const { account: tier1 } = state.tier1Accounts.slice().shift();
const tier1 = state.tier1Accounts.slice().shift();
{
const { account, admin } = await makeTestAccount(datasource, {
tier: 2,
parent_account_id: tier1.id,
parent_account_id: tier1?.account.id,
company_name: 'OMDS_US',
});
state.tier2Accounts.push({
@ -72,7 +73,7 @@ export const makeHierarchicalAccounts = async (
{
const { account, admin } = await makeTestAccount(datasource, {
tier: 2,
parent_account_id: tier1.id,
parent_account_id: tier1?.account.id,
company_name: 'OMDS_EU',
});
state.tier2Accounts.push({
@ -201,7 +202,7 @@ export const makeTestAccount = async (
}
// Accountの管理者を設定する
let secondaryAdminUserId = null;
let secondaryAdminUserId: number | null = null;
if (isPrimaryAdminNotExist && !isSecondaryAdminNotExist) {
secondaryAdminUserId = userId;
}
@ -224,6 +225,9 @@ export const makeTestAccount = async (
id: userId,
},
});
if (!account || !admin) {
throw new Error('Unexpected null');
}
return {
account: account,
@ -263,7 +267,9 @@ export const makeTestSimpleAccount = async (
id: result.id,
},
});
if (!account) {
throw new Error('Unexpected null');
}
return account;
};
@ -299,11 +305,15 @@ export const makeTestUser = async (
});
const result = identifiers.pop() as User;
return await datasource.getRepository(User).findOne({
const user = await datasource.getRepository(User).findOne({
where: {
id: result.id,
},
});
if (!user) {
throw new Error('Unexpected null');
}
return user;
};
/**
@ -312,7 +322,10 @@ export const makeTestUser = async (
* @param id ID
* @returns
*/
export const getAccount = async (dataSource: DataSource, id: number) => {
export const getAccount = async (
dataSource: DataSource,
id: number,
): Promise<Account | null> => {
return await dataSource.getRepository(Account).findOne({
where: { id: id },
});
@ -353,7 +366,7 @@ export const getUserFromExternalId = async (
export const getUser = async (
datasource: DataSource,
id: number,
): Promise<User> => {
): Promise<User | null> => {
const user = await datasource.getRepository(User).findOne({
where: {
id: id,
@ -381,3 +394,14 @@ export const getUserArchive = async (
): Promise<UserArchive[]> => {
return await dataSource.getRepository(UserArchive).find();
};
export const getLicenses = async (
datasource: DataSource,
account_id: number,
): Promise<License[]> => {
const licenses = await datasource.getRepository(License).find({
where: {
account_id: account_id,
},
});
return licenses;
};

View File

@ -244,7 +244,7 @@ export const OPTION_ITEM_VALUE_TYPE = {
* @const {string[]}
*/
export const ADB2C_SIGN_IN_TYPE = {
EAMILADDRESS: 'emailAddress',
EMAILADDRESS: 'emailAddress',
} as const;
/**

View File

@ -66,6 +66,8 @@ import {
GetAuthorsResponse,
GetAccountInfoMinimalAccessRequest,
GetAccountInfoMinimalAccessResponse,
DeleteWorktypeRequestParam,
DeleteWorktypeResponse,
} from './types/types';
import { USER_ROLES, ADMIN_ROLES, TIERS } from '../../constants';
import { AuthGuard } from '../../common/guards/auth/authguards';
@ -198,14 +200,26 @@ export class AccountsController {
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
@Get('me')
async getMyAccount(@Req() req: Request): Promise<GetMyAccountResponse> {
// アクセストークン取得
const accessToken = retrieveAuthorizationToken(req);
const payload = jwt.decode(accessToken, { json: true }) as AccessToken;
const context = makeContext(payload.userId);
const accessToken = retrieveAuthorizationToken(req) as string;
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);
//アカウントID取得処理
const accountInfo = await this.accountService.getAccountInfo(
context,
payload.userId,
userId,
);
return accountInfo;
}
@ -235,8 +249,21 @@ export class AccountsController {
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
@Get('authors')
async getAuthors(@Req() req: Request): Promise<GetAuthorsResponse> {
const accessToken = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(accessToken, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
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 authors = await this.accountService.getAuthors(context, userId);
@ -268,10 +295,23 @@ export class AccountsController {
@UseGuards(AuthGuard)
@Get('typists')
async getTypists(@Req() req: Request): Promise<GetTypistsResponse> {
const accessToken = retrieveAuthorizationToken(req);
const payload = jwt.decode(accessToken, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
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 typists = await this.accountService.getTypists(payload.userId);
const typists = await this.accountService.getTypists(userId);
return { typists };
}
@ -300,12 +340,23 @@ export class AccountsController {
@UseGuards(AuthGuard)
@Get('typist-groups')
async getTypistGroups(@Req() req: Request): Promise<GetTypistGroupsResponse> {
const accessToken = retrieveAuthorizationToken(req);
const payload = jwt.decode(accessToken, { json: true }) as AccessToken;
const typistGroups = await this.accountService.getTypistGroups(
payload.userId,
const accessToken = retrieveAuthorizationToken(req) as string;
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 typistGroups = await this.accountService.getTypistGroups(userId);
return { typistGroups };
}
@ -346,8 +397,22 @@ export class AccountsController {
const { typistGroupId } = param;
// アクセストークン取得
const accessToken = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(accessToken, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
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);
@ -395,8 +460,22 @@ export class AccountsController {
): Promise<CreateTypistGroupResponse> {
const { typistGroupName, typistIds } = body;
// アクセストークン取得
const accessToken = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(accessToken, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
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.accountService.createTypistGroup(
context,
@ -445,8 +524,22 @@ export class AccountsController {
const { typistGroupId } = param;
// アクセストークン取得
const accessToken = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(accessToken, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
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);
@ -496,10 +589,24 @@ export class AccountsController {
@Body() body: CreatePartnerAccountRequest,
): Promise<CreatePartnerAccountResponse> {
const { companyName, country, email, adminName } = body;
const accessToken = retrieveAuthorizationToken(req);
const payload = jwt.decode(accessToken, { json: true }) as AccessToken;
const context = makeContext(payload.userId);
const accessToken = retrieveAuthorizationToken(req) as string;
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, tier } = decodedAccessToken as AccessToken;
const context = makeContext(userId);
await this.accountService.createPartnerAccount(
context,
@ -507,8 +614,8 @@ export class AccountsController {
country,
email,
adminName,
payload.userId,
payload.tier,
userId,
tier,
);
return {};
@ -624,15 +731,28 @@ export class AccountsController {
): Promise<IssueLicenseResponse> {
const { orderedAccountId, poNumber } = body;
const token = retrieveAuthorizationToken(req);
const accessToken = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
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, tier } = decodedAccessToken as AccessToken;
const context = makeContext(accessToken.userId);
const context = makeContext(userId);
await this.accountService.issueLicense(
context,
orderedAccountId,
accessToken.userId,
accessToken.tier,
userId,
tier,
poNumber,
);
return {};
@ -692,14 +812,27 @@ export class AccountsController {
@Req() req: Request,
@Body() body: CancelIssueRequest,
): Promise<CancelIssueResponse> {
const token = retrieveAuthorizationToken(req);
const payload = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
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(payload.userId);
const context = makeContext(userId);
await this.accountService.cancelIssue(
context,
payload.userId,
userId,
body.poNumber,
body.orderedAccountId,
);
@ -727,8 +860,21 @@ export class AccountsController {
@UseGuards(AuthGuard)
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
async getWorktypes(@Req() req: Request): Promise<GetWorktypesResponse> {
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
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 worktypes = await this.accountService.getWorktypes(context, userId);
@ -766,8 +912,22 @@ export class AccountsController {
@Body() body: CreateWorktypesRequest,
): Promise<CreateWorktypeResponse> {
const { worktypeId, description } = body;
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
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.accountService.createWorktype(
@ -812,8 +972,22 @@ export class AccountsController {
): Promise<UpdateWorktypeResponse> {
const { worktypeId, description } = body;
const { id } = param;
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
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);
@ -828,6 +1002,59 @@ export class AccountsController {
return {};
}
@Post('/worktypes/:id/delete')
@ApiResponse({
status: HttpStatus.OK,
type: DeleteWorktypeResponse,
description: '成功時のレスポンス',
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: '指定WorktypeIDが削除済み / 指定WorktypeIDがWorkflowで使用中',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.UNAUTHORIZED,
description: '認証エラー',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: '想定外のサーバーエラー',
type: ErrorResponse,
})
@ApiOperation({ operationId: 'deleteWorktype' })
@ApiBearerAuth()
@UseGuards(AuthGuard)
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
async deleteWorktype(
@Req() req: Request,
@Param() param: DeleteWorktypeRequestParam,
): Promise<DeleteWorktypeResponse> {
const { id } = param;
const accessToken = retrieveAuthorizationToken(req) as string;
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.accountService.deleteWorktype(context, userId, id);
return {};
}
@Get('/worktypes/:id/option-items')
@ApiResponse({
status: HttpStatus.OK,
@ -858,8 +1085,22 @@ export class AccountsController {
@Param() param: GetOptionItemsRequestParam,
): Promise<GetOptionItemsResponse> {
const { id } = param;
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
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);
@ -904,8 +1145,22 @@ export class AccountsController {
): Promise<UpdateOptionItemsResponse> {
const { optionItems } = body;
const { id } = param;
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
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);
@ -949,8 +1204,22 @@ export class AccountsController {
@Body() body: PostActiveWorktypeRequest,
): Promise<PostActiveWorktypeResponse> {
const { id } = body;
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
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);
@ -993,8 +1262,22 @@ export class AccountsController {
@Query() query: GetPartnersRequest,
): Promise<GetPartnersResponse> {
const { limit, offset } = query;
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
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 response = await this.accountService.getPartners(
@ -1046,8 +1329,22 @@ export class AccountsController {
primaryAdminUserId,
secondryAdminUserId,
} = body;
const token = retrieveAuthorizationToken(req);
const { userId, tier } = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
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, tier } = decodedAccessToken as AccessToken;
const context = makeContext(userId);
await this.accountService.updateAccountInfo(
@ -1060,7 +1357,7 @@ export class AccountsController {
secondryAdminUserId,
);
return;
return {};
}
@Post('/delete')
@ -1092,12 +1389,26 @@ export class AccountsController {
@Body() body: DeleteAccountRequest,
): Promise<DeleteAccountResponse> {
const { accountId } = body;
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
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.accountService.deleteAccountAndData(context, userId, accountId);
return;
return {};
}
@Post('/minimal-access')

View File

@ -33,6 +33,7 @@ import {
GetPartnersResponse,
PostWorktypeOptionItem,
Author,
Partner,
} from './types/types';
import {
DateWithZeroTime,
@ -65,12 +66,15 @@ import {
import { WorktypesRepositoryService } from '../../repositories/worktypes/worktypes.repository.service';
import {
WorktypeIdAlreadyExistsError,
WorktypeIdInUseError,
WorktypeIdMaxCountError,
WorktypeIdNotFoundError,
} from '../../repositories/worktypes/errors/types';
@Injectable()
export class AccountsService {
private readonly mailFrom =
this.configService.getOrThrow<string>('MAIL_FROM');
constructor(
private readonly accountRepository: AccountsRepositoryService,
private readonly licensesRepository: LicensesRepositoryService,
@ -256,9 +260,6 @@ export class AccountsService {
}
try {
// メールの送信元を取得
const from = this.configService.get<string>('MAIL_FROM') ?? '';
// メールの内容を構成
const { subject, text, html } =
await this.sendgridService.createMailContentFromEmailConfirm(
@ -272,7 +273,7 @@ export class AccountsService {
await this.sendgridService.sendMail(
context,
email,
from,
this.mailFrom,
subject,
text,
html,
@ -393,7 +394,7 @@ export class AccountsService {
userInfo.account_id,
);
let parentInfo: Account;
let parentInfo: Account | undefined;
if (accountInfo.parent_account_id) {
parentInfo = await this.accountRepository.findAccountById(
accountInfo.parent_account_id,
@ -480,14 +481,20 @@ export class AccountsService {
const { account_id } = await this.usersRepository.findUserByExternalId(
externalId,
);
const userGroup = await this.userGroupsRepository.getTypistGroup(
const { name, userGroupMembers } =
await this.userGroupsRepository.getTypistGroup(
account_id,
typistGroupId,
);
if (!userGroupMembers) {
throw new TypistGroupNotExistError(
`Typist Group is not exist. typistGroupId: ${typistGroupId}`,
);
}
return {
typistGroupName: userGroup.name,
typistIds: userGroup.userGroupMembers.map((x) => x.user_id),
typistGroupName: name,
typistIds: userGroupMembers.map((x) => x.user_id),
};
} catch (e) {
this.logger.error(`error=${e}`);
@ -540,6 +547,11 @@ export class AccountsService {
const typists = typistUsers.map((x) => {
const user = adb2cUsers.find((adb2c) => adb2c.id === x.external_id);
if (!user) {
throw new Error(
`user not found. externalId: ${x.external_id}, userId: ${x.id}`,
);
}
return {
id: x.id,
name: user.displayName,
@ -585,6 +597,11 @@ export class AccountsService {
);
const authors = authorUsers.map((x) => {
if (!x.author_id) {
throw new Error(
`author_id is Not Found. externalId: ${x.external_id}, userId: ${x.id}`,
);
}
return {
id: x.id,
authorId: x.author_id,
@ -700,8 +717,8 @@ export class AccountsService {
creatorAccountTier + 1,
externalUser.sub,
USER_ROLES.NONE,
null,
null,
undefined,
undefined,
);
account = newAccount;
user = adminUser;
@ -742,7 +759,6 @@ export class AccountsService {
}
try {
const from = this.configService.get<string>('MAIL_FROM') || '';
const { subject, text, html } =
await this.sendgridService.createMailContentFromEmailConfirmForNormalUser(
account.id,
@ -752,7 +768,7 @@ export class AccountsService {
await this.sendgridService.sendMail(
context,
email,
from,
this.mailFrom,
subject,
text,
html,
@ -835,11 +851,19 @@ export class AccountsService {
// 各子アカウントのShortageを算出してreturn用の変数にマージする
const childrenPartnerLicenses: PartnerLicenseInfo[] = [];
for (const childPartnerLicenseFromRepository of getPartnerLicenseResult.childPartnerLicensesFromRepository) {
let childShortage;
const { allocatableLicenseWithMargin, expiringSoonLicense } =
childPartnerLicenseFromRepository;
let childShortage: number = 0;
if (childPartnerLicenseFromRepository.tier === TIERS.TIER5) {
childShortage =
childPartnerLicenseFromRepository.allocatableLicenseWithMargin -
childPartnerLicenseFromRepository.expiringSoonLicense;
if (
allocatableLicenseWithMargin === undefined ||
expiringSoonLicense === undefined
) {
throw new Error(
`Tier5 account has no allocatableLicenseWithMargin or expiringSoonLicense. accountId: ${accountId}`,
);
}
childShortage = allocatableLicenseWithMargin - expiringSoonLicense;
} else {
childShortage =
childPartnerLicenseFromRepository.stockLicense -
@ -906,13 +930,13 @@ export class AccountsService {
licenseOrder.issued_at !== null
? new Date(licenseOrder.issued_at)
.toISOString()
.substr(0, 10)
.substring(0, 10)
.replace(/-/g, '/')
: null,
: undefined,
numberOfOrder: licenseOrder.quantity,
orderDate: new Date(licenseOrder.ordered_at)
.toISOString()
.substr(0, 10)
.substring(0, 10)
.replace(/-/g, '/'),
poNumber: licenseOrder.po_number,
status: licenseOrder.status,
@ -1417,6 +1441,81 @@ export class AccountsService {
}
}
/**
*
* @param context
* @param externalId
* @param id
* @returns worktype
*/
async deleteWorktype(
context: Context,
externalId: string,
id: number,
): Promise<void> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.deleteWorktype.name} | params: { ` +
`externalId: ${externalId}, ` +
`id: ${id} };`,
);
try {
// 外部IDをもとにユーザー情報を取得する
const { account, account_id: accountId } =
await this.usersRepository.findUserByExternalId(externalId);
if (!account) {
throw new AccountNotFoundError(
`account not found. externalId: ${externalId}`,
);
}
// ワークタイプを削除する
await this.worktypesRepository.deleteWorktype(accountId, id);
} catch (e) {
this.logger.error(`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,
);
// 内部IDで指定されたWorktypeが存在しない場合は400エラーを返す
case WorktypeIdNotFoundError:
throw new HttpException(
makeErrorResponse('E011003'),
HttpStatus.BAD_REQUEST,
);
// 内部IDで指定されたWorktypeがWorkflowで使用中の場合は400エラーを返す
case WorktypeIdInUseError:
throw new HttpException(
makeErrorResponse('E011004'),
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.deleteWorktype.name}`,
);
}
}
/**
*
* @param context
@ -1558,7 +1657,7 @@ export class AccountsService {
async updateActiveWorktype(
context: Context,
externalId: string,
id: number,
id: number | undefined,
): Promise<void> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.updateActiveWorktype.name} | params: { ` +
@ -1625,34 +1724,40 @@ export class AccountsService {
const { account_id: accountId } =
await this.usersRepository.findUserByExternalId(externalId);
const partners = await this.accountRepository.getPartners(
const partnersRecords = await this.accountRepository.getPartners(
accountId,
limit,
offset,
);
// DBから取得したユーザーの外部IDをもとにADB2Cからユーザーを取得する
let externalIds = partners.partnersInfo.map(
let externalIds = partnersRecords.partnersInfo.map(
(x) => x.primaryAccountExternalId,
);
externalIds = externalIds.filter((item) => item !== undefined);
const adb2cUsers = await this.adB2cService.getUsers(context, externalIds);
// DBから取得した情報とADB2Cから取得した情報をマージ
const response = partners.partnersInfo.map((db) => {
const partners = partnersRecords.partnersInfo.map((db): Partner => {
const adb2cUser = adb2cUsers.find(
(adb2c) => db.primaryAccountExternalId === adb2c.id,
);
let primaryAdmin = undefined;
let mail = undefined;
if (adb2cUser) {
primaryAdmin = adb2cUser.displayName;
mail = adb2cUser.identities.find(
(identity) =>
identity.signInType === ADB2C_SIGN_IN_TYPE.EAMILADDRESS,
).issuerAssignedId;
if (!adb2cUser) {
throw new Error(
`adb2c user not found. externalId: ${db.primaryAccountExternalId}`,
);
}
const primaryAdmin = adb2cUser.displayName;
const mail = adb2cUser.identities?.find(
(identity) => identity.signInType === ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
)?.issuerAssignedId;
if (!mail) {
throw new Error(
`adb2c user mail not found. externalId: ${db.primaryAccountExternalId}`,
);
}
return {
name: db.name,
tier: db.tier,
@ -1665,17 +1770,15 @@ export class AccountsService {
});
return {
total: partners.total,
partners: response,
total: partnersRecords.total,
partners: partners,
};
} catch (e) {
this.logger.error(`error=${e}`);
if (e instanceof Error) {
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
} finally {
this.logger.log(`[OUT] [${context.trackingId}] ${this.getPartners.name}`);
}

View File

@ -30,7 +30,7 @@ export type LicensesRepositoryMockValue = {
orderHistories: LicenseOrder[];
}
| Error;
issueLicense: undefined | Error;
issueLicense: void | Error;
};
export type UsersRepositoryMockValue = {
findUserById: User | Error;
@ -61,10 +61,12 @@ export type ConfigMockValue = {
get: string | Error;
};
export type AccountsRepositoryMockValue = {
getLicenseSummaryInfo: {
getLicenseSummaryInfo:
| {
licenseSummary: LicenseSummaryInfo;
isStorageAvailable: boolean;
};
}
| Error;
createAccount: { newAccount: Account; adminUser: User } | Error;
};
@ -181,18 +183,7 @@ export const makeLicensesRepositoryMock = (
issueLicense:
issueLicense instanceof Error
? jest.fn<Promise<void>, []>().mockRejectedValue(issueLicense)
: jest
.fn<
Promise<{
context: Context;
orderedAccountId: number;
myAccountId: number;
tier: number;
poNumber: string;
}>,
[]
>()
.mockResolvedValue(issueLicense),
: jest.fn<Promise<void>, []>().mockResolvedValue(issueLicense),
};
};
export const makeUsersRepositoryMock = (value: UsersRepositoryMockValue) => {
@ -355,7 +346,7 @@ export const makeDefaultAccountsRepositoryMockValue =
user.created_by = 'test';
user.created_at = new Date();
user.updated_by = null;
user.updated_at = null;
user.updated_at = new Date();
return {
getLicenseSummaryInfo: {
licenseSummary: licenseSummaryInfo,
@ -385,7 +376,7 @@ export const makeDefaultUsersRepositoryMockValue =
user.created_by = 'test';
user.created_at = new Date();
user.updated_by = null;
user.updated_at = null;
user.updated_at = new Date();
const typists: User[] = [];
typists.push(
@ -434,7 +425,7 @@ export const makeDefaultUserGroupsRepositoryMockValue =
user.created_by = 'test';
user.created_at = new Date();
user.updated_by = null;
user.updated_at = null;
user.updated_at = new Date();
return {
getUserGroups: [
@ -444,6 +435,10 @@ export const makeDefaultUserGroupsRepositoryMockValue =
name: 'GroupA',
created_by: 'test',
updated_by: 'test',
created_at: new Date(),
deleted_at: null,
updated_at: null,
userGroupMembers: null,
},
{
id: 2,
@ -451,6 +446,10 @@ export const makeDefaultUserGroupsRepositoryMockValue =
name: 'GroupB',
created_by: 'test',
updated_by: 'test',
created_at: new Date(),
deleted_at: null,
updated_at: null,
userGroupMembers: null,
},
],
};

View File

@ -23,14 +23,14 @@ export const getSortCriteria = async (dataSource: DataSource) => {
export const createLicense = async (
datasource: DataSource,
licenseId: number,
expiry_date: Date,
expiry_date: Date | null,
accountId: number,
type: string,
status: string,
allocated_user_id: number,
order_id: number,
deleted_at: Date,
delete_order_id: number,
allocated_user_id: number | null,
order_id: number | null,
deleted_at: Date | null,
delete_order_id: number | null,
): Promise<void> => {
const { identifiers } = await datasource.getRepository(License).insert({
id: licenseId,
@ -54,7 +54,7 @@ export const createLicense = async (
export const createLicenseSetExpiryDateAndStatus = async (
datasource: DataSource,
accountId: number,
expiryDate: Date,
expiryDate: Date | null,
status: string,
): Promise<void> => {
const { identifiers } = await datasource.getRepository(License).insert({
@ -171,19 +171,21 @@ export const createOptionItems = async (
datasource: DataSource,
worktypeId: number,
): Promise<OptionItem[]> => {
const optionItems = [];
const optionItems: OptionItem[] = [];
for (let i = 0; i < 10; i++) {
optionItems.push({
worktype_id: worktypeId,
item_label: '',
default_value_type: OPTION_ITEM_VALUE_TYPE.DEFAULT,
initial_value: '',
created_by: 'test_runner',
created_at: new Date(),
updated_by: 'updater',
updated_at: new Date(),
});
const optionItem = new OptionItem();
{
optionItem.worktype_id = worktypeId;
optionItem.item_label = '';
optionItem.default_value_type = OPTION_ITEM_VALUE_TYPE.DEFAULT;
optionItem.initial_value = '';
optionItem.created_by = 'test_runner';
optionItem.created_at = new Date();
optionItem.updated_by = 'updater';
optionItem.updated_at = new Date();
}
optionItems.push(optionItem);
}
await datasource.getRepository(OptionItem).insert(optionItems);

View File

@ -327,8 +327,8 @@ export class GetOrderHistoriesRequest {
export class LicenseOrder {
@ApiProperty({ description: '注文日付' })
orderDate: string;
@ApiProperty({ description: '発行日付' })
issueDate: string;
@ApiProperty({ description: '発行日付', required: false })
issueDate?: string;
@ApiProperty({ description: '注文数' })
numberOfOrder: number;
@ApiProperty({ description: 'POナンバー' })
@ -497,6 +497,16 @@ export class UpdateWorktypeRequestParam {
id: number;
}
export class DeleteWorktypeRequestParam {
@ApiProperty({ description: 'Worktypeの内部ID' })
@Type(() => Number)
@IsInt()
@Min(0)
id: number;
}
export class DeleteWorktypeResponse {}
export class PostActiveWorktypeRequest {
@ApiProperty({
required: false,

View File

@ -5,12 +5,19 @@ import {
makeAdB2cServiceMock,
makeDefaultAdB2cMockValue,
} from './test/auth.service.mock';
import { ConfigModule } from '@nestjs/config';
describe('AuthController', () => {
let controller: AuthController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
envFilePath: ['.env.local', '.env'],
isGlobal: true,
}),
],
controllers: [AuthController],
providers: [AuthService],
})

View File

@ -3,6 +3,7 @@ import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import {
makeAuthServiceMock,
makeDefaultAdB2cMockValue,
makeDefaultConfigValue,
makeDefaultGetPublicKeyFromJwk,
} from './test/auth.service.mock';
import { DataSource } from 'typeorm';
@ -16,7 +17,8 @@ import { v4 as uuidv4 } from 'uuid';
describe('AuthService', () => {
it('IDトークンの検証とペイロードの取得に成功する', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
const service = await makeAuthServiceMock(adb2cParam);
const configMockValue = makeDefaultConfigValue();
const service = await makeAuthServiceMock(adb2cParam, configMockValue);
//JWKの生成→PEM変換を自力で表現することが厳しいためMockで代替
service.getPublicKeyFromJwk = makeDefaultGetPublicKeyFromJwk;
const token =
@ -27,7 +29,8 @@ describe('AuthService', () => {
it('IDトークンの形式が不正な場合、形式不正エラーとなる。', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
const service = await makeAuthServiceMock(adb2cParam);
const configMockValue = makeDefaultConfigValue();
const service = await makeAuthServiceMock(adb2cParam, configMockValue);
const token = 'invalid.id.token';
await expect(service.getVerifiedIdToken(token)).rejects.toEqual(
@ -37,7 +40,8 @@ describe('AuthService', () => {
it('IDトークンの有効期限が切れている場合、有効期限切れエラーとなる。', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
const service = await makeAuthServiceMock(adb2cParam);
const configMockValue = makeDefaultConfigValue();
const service = await makeAuthServiceMock(adb2cParam, configMockValue);
//JWKの生成→PEM変換を自力で表現することが厳しいためMockで代替
service.getPublicKeyFromJwk = makeDefaultGetPublicKeyFromJwk;
const token =
@ -50,7 +54,8 @@ describe('AuthService', () => {
it('IDトークンが開始日より前の場合、開始前エラーとなる。', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
const service = await makeAuthServiceMock(adb2cParam);
const configMockValue = makeDefaultConfigValue();
const service = await makeAuthServiceMock(adb2cParam, configMockValue);
//JWKの生成→PEM変換を自力で表現することが厳しいためMockで代替
service.getPublicKeyFromJwk = makeDefaultGetPublicKeyFromJwk;
const token =
@ -63,7 +68,8 @@ describe('AuthService', () => {
it('IDトークンの署名が不正な場合、署名不正エラーとなる。', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
const service = await makeAuthServiceMock(adb2cParam);
const configMockValue = makeDefaultConfigValue();
const service = await makeAuthServiceMock(adb2cParam, configMockValue);
const token =
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtpZCJ9.eyJleHAiOjkwMDAwMDAwMDAsIm5iZiI6MTAwMDAwMDAwMCwidmVyIjoiMS4wIiwiaXNzIjoiaXNzdXNlciIsInN1YiI6InN1YiIsImF1ZCI6ImF1ZCIsIm5vbmNlIjoiZGVmYXVsdE5vbmNlIiwiaWF0IjoxMDAwMDAwMDAwLCJhdXRoX3RpbWUiOjEwMDAwMDAwMDAsImVtYWlscyI6WyJ4eHhAeHguY29tIl0sInRmcCI6InNpZ25pbl91c2VyZmxvdyJ9.sign';
@ -74,7 +80,8 @@ describe('AuthService', () => {
it('IDトークンの発行元が想定と異なる場合、発行元不正エラーとなる。', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
const service = await makeAuthServiceMock(adb2cParam);
const configMockValue = makeDefaultConfigValue();
const service = await makeAuthServiceMock(adb2cParam, configMockValue);
//JWKの生成→PEM変換を自力で表現することが厳しいためMockで代替
service.getPublicKeyFromJwk = makeDefaultGetPublicKeyFromJwk;
const token =
@ -87,8 +94,9 @@ describe('AuthService', () => {
it('Azure ADB2Cでネットワークエラーとなる場合、エラーとなる。メタデータ', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
const configMockValue = makeDefaultConfigValue();
adb2cParam.getMetaData = new Error('failed get metadata');
const service = await makeAuthServiceMock(adb2cParam);
const service = await makeAuthServiceMock(adb2cParam, configMockValue);
const token =
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtpZCJ9.eyJleHAiOjkwMDAwMDAwMDAsIm5iZiI6MTAwMDAwMDAwMCwidmVyIjoiMS4wIiwiaXNzIjoiaXNzdWVyIiwic3ViIjoic3ViIiwiYXVkIjoiYXVkIiwibm9uY2UiOiJkZWZhdWx0Tm9uY2UiLCJpYXQiOjEwMDAwMDAwMDAsImF1dGhfdGltZSI6MTAwMDAwMDAwMCwiZW1haWxzIjpbInh4eEB4eC5jb20iXSwidGZwIjoic2lnbmluX3VzZXJmbG93In0.RyieW-VHsHPQOjXbbhRc307AYJOc1sq2hrcu4SW1-K0pvLlkplepxvx02a3vCwQrnBYrIP5w6HExG-S_JgW5nYyWr6DeY11mA484n9KA8GeAcAXV37StH1gfWUJvfGb4C8BaMbMM9Ix4Z9NGwKA9vjNwevfmBZnz9lQUePgv6BJNmyvCt8ElJ01O-1WODbZuojJ4xXymA1OqluzfbphPOsqWTSNmTn0emkLjjnlMQf1iwM4C_kvvr8dUCFg0_UGDfQVJnzPEKB38UqnhLnC5WacrddDwQ0kBuGKZgZ_63Q_7fOvqAZivqLK7BPmbPxi6mx3R1S9Eq2ugzpY1LfJOjA';
@ -101,8 +109,9 @@ describe('AuthService', () => {
});
it('Azure ADB2Cでネットワークエラーとなる場合、エラーとなる。キーセット', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
const configMockValue = makeDefaultConfigValue();
adb2cParam.getSignKeySets = new Error('failed get keyset');
const service = await makeAuthServiceMock(adb2cParam);
const service = await makeAuthServiceMock(adb2cParam, configMockValue);
const token =
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtpZCJ9.eyJleHAiOjkwMDAwMDAwMDAsIm5iZiI6MTAwMDAwMDAwMCwidmVyIjoiMS4wIiwiaXNzIjoiaXNzdWVyIiwic3ViIjoic3ViIiwiYXVkIjoiYXVkIiwibm9uY2UiOiJkZWZhdWx0Tm9uY2UiLCJpYXQiOjEwMDAwMDAwMDAsImF1dGhfdGltZSI6MTAwMDAwMDAwMCwiZW1haWxzIjpbInh4eEB4eC5jb20iXSwidGZwIjoic2lnbmluX3VzZXJmbG93In0.RyieW-VHsHPQOjXbbhRc307AYJOc1sq2hrcu4SW1-K0pvLlkplepxvx02a3vCwQrnBYrIP5w6HExG-S_JgW5nYyWr6DeY11mA484n9KA8GeAcAXV37StH1gfWUJvfGb4C8BaMbMM9Ix4Z9NGwKA9vjNwevfmBZnz9lQUePgv6BJNmyvCt8ElJ01O-1WODbZuojJ4xXymA1OqluzfbphPOsqWTSNmTn0emkLjjnlMQf1iwM4C_kvvr8dUCFg0_UGDfQVJnzPEKB38UqnhLnC5WacrddDwQ0kBuGKZgZ_63Q_7fOvqAZivqLK7BPmbPxi6mx3R1S9Eq2ugzpY1LfJOjA';
@ -116,10 +125,11 @@ describe('AuthService', () => {
it('Azure ADB2Cから取得した鍵が一致しない場合、エラーとなる。', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
const configMockValue = makeDefaultConfigValue();
adb2cParam.getSignKeySets = [
{ kid: 'invalid', kty: 'RSA', nbf: 0, use: 'sig', e: '', n: '' },
];
const service = await makeAuthServiceMock(adb2cParam);
const service = await makeAuthServiceMock(adb2cParam, configMockValue);
const token =
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtpZCJ9.eyJleHAiOjkwMDAwMDAwMDAsIm5iZiI6MTAwMDAwMDAwMCwidmVyIjoiMS4wIiwiaXNzIjoiaXNzdWVyIiwic3ViIjoic3ViIiwiYXVkIjoiYXVkIiwibm9uY2UiOiJkZWZhdWx0Tm9uY2UiLCJpYXQiOjEwMDAwMDAwMDAsImF1dGhfdGltZSI6MTAwMDAwMDAwMCwiZW1haWxzIjpbInh4eEB4eC5jb20iXSwidGZwIjoic2lnbmluX3VzZXJmbG93In0.RyieW-VHsHPQOjXbbhRc307AYJOc1sq2hrcu4SW1-K0pvLlkplepxvx02a3vCwQrnBYrIP5w6HExG-S_JgW5nYyWr6DeY11mA484n9KA8GeAcAXV37StH1gfWUJvfGb4C8BaMbMM9Ix4Z9NGwKA9vjNwevfmBZnz9lQUePgv6BJNmyvCt8ElJ01O-1WODbZuojJ4xXymA1OqluzfbphPOsqWTSNmTn0emkLjjnlMQf1iwM4C_kvvr8dUCFg0_UGDfQVJnzPEKB38UqnhLnC5WacrddDwQ0kBuGKZgZ_63Q_7fOvqAZivqLK7BPmbPxi6mx3R1S9Eq2ugzpY1LfJOjA';
@ -133,7 +143,7 @@ describe('AuthService', () => {
});
describe('checkIsAcceptedLatestVersion', () => {
let source: DataSource = null;
let source: DataSource | null = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
@ -146,11 +156,14 @@ describe('checkIsAcceptedLatestVersion', () => {
});
afterEach(async () => {
if (!source) return;
await source.destroy();
source = null;
});
it('同意済み利用規約バージョンが最新のときにチェックが通ること(第五)', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<AuthService>(AuthService);
const { admin } = await makeTestAccount(source, {
tier: 5,
@ -171,7 +184,9 @@ describe('checkIsAcceptedLatestVersion', () => {
});
it('同意済み利用規約バージョンが最新のときにチェックが通ること(第一~第四)', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<AuthService>(AuthService);
const { admin } = await makeTestAccount(source, {
tier: 4,
@ -192,7 +207,9 @@ describe('checkIsAcceptedLatestVersion', () => {
});
it('同意済み利用規約バージョンが最新でないときにチェックが通らないこと(第五)', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<AuthService>(AuthService);
const { admin } = await makeTestAccount(source, {
tier: 5,
@ -213,7 +230,9 @@ describe('checkIsAcceptedLatestVersion', () => {
});
it('同意済み利用規約EULAバージョンが最新でないときにチェックが通らないこと第一第四', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<AuthService>(AuthService);
const { admin } = await makeTestAccount(source, {
tier: 4,
@ -234,7 +253,9 @@ describe('checkIsAcceptedLatestVersion', () => {
});
it('同意済み利用規約バージョンDPAが最新でないときにチェックが通らないこと第一第四', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<AuthService>(AuthService);
const { admin } = await makeTestAccount(source, {
tier: 4,

View File

@ -25,6 +25,13 @@ import { Context } from '../../common/log';
@Injectable()
export class AuthService {
private readonly refreshTokenLifetimeWeb =
this.configService.getOrThrow<number>('REFRESH_TOKEN_LIFETIME_WEB');
private readonly refreshTokenLifetimeDefault =
this.configService.getOrThrow<number>('REFRESH_TOKEN_LIFETIME_DEFAULT');
private readonly accessTokenlifetime = this.configService.getOrThrow<number>(
'ACCESS_TOKEN_LIFETIME_WEB',
);
constructor(
private readonly adB2cService: AdB2cService,
private readonly configService: ConfigService,
@ -68,10 +75,7 @@ export class AuthService {
this.logger.log(
`[IN] [${context.trackingId}] ${this.generateRefreshToken.name}`,
);
const lifetimeWeb = this.configService.get('REFRESH_TOKEN_LIFETIME_WEB');
const lifetimeDefault = this.configService.get(
'REFRESH_TOKEN_LIFETIME_DEFAULT',
);
let user: User;
// ユーザー情報とユーザーが属しているアカウント情報を取得
try {
@ -106,7 +110,10 @@ export class AuthService {
);
}
// 要求された環境用トークンの寿命を決定
const refreshTokenLifetime = type === 'web' ? lifetimeWeb : lifetimeDefault;
const refreshTokenLifetime =
type === 'web'
? this.refreshTokenLifetimeWeb
: this.refreshTokenLifetimeDefault;
const privateKey = getPrivateKey(this.configService);
// ユーザーのロールを設定
@ -165,7 +172,6 @@ export class AuthService {
this.logger.log(
`[IN] [${context.trackingId}] ${this.generateAccessToken.name}`,
);
const lifetime = this.configService.get('ACCESS_TOKEN_LIFETIME_WEB');
const privateKey = getPrivateKey(this.configService);
const pubkey = getPublicKey(this.configService);
@ -188,7 +194,7 @@ export class AuthService {
tier: token.tier,
userId: token.userId,
},
lifetime,
this.accessTokenlifetime,
privateKey,
);
@ -205,11 +211,14 @@ export class AuthService {
async getVerifiedIdToken(token: string): Promise<IDToken> {
this.logger.log(`[IN] ${this.getVerifiedIdToken.name}`);
let kid = '';
let kid: string | undefined = '';
try {
// JWTトークンのヘッダを見るため一度デコードする
const decodedToken = jwt.decode(token, { complete: true });
kid = decodedToken.header.kid;
kid = decodedToken?.header.kid;
if (!kid) {
throw new Error('kid not found');
}
} catch (e) {
this.logger.error(`error=${e}`);
throw new HttpException(
@ -343,7 +352,7 @@ export class AuthService {
): Promise<boolean> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.isAcceptedLatestVersion.name} | params: { ` +
`idToken: ${idToken}, };`,
`idToken.sub: ${idToken.sub}, };`,
);
try {

View File

@ -9,9 +9,13 @@ export type AdB2cMockValue = {
getMetaData: B2cMetadata | Error;
getSignKeySets: JwkSignKey[] | Error;
};
export type ConfigMockValue = {
getOrThrow: number;
};
export const makeAuthServiceMock = async (
adB2cMockValue: AdB2cMockValue,
configMockValue: ConfigMockValue,
): Promise<AuthService> => {
const module: TestingModule = await Test.createTestingModule({
providers: [AuthService],
@ -21,7 +25,7 @@ export const makeAuthServiceMock = async (
case AdB2cService:
return makeAdB2cServiceMock(adB2cMockValue);
case ConfigService:
return {};
return makeConfigMock(configMockValue);
case UsersRepositoryService:
return {};
}
@ -80,3 +84,16 @@ export const makeDefaultGetPublicKeyFromJwk = (jwkKey: JwkSignKey): string => {
'-----END PUBLIC KEY-----',
].join('\n');
};
export const makeConfigMock = (value: ConfigMockValue) => {
const { getOrThrow } = value;
return {
getOrThrow: jest.fn<Promise<number>, []>().mockResolvedValue(getOrThrow),
};
};
export const makeDefaultConfigValue = (): ConfigMockValue => {
return {
getOrThrow: 80000,
};
};

View File

@ -2,6 +2,7 @@ import {
Body,
Controller,
Get,
HttpException,
HttpStatus,
Post,
Query,
@ -37,6 +38,7 @@ import { ADMIN_ROLES, USER_ROLES } from '../../constants';
import { retrieveAuthorizationToken } from '../../common/http/helper';
import { Request } from 'express';
import { makeContext } from '../../common/log';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
@ApiTags('files')
@Controller('files')
@ -75,10 +77,23 @@ export class FilesController {
@Req() req: Request,
@Body() body: AudioUploadFinishedRequest,
): Promise<AudioUploadFinishedResponse> {
const token = retrieveAuthorizationToken(req);
const accessToken = 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(accessToken.userId);
const context = makeContext(userId);
const {
url,
@ -99,7 +114,7 @@ export class FilesController {
const res = await this.filesService.uploadFinished(
context,
accessToken.userId,
userId,
url,
authorId,
fileName,
@ -149,10 +164,23 @@ export class FilesController {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@Query() _query: AudioUploadLocationRequest,
): Promise<AudioUploadLocationResponse> {
const token = retrieveAuthorizationToken(req);
const accessToken = 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(accessToken.userId);
const context = makeContext(userId);
const url = await this.filesService.publishUploadSas(context, accessToken);
return { url };
@ -195,14 +223,27 @@ export class FilesController {
): Promise<AudioDownloadLocationResponse> {
const { audioFileId } = body;
const token = retrieveAuthorizationToken(req);
const accessToken = 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(accessToken.userId);
const context = makeContext(userId);
const url = await this.filesService.publishAudioFileDownloadSas(
context,
accessToken.userId,
userId,
audioFileId,
);
@ -246,14 +287,27 @@ export class FilesController {
): Promise<TemplateDownloadLocationResponse> {
const { audioFileId } = body;
const token = retrieveAuthorizationToken(req);
const accessToken = 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(accessToken.userId);
const context = makeContext(userId);
const url = await this.filesService.publishTemplateFileDownloadSas(
context,
accessToken.userId,
userId,
audioFileId,
);
@ -287,8 +341,21 @@ export class FilesController {
async uploadTemplateLocation(
@Req() req: Request,
): Promise<TemplateUploadLocationResponse> {
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);
@ -333,8 +400,21 @@ export class FilesController {
@Body() body: TemplateUploadFinishedRequest,
): Promise<TemplateUploadFinishedReqponse> {
const { name, url } = body;
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);
await this.filesService.templateUploadFinished(context, userId, url, name);

View File

@ -35,11 +35,10 @@ describe('音声ファイルアップロードURL取得', () => {
);
expect(
await service.publishUploadSas(makeContext('trackingId'), {
userId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx',
role: 'Author',
tier: 5,
}),
await service.publishUploadSas(
makeContext('trackingId'),
'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx',
),
).toEqual('https://blob-storage?sas-token');
});
@ -57,11 +56,10 @@ describe('音声ファイルアップロードURL取得', () => {
);
expect(
await service.publishUploadSas(makeContext('trackingId'), {
userId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx',
role: 'Author',
tier: 5,
}),
await service.publishUploadSas(
makeContext('trackingId'),
'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx',
),
).toEqual('https://blob-storage?sas-token');
});
@ -78,11 +76,10 @@ describe('音声ファイルアップロードURL取得', () => {
);
await expect(
service.publishUploadSas(makeContext('trackingId'), {
userId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx',
role: 'Author',
tier: 5,
}),
service.publishUploadSas(
makeContext('trackingId'),
'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx',
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E009999'), HttpStatus.UNAUTHORIZED),
);
@ -102,11 +99,10 @@ describe('音声ファイルアップロードURL取得', () => {
blobParam.publishUploadSas = new Error('Azure service down');
await expect(
service.publishUploadSas(makeContext('trackingId'), {
userId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx',
role: 'Author',
tier: 5,
}),
service.publishUploadSas(
makeContext('trackingId'),
'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx',
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E009999'), HttpStatus.UNAUTHORIZED),
);
@ -295,7 +291,7 @@ describe('タスク作成', () => {
});
describe('音声ファイルダウンロードURL取得', () => {
let source: DataSource = null;
let source: DataSource | null = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
@ -308,11 +304,13 @@ describe('音声ファイルダウンロードURL取得', () => {
});
afterEach(async () => {
if (!source) return;
await source.destroy();
source = null;
});
it('ダウンロードSASトークンが乗っているURLを取得できる', async () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const {
external_id: externalId,
@ -333,7 +331,7 @@ describe('音声ファイルダウンロードURL取得', () => {
'test.zip',
'InProgress',
undefined,
authorId,
authorId ?? '',
);
const blobParam = makeBlobstorageServiceMockValue();
@ -341,6 +339,7 @@ describe('音声ファイルダウンロードURL取得', () => {
blobParam.fileExists = true;
const module = await makeTestingModuleWithBlob(source, blobParam);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
expect(
@ -353,6 +352,7 @@ describe('音声ファイルダウンロードURL取得', () => {
});
it('Typistの場合、タスクのステータスが[Inprogress,Pending]以外でエラー', async () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId, id: userId } = await makeTestUser(source, {
account_id: accountId,
@ -382,6 +382,7 @@ describe('音声ファイルダウンロードURL取得', () => {
blobParam.fileExists = true;
const module = await makeTestingModuleWithBlob(source, blobParam);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
await expect(
@ -396,6 +397,7 @@ describe('音声ファイルダウンロードURL取得', () => {
});
it('Typistの場合、自身が担当するタスクでない場合エラー', async () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId } = await makeTestUser(source, {
account_id: accountId,
@ -429,6 +431,7 @@ describe('音声ファイルダウンロードURL取得', () => {
blobParam.fileExists = true;
const module = await makeTestingModuleWithBlob(source, blobParam);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
await expect(
@ -443,6 +446,7 @@ describe('音声ファイルダウンロードURL取得', () => {
});
it('Authorの場合、自身が登録したタスクでない場合エラー', async () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId, id: userId } = await makeTestUser(source, {
account_id: accountId,
@ -467,6 +471,7 @@ describe('音声ファイルダウンロードURL取得', () => {
blobParam.fileExists = true;
const module = await makeTestingModuleWithBlob(source, blobParam);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
await expect(
@ -481,6 +486,7 @@ describe('音声ファイルダウンロードURL取得', () => {
});
it('Taskが存在しない場合はエラーとなる', async () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId } = await makeTestUser(source, {
account_id: accountId,
@ -492,6 +498,7 @@ describe('音声ファイルダウンロードURL取得', () => {
const blobParam = makeBlobstorageServiceMockValue();
const module = await makeTestingModuleWithBlob(source, blobParam);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
await expect(
@ -506,6 +513,7 @@ describe('音声ファイルダウンロードURL取得', () => {
});
it('blobストレージにファイルが存在しない場合はエラーとなる', async () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const {
external_id: externalId,
@ -526,7 +534,7 @@ describe('音声ファイルダウンロードURL取得', () => {
'test.zip',
'InProgress',
undefined,
authorId,
authorId ?? '',
);
const blobParam = makeBlobstorageServiceMockValue();
@ -534,6 +542,7 @@ describe('音声ファイルダウンロードURL取得', () => {
blobParam.fileExists = false;
const module = await makeTestingModuleWithBlob(source, blobParam);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
await expect(
@ -549,7 +558,7 @@ describe('音声ファイルダウンロードURL取得', () => {
});
describe('テンプレートファイルダウンロードURL取得', () => {
let source: DataSource = null;
let source: DataSource | null = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
@ -562,11 +571,13 @@ describe('テンプレートファイルダウンロードURL取得', () => {
});
afterEach(async () => {
if (!source) return;
await source.destroy();
source = null;
});
it('ダウンロードSASトークンが乗っているURLを取得できる', async () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId, author_id: authorId } = await makeTestUser(
source,
@ -586,7 +597,7 @@ describe('テンプレートファイルダウンロードURL取得', () => {
'test.zip',
'InProgress',
undefined,
authorId,
authorId ?? '',
);
const blobParam = makeBlobstorageServiceMockValue();
@ -594,6 +605,7 @@ describe('テンプレートファイルダウンロードURL取得', () => {
blobParam.fileExists = true;
const module = await makeTestingModuleWithBlob(source, blobParam);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
expect(
@ -606,6 +618,7 @@ describe('テンプレートファイルダウンロードURL取得', () => {
});
it('Typistの場合、タスクのステータスが[Inprogress,Pending]以外でエラー', async () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId, id: userId } = await makeTestUser(source, {
account_id: accountId,
@ -629,6 +642,7 @@ describe('テンプレートファイルダウンロードURL取得', () => {
blobParam.fileExists = true;
const module = await makeTestingModuleWithBlob(source, blobParam);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
await expect(
@ -643,6 +657,7 @@ describe('テンプレートファイルダウンロードURL取得', () => {
});
it('Typistの場合、自身が担当するタスクでない場合エラー', async () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId } = await makeTestUser(source, {
account_id: accountId,
@ -672,6 +687,7 @@ describe('テンプレートファイルダウンロードURL取得', () => {
blobParam.fileExists = true;
const module = await makeTestingModuleWithBlob(source, blobParam);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
await expect(
@ -686,6 +702,7 @@ describe('テンプレートファイルダウンロードURL取得', () => {
});
it('Authorの場合、自身が登録したタスクでない場合エラー', async () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId } = await makeTestUser(source, {
account_id: accountId,
@ -710,6 +727,7 @@ describe('テンプレートファイルダウンロードURL取得', () => {
blobParam.fileExists = true;
const module = await makeTestingModuleWithBlob(source, blobParam);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
await expect(
@ -724,6 +742,7 @@ describe('テンプレートファイルダウンロードURL取得', () => {
});
it('Taskが存在しない場合はエラーとなる', async () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId } = await makeTestUser(source, {
account_id: accountId,
@ -735,6 +754,7 @@ describe('テンプレートファイルダウンロードURL取得', () => {
const blobParam = makeBlobstorageServiceMockValue();
const module = await makeTestingModuleWithBlob(source, blobParam);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
await expect(
@ -749,6 +769,7 @@ describe('テンプレートファイルダウンロードURL取得', () => {
});
it('blobストレージにファイルが存在しない場合はエラーとなる', async () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId, author_id: authorId } = await makeTestUser(
source,
@ -768,7 +789,7 @@ describe('テンプレートファイルダウンロードURL取得', () => {
'test.zip',
'InProgress',
undefined,
authorId,
authorId ?? '',
);
const blobParam = makeBlobstorageServiceMockValue();
@ -776,6 +797,7 @@ describe('テンプレートファイルダウンロードURL取得', () => {
blobParam.fileExists = false;
const module = await makeTestingModuleWithBlob(source, blobParam);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
await expect(
@ -791,7 +813,7 @@ describe('テンプレートファイルダウンロードURL取得', () => {
});
describe('publishTemplateFileUploadSas', () => {
let source: DataSource = null;
let source: DataSource | null = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
@ -804,12 +826,15 @@ describe('publishTemplateFileUploadSas', () => {
});
afterEach(async () => {
if (!source) return;
await source.destroy();
source = null;
});
it('テンプレートファイルアップロードSASトークンが乗っているURLを取得できる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
@ -832,7 +857,9 @@ describe('publishTemplateFileUploadSas', () => {
});
it('blobストレージにコンテナが存在しない場合はエラーとなる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
// 第五階層のアカウント作成
const { admin } = await makeTestAccount(source, { tier: 5 });
@ -858,7 +885,9 @@ describe('publishTemplateFileUploadSas', () => {
});
it('SASトークンの取得に失敗した場合はエラーとなる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
// 第五階層のアカウント作成
const { admin } = await makeTestAccount(source, { tier: 5 });
@ -887,7 +916,7 @@ describe('publishTemplateFileUploadSas', () => {
});
describe('templateUploadFinished', () => {
let source: DataSource = null;
let source: DataSource | null = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
@ -900,12 +929,15 @@ describe('templateUploadFinished', () => {
});
afterEach(async () => {
if (!source) return;
await source.destroy();
source = null;
});
it('アップロード完了後のテンプレートファイル情報をDBに保存できる新規追加', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
@ -937,7 +969,9 @@ describe('templateUploadFinished', () => {
});
it('アップロード完了後のテンプレートファイル情報をDBに保存できる更新', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });
@ -975,7 +1009,9 @@ describe('templateUploadFinished', () => {
});
it('DBへの保存に失敗した場合はエラーとなる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<FilesService>(FilesService);
// 第五階層のアカウント作成
const { account, admin } = await makeTestAccount(source, { tier: 5 });

View File

@ -24,6 +24,7 @@ import {
} from '../../repositories/tasks/errors/types';
import { Context } from '../../common/log';
import { TemplateFilesRepositoryService } from '../../repositories/template_files/template_files.repository.service';
import { AccountNotFoundError } from '../../repositories/accounts/errors/types';
@Injectable()
export class FilesService {
@ -206,7 +207,7 @@ export class FilesService {
*/
async publishUploadSas(
context: Context,
token: AccessToken,
externalId: string,
): Promise<string> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.publishUploadSas.name}`,
@ -216,10 +217,11 @@ export class FilesService {
let accountId: number;
let country: string;
try {
const user = await this.usersRepository.findUserByExternalId(
token.userId,
);
accountId = user.account.id;
const user = await this.usersRepository.findUserByExternalId(externalId);
if (!user.account) {
throw new AccountNotFoundError('account not found.');
}
accountId = user.account_id;
country = user.account.country;
} catch (e) {
this.logger.error(`error=${e}`);
@ -291,14 +293,17 @@ export class FilesService {
let userId: number;
let country: string;
let isTypist: boolean;
let authorId: string;
let authorId: string | undefined;
try {
const user = await this.usersRepository.findUserByExternalId(externalId);
if (!user.account) {
throw new AccountNotFoundError('account not found.');
}
accountId = user.account.id;
userId = user.id;
country = user.account.country;
isTypist = user.role === USER_ROLES.TYPIST;
authorId = user.author_id;
authorId = user.author_id ?? undefined;
} catch (e) {
this.logger.error(`error=${e}`);
@ -321,7 +326,7 @@ export class FilesService {
accountId,
status,
);
const file = task.file;
const { file } = task;
// タスクに紐づく音声ファイルだけが消される場合がある。
// その場合はダウンロード不可なので不在エラーとして扱う
@ -332,9 +337,9 @@ export class FilesService {
}
// ユーザーがAuthorの場合、自身が追加したタスクでない場合はエラー
if (!isTypist && task.file.author_id !== authorId) {
if (!isTypist && file.author_id !== authorId) {
throw new AuthorUserNotMatchError(
`task author is not match. audio_file_id:${audioFileId}, task.file.author_id:${task.file.author_id}, authorId:${authorId}`,
`task author is not match. audio_file_id:${audioFileId}, task.file.author_id:${file.author_id}, authorId:${authorId}`,
);
}
@ -425,14 +430,17 @@ export class FilesService {
let userId: number;
let country: string;
let isTypist: boolean;
let authorId: string;
let authorId: string | undefined;
try {
const user = await this.usersRepository.findUserByExternalId(externalId);
accountId = user.account.id;
if (!user.account) {
throw new AccountNotFoundError('account not found.');
}
accountId = user.account_id;
userId = user.id;
country = user.account.country;
isTypist = user.role === USER_ROLES.TYPIST;
authorId = user.author_id;
authorId = user.author_id ?? undefined;
} catch (e) {
this.logger.error(`error=${e}`);
this.logger.log(
@ -454,6 +462,15 @@ export class FilesService {
accountId,
status,
);
const { file } = task;
// タスクに紐づく音声ファイルだけが消される場合がある。
// その場合はダウンロード不可なので不在エラーとして扱う
if (!file) {
throw new AudioFileNotFoundError(
`Audio file is not exists in DB. audio_file_id:${audioFileId}`,
);
}
const template_file = task.template_file;
@ -466,9 +483,9 @@ export class FilesService {
}
// ユーザーがAuthorの場合、自身が追加したタスクでない場合はエラー
if (!isTypist && task.file.author_id !== authorId) {
if (!isTypist && file.author_id !== authorId) {
throw new AuthorUserNotMatchError(
`task author is not match. audio_file_id:${audioFileId}, task.file.author_id:${task.file.author_id}, authorId:${authorId}`,
`task author is not match. audio_file_id:${audioFileId}, task.file.author_id:${file.author_id}, authorId:${authorId}`,
);
}
@ -515,6 +532,7 @@ export class FilesService {
makeErrorResponse('E010603'),
HttpStatus.BAD_REQUEST,
);
case AudioFileNotFoundError:
case TemplateFileNotFoundError:
throw new HttpException(
makeErrorResponse('E010701'),
@ -552,15 +570,18 @@ export class FilesService {
`[IN] [${context.trackingId}] ${this.publishTemplateFileUploadSas.name} | params: { externalId: ${externalId} };`,
);
try {
const {
account: { id: accountId, country },
} = await this.usersRepository.findUserByExternalId(externalId);
const { account } = await this.usersRepository.findUserByExternalId(
externalId,
);
if (!account) {
throw new AccountNotFoundError('account not found.');
}
// 国に応じたリージョンのBlobストレージにコンテナが存在するか確認
const isContainerExists = await this.blobStorageService.containerExists(
context,
accountId,
country,
account.id,
account.country,
);
if (!isContainerExists) {
throw new Error('container not found.');
@ -569,8 +590,8 @@ export class FilesService {
// SASトークン発行
const url = await this.blobStorageService.publishTemplateUploadSas(
context,
accountId,
country,
account.id,
account.country,
);
return url;

View File

@ -134,12 +134,15 @@ export const makeDefaultUsersRepositoryMockValue =
created_by: 'test',
created_at: new Date(),
updated_by: null,
updated_at: null,
updated_at: new Date(),
auto_renew: true,
license_alert: true,
notification: true,
encryption: false,
prompt: false,
encryption_password: null,
license: null,
userGroupMembers: null,
account: {
id: 2,
parent_account_id: 2,
@ -154,7 +157,10 @@ export const makeDefaultUsersRepositoryMockValue =
created_by: '',
created_at: new Date(),
updated_by: '',
updated_at: null,
updated_at: new Date(),
active_worktype_id: null,
secondary_admin_user_id: null,
user: null,
},
},
};
@ -172,6 +178,14 @@ export const makeDefaultTasksRepositoryMockValue =
status: 'Uploaded',
priority: '01',
created_at: new Date(),
finished_at: null,
started_at: null,
template_file_id: null,
typist_user_id: null,
file: null,
option_items: null,
template_file: null,
typist_user: null,
},
getTasksFromAccountId: {
tasks: [],

View File

@ -98,7 +98,7 @@ export const createTask = async (
export const makeTestingModuleWithBlob = async (
datasource: DataSource,
blobStorageService: BlobstorageServiceMockValue,
): Promise<TestingModule> => {
): Promise<TestingModule | undefined> => {
try {
const module: TestingModule = await Test.createTestingModule({
imports: [

View File

@ -2,6 +2,7 @@ import {
Body,
Controller,
Get,
HttpException,
HttpStatus,
Post,
Req,
@ -34,6 +35,7 @@ import { RoleGuard } from '../../common/guards/role/roleguards';
import { ADMIN_ROLES, TIERS } from '../../constants';
import jwt from 'jsonwebtoken';
import { makeContext } from '../../common/log';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
@ApiTags('licenses')
@Controller('licenses')
@ -73,12 +75,25 @@ export class LicensesController {
@Req() req: Request,
@Body() body: CreateOrdersRequest,
): Promise<CreateOrdersResponse> {
const accessToken = retrieveAuthorizationToken(req);
const payload = jwt.decode(accessToken, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
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;
// ライセンス注文処理
await this.licensesService.licenseOrders(
payload,
userId,
body.poNumber,
body.orderCount,
);
@ -111,11 +126,24 @@ export class LicensesController {
@Req() req: Request,
@Body() body: IssueCardLicensesRequest,
): Promise<IssueCardLicensesResponse> {
const accessToken = retrieveAuthorizationToken(req);
const payload = jwt.decode(accessToken, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
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 cardLicenseKeys = await this.licensesService.issueCardLicenseKeys(
payload.userId,
userId,
body.createCount,
);
@ -154,11 +182,24 @@ export class LicensesController {
@Req() req: Request,
@Body() body: ActivateCardLicensesRequest,
): Promise<ActivateCardLicensesResponse> {
const accessToken = retrieveAuthorizationToken(req);
const payload = jwt.decode(accessToken, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
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;
await this.licensesService.activateCardLicenseKey(
payload.userId,
userId,
body.cardLicenseKey,
);
@ -194,16 +235,26 @@ export class LicensesController {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@Req() req: Request,
): Promise<GetAllocatableLicensesResponse> {
const token = retrieveAuthorizationToken(req);
const payload = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
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(payload.userId);
const context = makeContext(userId);
const allocatableLicenses =
await this.licensesService.getAllocatableLicenses(
context,
payload.userId,
);
await this.licensesService.getAllocatableLicenses(context, userId);
return allocatableLicenses;
}
@ -245,16 +296,25 @@ export class LicensesController {
@Req() req: Request,
@Body() body: CancelOrderRequest,
): Promise<CancelOrderResponse> {
const token = retrieveAuthorizationToken(req);
const payload = jwt.decode(token, { json: true }) as AccessToken;
const context = makeContext(payload.userId);
await this.licensesService.cancelOrder(
context,
payload.userId,
body.poNumber,
const accessToken = retrieveAuthorizationToken(req) as string;
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.licensesService.cancelOrder(context, userId, body.poNumber);
return {};
}
}

View File

@ -56,11 +56,11 @@ describe('LicensesService', () => {
accountsRepositoryMockValue,
);
const body = new CreateOrdersRequest();
const token: AccessToken = { userId: '0001', role: '', tier: 5 };
const userId = '0001';
body.orderCount = 1000;
body.poNumber = '1';
expect(
await service.licenseOrders(token, body.poNumber, body.orderCount),
await service.licenseOrders(userId, body.poNumber, body.orderCount),
).toEqual(undefined);
});
it('ユーザID取得できなかった場合、エラーとなる', async () => {
@ -78,11 +78,11 @@ describe('LicensesService', () => {
accountsRepositoryMockValue,
);
const body = new CreateOrdersRequest();
const token: AccessToken = { userId: '', role: '', tier: 5 };
const userId = '';
body.orderCount = 1000;
body.poNumber = '1';
await expect(
service.licenseOrders(token, body.poNumber, body.orderCount),
service.licenseOrders(userId, body.poNumber, body.orderCount),
).rejects.toEqual(
new HttpException(
makeErrorResponse('E009999'),
@ -105,11 +105,11 @@ describe('LicensesService', () => {
accountsRepositoryMockValue,
);
const body = new CreateOrdersRequest();
const token: AccessToken = { userId: '0001', role: '', tier: 5 };
const userId = '0001';
body.orderCount = 1000;
body.poNumber = '1';
await expect(
service.licenseOrders(token, body.poNumber, body.orderCount),
service.licenseOrders(userId, body.poNumber, body.orderCount),
).rejects.toEqual(
new HttpException(
makeErrorResponse('E009999'),
@ -130,11 +130,11 @@ describe('LicensesService', () => {
accountsRepositoryMockValue,
);
const body = new CreateOrdersRequest();
const token: AccessToken = { userId: '0001', role: '', tier: 5 };
const userId = '0001';
body.orderCount = 1000;
body.poNumber = '1';
await expect(
service.licenseOrders(token, body.poNumber, body.orderCount),
service.licenseOrders(userId, body.poNumber, body.orderCount),
).rejects.toEqual(
new HttpException(
makeErrorResponse('E010401'),
@ -154,7 +154,7 @@ describe('LicensesService', () => {
accountsRepositoryMockValue,
);
const body = new IssueCardLicensesRequest();
const token: AccessToken = { userId: '0001', role: '', tier: 5 };
const userId = '0001';
body.createCount = 10;
const issueCardLicensesResponse: IssueCardLicensesResponse = {
cardLicenseKeys: [
@ -171,7 +171,7 @@ describe('LicensesService', () => {
],
};
expect(
await service.issueCardLicenseKeys(token.userId, body.createCount),
await service.issueCardLicenseKeys(userId, body.createCount),
).toEqual(issueCardLicensesResponse);
});
it('カードライセンス発行に失敗した場合、エラーになる', async () => {
@ -187,10 +187,10 @@ describe('LicensesService', () => {
accountsRepositoryMockValue,
);
const body = new IssueCardLicensesRequest();
const token: AccessToken = { userId: '0001', role: '', tier: 5 };
const userId = '0001';
body.createCount = 1000;
await expect(
service.issueCardLicenseKeys(token.userId, body.createCount),
service.issueCardLicenseKeys(userId, body.createCount),
).rejects.toEqual(
new HttpException(
makeErrorResponse('E009999'),
@ -210,10 +210,10 @@ describe('LicensesService', () => {
accountsRepositoryMockValue,
);
const body = new ActivateCardLicensesRequest();
const token: AccessToken = { userId: '0001', role: '', tier: 5 };
const userId = '0001';
body.cardLicenseKey = 'WZCETXC0Z9PQZ9GKRGGY';
expect(
await service.activateCardLicenseKey(token.userId, body.cardLicenseKey),
await service.activateCardLicenseKey(userId, body.cardLicenseKey),
).toEqual(undefined);
});
it('カードライセンス取り込みに失敗した場合、エラーになるDBエラー', async () => {
@ -229,10 +229,10 @@ describe('LicensesService', () => {
accountsRepositoryMockValue,
);
const body = new ActivateCardLicensesRequest();
const token: AccessToken = { userId: '0001', role: '', tier: 5 };
const userId = '0001';
body.cardLicenseKey = 'WZCETXC0Z9PQZ9GKRGGY';
await expect(
service.activateCardLicenseKey(token.userId, body.cardLicenseKey),
service.activateCardLicenseKey(userId, body.cardLicenseKey),
).rejects.toEqual(
new HttpException(
makeErrorResponse('E009999'),
@ -254,10 +254,10 @@ describe('LicensesService', () => {
accountsRepositoryMockValue,
);
const body = new ActivateCardLicensesRequest();
const token: AccessToken = { userId: '0001', role: '', tier: 5 };
const userId = '0001';
body.cardLicenseKey = 'WZCETXC0Z9PQZ9GKRGGY';
await expect(
service.activateCardLicenseKey(token.userId, body.cardLicenseKey),
service.activateCardLicenseKey(userId, body.cardLicenseKey),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010801'), HttpStatus.BAD_REQUEST),
);
@ -276,10 +276,10 @@ describe('LicensesService', () => {
accountsRepositoryMockValue,
);
const body = new ActivateCardLicensesRequest();
const token: AccessToken = { userId: '0001', role: '', tier: 5 };
const userId = '0001';
body.cardLicenseKey = 'WZCETXC0Z9PQZ9GKRGGY';
await expect(
service.activateCardLicenseKey(token.userId, body.cardLicenseKey),
service.activateCardLicenseKey(userId, body.cardLicenseKey),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010802'), HttpStatus.BAD_REQUEST),
);
@ -287,7 +287,7 @@ describe('LicensesService', () => {
});
describe('DBテスト', () => {
let source: DataSource = null;
let source: DataSource | null = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
@ -300,12 +300,15 @@ describe('DBテスト', () => {
});
afterEach(async () => {
if (!source) return;
await source.destroy();
source = null;
});
it('カードライセンス発行が完了する(発行数が合っているか確認)', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId } = await makeTestUser(source, {
@ -323,7 +326,9 @@ describe('DBテスト', () => {
});
it('カードライセンス取り込みが完了する', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId } = await makeTestUser(source, {
@ -362,13 +367,15 @@ describe('DBテスト', () => {
);
const dbSelectResultFromLicense = await selectLicense(source, license_id);
expect(
dbSelectResultFromCardLicense.cardLicense.activated_at,
dbSelectResultFromCardLicense?.cardLicense?.activated_at,
).toBeDefined();
expect(dbSelectResultFromLicense.license.account_id).toEqual(accountId);
expect(dbSelectResultFromLicense?.license?.account_id).toEqual(accountId);
});
it('取込可能なライセンスのみが取得できる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const now = new Date();
const { id: accountId } = await makeTestSimpleAccount(source);
@ -513,7 +520,7 @@ describe('DBテスト', () => {
});
describe('ライセンス割り当て', () => {
let source: DataSource = null;
let source: DataSource | null = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
@ -526,12 +533,15 @@ describe('ライセンス割り当て', () => {
});
afterEach(async () => {
if (!source) return;
await source.destroy();
source = null;
});
it('未割当のライセンスに対して、ライセンス割り当てが完了する', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: userId } = await makeTestUser(source, {
@ -567,11 +577,11 @@ describe('ライセンス割り当て', () => {
await service.allocateLicense(makeContext('trackingId'), userId, 1);
const resultLicense = await selectLicense(source, 1);
expect(resultLicense.license.allocated_user_id).toBe(userId);
expect(resultLicense.license.status).toBe(
expect(resultLicense.license?.allocated_user_id).toBe(userId);
expect(resultLicense.license?.status).toBe(
LICENSE_ALLOCATED_STATUS.ALLOCATED,
);
expect(resultLicense.license.expiry_date.setMilliseconds(0)).toEqual(
expect(resultLicense.license?.expiry_date?.setMilliseconds(0)).toEqual(
expiry_date.setMilliseconds(0),
);
const licenseAllocationHistory = await selectLicenseAllocationHistory(
@ -579,22 +589,24 @@ describe('ライセンス割り当て', () => {
userId,
1,
);
expect(licenseAllocationHistory.licenseAllocationHistory.user_id).toBe(
expect(licenseAllocationHistory.licenseAllocationHistory?.user_id).toBe(
userId,
);
expect(licenseAllocationHistory.licenseAllocationHistory.license_id).toBe(
expect(licenseAllocationHistory.licenseAllocationHistory?.license_id).toBe(
1,
);
expect(licenseAllocationHistory.licenseAllocationHistory.is_allocated).toBe(
true,
);
expect(licenseAllocationHistory.licenseAllocationHistory.account_id).toBe(
expect(
licenseAllocationHistory.licenseAllocationHistory?.is_allocated,
).toBe(true);
expect(licenseAllocationHistory.licenseAllocationHistory?.account_id).toBe(
accountId,
);
});
it('再割り当て可能なライセンスに対して、ライセンス割り当てが完了する', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: userId } = await makeTestUser(source, {
@ -630,30 +642,32 @@ describe('ライセンス割り当て', () => {
await service.allocateLicense(makeContext('trackingId'), userId, 1);
const result = await selectLicense(source, 1);
expect(result.license.allocated_user_id).toBe(userId);
expect(result.license.status).toBe(LICENSE_ALLOCATED_STATUS.ALLOCATED);
expect(result.license.expiry_date).toEqual(date);
expect(result.license?.allocated_user_id).toBe(userId);
expect(result.license?.status).toBe(LICENSE_ALLOCATED_STATUS.ALLOCATED);
expect(result.license?.expiry_date).toEqual(date);
const licenseAllocationHistory = await selectLicenseAllocationHistory(
source,
userId,
1,
);
expect(licenseAllocationHistory.licenseAllocationHistory.user_id).toBe(
expect(licenseAllocationHistory.licenseAllocationHistory?.user_id).toBe(
userId,
);
expect(licenseAllocationHistory.licenseAllocationHistory.license_id).toBe(
expect(licenseAllocationHistory.licenseAllocationHistory?.license_id).toBe(
1,
);
expect(licenseAllocationHistory.licenseAllocationHistory.is_allocated).toBe(
true,
);
expect(licenseAllocationHistory.licenseAllocationHistory.account_id).toBe(
expect(
licenseAllocationHistory.licenseAllocationHistory?.is_allocated,
).toBe(true);
expect(licenseAllocationHistory.licenseAllocationHistory?.account_id).toBe(
accountId,
);
});
it('未割当のライセンスに対して、別のライセンスが割り当てられているユーザーの割り当てが完了する', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: userId } = await makeTestUser(source, {
@ -705,32 +719,32 @@ describe('ライセンス割り当て', () => {
// もともと割り当てられていたライセンスの状態確認
const result1 = await selectLicense(source, 1);
expect(result1.license.allocated_user_id).toBe(null);
expect(result1.license.status).toBe(LICENSE_ALLOCATED_STATUS.REUSABLE);
expect(result1.license.expiry_date).toEqual(date);
expect(result1.license?.allocated_user_id).toBe(null);
expect(result1.license?.status).toBe(LICENSE_ALLOCATED_STATUS.REUSABLE);
expect(result1.license?.expiry_date).toEqual(date);
const licenseAllocationHistory = await selectLicenseAllocationHistory(
source,
userId,
1,
);
expect(licenseAllocationHistory.licenseAllocationHistory.user_id).toBe(
expect(licenseAllocationHistory.licenseAllocationHistory?.user_id).toBe(
userId,
);
expect(licenseAllocationHistory.licenseAllocationHistory.license_id).toBe(
expect(licenseAllocationHistory.licenseAllocationHistory?.license_id).toBe(
1,
);
expect(licenseAllocationHistory.licenseAllocationHistory.is_allocated).toBe(
false,
);
expect(licenseAllocationHistory.licenseAllocationHistory.account_id).toBe(
expect(
licenseAllocationHistory.licenseAllocationHistory?.is_allocated,
).toBe(false);
expect(licenseAllocationHistory.licenseAllocationHistory?.account_id).toBe(
accountId,
);
// 新たに割り当てたライセンスの状態確認
const result2 = await selectLicense(source, 2);
expect(result2.license.allocated_user_id).toBe(userId);
expect(result2.license.status).toBe(LICENSE_ALLOCATED_STATUS.ALLOCATED);
expect(result2.license.expiry_date.setMilliseconds(0)).toEqual(
expect(result2.license?.allocated_user_id).toBe(userId);
expect(result2.license?.status).toBe(LICENSE_ALLOCATED_STATUS.ALLOCATED);
expect(result2.license?.expiry_date?.setMilliseconds(0)).toEqual(
expiry_date.setMilliseconds(0),
);
const newlicenseAllocationHistory = await selectLicenseAllocationHistory(
@ -738,22 +752,24 @@ describe('ライセンス割り当て', () => {
userId,
2,
);
expect(newlicenseAllocationHistory.licenseAllocationHistory.user_id).toBe(
expect(newlicenseAllocationHistory.licenseAllocationHistory?.user_id).toBe(
userId,
);
expect(
newlicenseAllocationHistory.licenseAllocationHistory.license_id,
newlicenseAllocationHistory.licenseAllocationHistory?.license_id,
).toBe(2);
expect(
newlicenseAllocationHistory.licenseAllocationHistory.is_allocated,
newlicenseAllocationHistory.licenseAllocationHistory?.is_allocated,
).toBe(true);
expect(
newlicenseAllocationHistory.licenseAllocationHistory.account_id,
newlicenseAllocationHistory.licenseAllocationHistory?.account_id,
).toBe(accountId);
});
it('割り当て時にライセンス履歴テーブルへの登録が完了する元がNORMALのとき', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: userId } = await makeTestUser(source, {
@ -806,12 +822,14 @@ describe('ライセンス割り当て', () => {
2,
);
expect(
licenseAllocationHistory.licenseAllocationHistory.switch_from_type,
licenseAllocationHistory.licenseAllocationHistory?.switch_from_type,
).toBe('NONE');
});
it('割り当て時にライセンス履歴テーブルへの登録が完了する元がCARDのとき', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: userId } = await makeTestUser(source, {
@ -864,12 +882,14 @@ describe('ライセンス割り当て', () => {
2,
);
expect(
licenseAllocationHistory.licenseAllocationHistory.switch_from_type,
licenseAllocationHistory.licenseAllocationHistory?.switch_from_type,
).toBe('CARD');
});
it('割り当て時にライセンス履歴テーブルへの登録が完了する元がTRIALのとき', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: userId } = await makeTestUser(source, {
@ -922,12 +942,14 @@ describe('ライセンス割り当て', () => {
2,
);
expect(
licenseAllocationHistory.licenseAllocationHistory.switch_from_type,
licenseAllocationHistory.licenseAllocationHistory?.switch_from_type,
).toBe('TRIAL');
});
it('有効期限が切れているライセンスを割り当てようとした場合、エラーになる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: userId } = await makeTestUser(source, {
@ -961,7 +983,9 @@ describe('ライセンス割り当て', () => {
});
it('割り当て不可なライセンスを割り当てようとした場合、エラーになる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: userId } = await makeTestUser(source, {
@ -1013,7 +1037,7 @@ describe('ライセンス割り当て', () => {
});
describe('ライセンス割り当て解除', () => {
let source: DataSource = null;
let source: DataSource | null = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
@ -1026,12 +1050,15 @@ describe('ライセンス割り当て解除', () => {
});
afterEach(async () => {
if (!source) return;
await source.destroy();
source = null;
});
it('ライセンスの割り当て解除が完了する', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: userId } = await makeTestUser(source, {
@ -1068,11 +1095,11 @@ describe('ライセンス割り当て解除', () => {
// 割り当て解除したライセンスの状態確認
const deallocatedLicense = await selectLicense(source, 1);
expect(deallocatedLicense.license.allocated_user_id).toBe(null);
expect(deallocatedLicense.license.status).toBe(
expect(deallocatedLicense.license?.allocated_user_id).toBe(null);
expect(deallocatedLicense.license?.status).toBe(
LICENSE_ALLOCATED_STATUS.REUSABLE,
);
expect(deallocatedLicense.license.expiry_date).toEqual(date);
expect(deallocatedLicense.license?.expiry_date).toEqual(date);
// ライセンス履歴テーブルの状態確認
const licenseAllocationHistory = await selectLicenseAllocationHistory(
@ -1080,25 +1107,27 @@ describe('ライセンス割り当て解除', () => {
userId,
1,
);
expect(licenseAllocationHistory.licenseAllocationHistory.user_id).toBe(
expect(licenseAllocationHistory.licenseAllocationHistory?.user_id).toBe(
userId,
);
expect(licenseAllocationHistory.licenseAllocationHistory.license_id).toBe(
expect(licenseAllocationHistory.licenseAllocationHistory?.license_id).toBe(
1,
);
expect(licenseAllocationHistory.licenseAllocationHistory.is_allocated).toBe(
false,
);
expect(licenseAllocationHistory.licenseAllocationHistory.account_id).toBe(
expect(
licenseAllocationHistory.licenseAllocationHistory?.is_allocated,
).toBe(false);
expect(licenseAllocationHistory.licenseAllocationHistory?.account_id).toBe(
accountId,
);
expect(
licenseAllocationHistory.licenseAllocationHistory.switch_from_type,
licenseAllocationHistory.licenseAllocationHistory?.switch_from_type,
).toBe('NONE');
});
it('ライセンスが既に割り当て解除されていた場合、エラーとなる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: userId } = await makeTestUser(source, {
@ -1158,7 +1187,7 @@ describe('ライセンス割り当て解除', () => {
});
describe('ライセンス注文キャンセル', () => {
let source: DataSource = null;
let source: DataSource | null = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
@ -1171,12 +1200,15 @@ describe('ライセンス注文キャンセル', () => {
});
afterEach(async () => {
if (!source) return;
await source.destroy();
source = null;
});
it('ライセンス注文のキャンセルが完了する', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { tier2Accounts: tier2Accounts } = await makeHierarchicalAccounts(
source,
);
@ -1185,7 +1217,7 @@ describe('ライセンス注文キャンセル', () => {
source,
poNumber,
tier2Accounts[0].account.id,
tier2Accounts[0].account.parent_account_id,
tier2Accounts[0].account.parent_account_id ?? 0,
null,
10,
'Issue Requesting',
@ -1195,7 +1227,7 @@ describe('ライセンス注文キャンセル', () => {
source,
poNumber,
tier2Accounts[0].account.id,
tier2Accounts[0].account.parent_account_id,
tier2Accounts[0].account.parent_account_id ?? 0,
null,
10,
'Order Canceled',
@ -1214,12 +1246,14 @@ describe('ライセンス注文キャンセル', () => {
tier2Accounts[0].account.id,
poNumber,
);
expect(orderRecord.orderLicense.canceled_at).toBeDefined();
expect(orderRecord.orderLicense.status).toBe('Order Canceled');
expect(orderRecord.orderLicense?.canceled_at).toBeDefined();
expect(orderRecord.orderLicense?.status).toBe('Order Canceled');
});
it('ライセンスが既に発行済みの場合、エラーとなる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { tier2Accounts: tier2Accounts } = await makeHierarchicalAccounts(
source,
);
@ -1228,7 +1262,7 @@ describe('ライセンス注文キャンセル', () => {
source,
poNumber,
tier2Accounts[0].account.id,
tier2Accounts[0].account.parent_account_id,
tier2Accounts[0].account.parent_account_id ?? 0,
null,
10,
'Issued',
@ -1247,7 +1281,9 @@ describe('ライセンス注文キャンセル', () => {
});
it('ライセンスが既にキャンセル済みの場合、エラーとなる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { tier2Accounts: tier2Accounts } = await makeHierarchicalAccounts(
source,
@ -1257,7 +1293,7 @@ describe('ライセンス注文キャンセル', () => {
source,
poNumber,
tier2Accounts[0].account.id,
tier2Accounts[0].account.parent_account_id,
tier2Accounts[0].account.parent_account_id ?? 0,
null,
10,
'Order Canceled',

View File

@ -33,20 +33,20 @@ export class LicensesService {
* @param body
*/
async licenseOrders(
accessToken: AccessToken,
externalId: string,
poNumber: string,
orderCount: number,
): Promise<void> {
//アクセストークンからユーザーIDを取得する
this.logger.log(`[IN] ${this.licenseOrders.name}`);
const userId = accessToken.userId;
let myAccountId: number;
let parentAccountId: number;
let parentAccountId: number | undefined;
// ユーザIDからアカウントIDを取得する
try {
myAccountId = (await this.usersRepository.findUserByExternalId(userId))
.account_id;
myAccountId = (
await this.usersRepository.findUserByExternalId(externalId)
).account_id;
} catch (e) {
this.logger.error(`error=${e}`);
switch (e.constructor) {
@ -65,9 +65,13 @@ export class LicensesService {
// 親アカウントIDを取得
try {
parentAccountId = (
await this.accountsRepository.findAccountById(myAccountId)
).parent_account_id;
parentAccountId =
(await this.accountsRepository.findAccountById(myAccountId))
.parent_account_id ?? undefined;
// 親アカウントIDが取得できない場合はエラー
if (parentAccountId === undefined) {
throw new Error('parent account id is undefined');
}
} catch (e) {
this.logger.error(`error=${e}`);
switch (e.constructor) {

View File

@ -124,11 +124,11 @@ export const makeDefaultUsersRepositoryMockValue =
user1.notification = false;
user1.encryption = false;
user1.prompt = false;
user1.deleted_at = undefined;
user1.deleted_at = null;
user1.created_by = 'test';
user1.created_at = new Date();
user1.updated_by = undefined;
user1.updated_at = undefined;
user1.updated_by = null;
user1.updated_at = new Date();
return {
findUserByExternalId: user1,

View File

@ -12,14 +12,14 @@ import {
export const createLicense = async (
datasource: DataSource,
licenseId: number,
expiry_date: Date,
expiry_date: Date | null,
accountId: number,
type: string,
status: string,
allocated_user_id: number,
order_id: number,
deleted_at: Date,
delete_order_id: number,
allocated_user_id: number | null,
order_id: number | null,
deleted_at: Date | null,
delete_order_id: number | null,
): Promise<void> => {
const { identifiers } = await datasource.getRepository(License).insert({
id: licenseId,
@ -107,7 +107,7 @@ export const createOrder = async (
poNumber: string,
fromId: number,
toId: number,
issuedAt: Date,
issuedAt: Date | null,
quantity: number,
status: string,
): Promise<void> => {
@ -138,7 +138,7 @@ export const selectCardLicensesCount = async (
export const selectCardLicense = async (
datasource: DataSource,
cardLicenseKey: string,
): Promise<{ cardLicense: CardLicense }> => {
): Promise<{ cardLicense: CardLicense | null }> => {
const cardLicense = await datasource.getRepository(CardLicense).findOne({
where: {
card_license_key: cardLicenseKey,
@ -150,7 +150,7 @@ export const selectCardLicense = async (
export const selectLicense = async (
datasource: DataSource,
id: number,
): Promise<{ license: License }> => {
): Promise<{ license: License | null }> => {
const license = await datasource.getRepository(License).findOne({
where: {
id: id,
@ -163,7 +163,7 @@ export const selectLicenseAllocationHistory = async (
datasource: DataSource,
userId: number,
licence_id: number,
): Promise<{ licenseAllocationHistory: LicenseAllocationHistory }> => {
): Promise<{ licenseAllocationHistory: LicenseAllocationHistory | null }> => {
const licenseAllocationHistory = await datasource
.getRepository(LicenseAllocationHistory)
.findOne({
@ -182,7 +182,7 @@ export const selectOrderLicense = async (
datasource: DataSource,
accountId: number,
poNumber: string,
): Promise<{ orderLicense: LicenseOrder }> => {
): Promise<{ orderLicense: LicenseOrder | null }> => {
const orderLicense = await datasource.getRepository(LicenseOrder).findOne({
where: {
from_account_id: accountId,

View File

@ -47,8 +47,8 @@ export class GetAllocatableLicensesRequest {}
export class AllocatableLicenseInfo {
@ApiProperty()
licenseId: number;
@ApiProperty()
expiryDate: Date;
@ApiProperty({ required: false })
expiryDate?: Date;
}
export class GetAllocatableLicensesResponse {
@ApiProperty({ type: [AllocatableLicenseInfo] })

View File

@ -77,14 +77,14 @@ export const makeDefaultUsersRepositoryMockValue =
user.external_id = 'external_id';
user.account_id = 123;
user.role = 'none';
user.author_id = undefined;
user.author_id = null;
user.accepted_eula_version = '1.0';
user.accepted_dpa_version = '1.0';
user.email_verified = true;
user.auto_renew = false;
user.license_alert = false;
user.notification = false;
user.deleted_at = undefined;
user.deleted_at = null;
user.created_by = 'test';
user.created_at = new Date();
user.updated_by = 'test';

View File

@ -3,6 +3,7 @@ import {
Controller,
Get,
Headers,
HttpException,
HttpStatus,
Param,
ParseIntPipe,
@ -32,6 +33,8 @@ import {
TasksResponse,
} from './types/types';
import {
SortDirection,
TaskListSortableAttribute,
isSortDirection,
isTaskListSortableAttribute,
} from '../../common/types/sort';
@ -43,6 +46,7 @@ import { RoleGuard } from '../../common/guards/role/roleguards';
import { ADMIN_ROLES, USER_ROLES } from '../../constants';
import { Roles } from '../../common/types/role';
import { makeContext } from '../../common/log';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
@ApiTags('tasks')
@Controller('tasks')
@ -80,22 +84,38 @@ export class TasksController {
@Req() req,
@Query() body: TasksRequest,
): Promise<TasksResponse> {
const accessToken = retrieveAuthorizationToken(req);
const decodedToken = jwt.decode(accessToken, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
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, role } = decodedAccessToken as AccessToken;
// RoleGuardでroleの文字列に想定外の文字列や重複がないことは担保されているためここでは型変換のみ行う
const roles = role.split(' ') as Roles[];
const context = makeContext(decodedToken.userId);
const context = makeContext(userId);
const { limit, offset, status } = body;
const paramName = isTaskListSortableAttribute(body.paramName)
? body.paramName
const paramName = isTaskListSortableAttribute(body.paramName ?? '')
? (body.paramName as TaskListSortableAttribute)
: undefined;
const direction = isSortDirection(body.direction)
? body.direction
const direction = isSortDirection(body.direction ?? '')
? (body.direction as SortDirection)
: undefined;
const { tasks, total } = await this.taskService.getTasks(
context,
decodedToken,
userId,
roles,
offset,
limit,
// statusが指定されていない場合は全てのステータスを取得する
@ -183,10 +203,22 @@ export class TasksController {
@Param() param: ChangeStatusRequest,
): Promise<ChangeStatusResponse> {
// AuthGuardでチェック済みなのでここでのアクセストークンチェックはしない
const accessToken = retrieveAuthorizationToken(req);
const { role, userId } = jwt.decode(accessToken, {
json: true,
}) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
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, role } = decodedAccessToken as AccessToken;
// RoleGuardでroleの文字列に想定外の文字列や重複がないことは担保されているためここでは型変換のみ行う
const roles = role.split(' ') as Roles[];
@ -241,10 +273,22 @@ export class TasksController {
): Promise<ChangeStatusResponse> {
const { audioFileId } = params;
// AuthGuardでチェック済みなのでここでのアクセストークンチェックはしない
const accessToken = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(accessToken, {
json: true,
}) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
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, role } = decodedAccessToken as AccessToken;
const context = makeContext(userId);
@ -296,10 +340,22 @@ export class TasksController {
): Promise<ChangeStatusResponse> {
const { audioFileId } = params;
// AuthGuardでチェック済みなのでここでのアクセストークンチェックはしない
const accessToken = retrieveAuthorizationToken(req);
const { userId, role } = jwt.decode(accessToken, {
json: true,
}) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
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, role } = decodedAccessToken as AccessToken;
// RoleGuardでroleの文字列に想定外の文字列や重複がないことは担保されているためここでは型変換のみ行う
const roles = role.split(' ') as Roles[];
@ -353,10 +409,22 @@ export class TasksController {
): Promise<ChangeStatusResponse> {
const { audioFileId } = params;
// AuthGuardでチェック済みなのでここでのアクセストークンチェックはしない
const accessToken = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(accessToken, {
json: true,
}) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
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);
@ -491,11 +559,22 @@ export class TasksController {
@Body() body: PostCheckoutPermissionRequest,
): Promise<PostCheckoutPermissionResponse> {
const { assignees } = body;
const accessToken = retrieveAuthorizationToken(req);
const { role, userId } = jwt.decode(accessToken, {
json: true,
}) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
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, role } = decodedAccessToken as AccessToken;
// RoleGuardでroleの文字列に想定外の文字列や重複がないことは担保されているためここでは型変換のみ行う
const roles = role.split(' ') as Roles[];

File diff suppressed because it is too large Load Diff

View File

@ -45,10 +45,10 @@ export class TasksService {
private readonly notificationhubService: NotificationhubService,
) {}
// TODO [Task2244] 引数にAccessTokenがあるのは不適切なのでController側で分解したい
async getTasks(
context: Context,
accessToken: AccessToken,
userId: string,
roles: Roles[],
offset: number,
limit: number,
status?: string[],
@ -59,10 +59,6 @@ export class TasksService {
`[IN] [${context.trackingId}] ${this.getTasks.name} | params: { offset: ${offset}, limit: ${limit}, status: ${status}, paramName: ${paramName}, direction: ${direction} };`,
);
const { role, userId } = accessToken;
// TODO [Task2244] Roleに型で定義されている値が入っているかをチェックして異常値を弾く実装に修正する
const roles = role.split(' ');
// パラメータが省略された場合のデフォルト値: 保存するソート条件の値の初期値と揃える
const defaultParamName: TaskListSortableAttribute = 'JOB_NUMBER';
const defaultDirection: SortDirection = 'ASC';
@ -95,6 +91,10 @@ export class TasksService {
return { tasks: tasks, total: result.count };
}
if (roles.includes(USER_ROLES.AUTHOR)) {
// API実行者がAuthorで、AuthorIDが存在しないことは想定外のため、エラーとする
if (!author_id) {
throw new Error('AuthorID not found');
}
const result =
await this.taskRepository.getTasksFromAuthorIdAndAccountId(
author_id,
@ -179,6 +179,10 @@ export class TasksService {
await this.usersRepository.findUserByExternalId(externalId);
if (roles.includes(USER_ROLES.AUTHOR)) {
// API実行者がAuthorで、AuthorIDが存在しないことは想定外のため、エラーとする
if (!author_id) {
throw new Error('AuthorID not found');
}
await this.taskRepository.getTaskFromAudioFileId(
audioFileId,
account_id,
@ -407,30 +411,21 @@ export class TasksService {
permissions: CheckoutPermission[],
): Promise<AdB2cUser[]> {
// 割り当て候補の外部IDを列挙
const assigneesExternalIds = permissions.map((x) => {
if (x.user) {
return x.user.external_id;
}
});
const assigneesExternalIds = permissions.flatMap((permission) =>
permission.user ? [permission.user.external_id] : [],
);
// 割り当てられているタイピストの外部IDを列挙
const typistExternalIds = tasks.flatMap((x) => {
if (x.typist_user) {
return x.typist_user.external_id;
}
});
const typistExternalIds = tasks.flatMap((task) =>
task.typist_user ? [task.typist_user.external_id] : [],
);
//重複をなくす
const distinctedExternalIds = [
...new Set(assigneesExternalIds.concat(typistExternalIds)),
];
// undefinedがあった場合、取り除く
const filteredExternalIds: string[] = distinctedExternalIds.filter(
(x): x is string => x !== undefined,
);
// B2Cからユーザー名を取得する
return await this.adB2cService.getUsers(context, filteredExternalIds);
return await this.adB2cService.getUsers(context, distinctedExternalIds);
}
/**
*
@ -451,10 +446,14 @@ export class TasksService {
);
const { author_id, account_id } =
await this.usersRepository.findUserByExternalId(externalId);
// RoleがAuthorで、AuthorIDが存在しないことは想定外のため、エラーとする
if (role.includes(USER_ROLES.AUTHOR) && !author_id) {
throw new Error('AuthorID not found');
}
await this.taskRepository.changeCheckoutPermission(
audioFileId,
author_id,
author_id ?? undefined,
account_id,
role,
assignees,
@ -462,11 +461,16 @@ export class TasksService {
// すべての割り当て候補ユーザーを取得する
const assigneesGroupIds = assignees
.filter((x) => x.typistGroupId)
.map((x) => x.typistGroupId);
.filter((assignee) => assignee.typistGroupId)
.flatMap((assignee) =>
assignee.typistGroupId ? [assignee.typistGroupId] : [],
);
const assigneesUserIds = assignees
.filter((x) => x.typistUserId)
.map((x) => x.typistUserId);
.filter((assignee) => assignee.typistUserId)
.flatMap((assignee) =>
assignee.typistUserId ? [assignee.typistUserId] : [],
);
const groupMembers =
await this.userGroupsRepositoryService.getGroupMembersFromGroupIds(

View File

@ -263,6 +263,11 @@ export const makeDefaultUserGroupsRepositoryMockValue =
user_id: 1,
created_by: 'test',
updated_by: 'test',
created_at: new Date(),
deleted_at: null,
updated_at: null,
user: null,
userGroup: null,
},
{
id: 2,
@ -270,6 +275,11 @@ export const makeDefaultUserGroupsRepositoryMockValue =
user_id: 2,
created_by: 'test',
updated_by: 'test',
created_at: new Date(),
deleted_at: null,
updated_at: null,
user: null,
userGroup: null,
},
{
id: 3,
@ -277,6 +287,11 @@ export const makeDefaultUserGroupsRepositoryMockValue =
user_id: 1,
created_by: 'test',
updated_by: 'test',
created_at: new Date(),
deleted_at: null,
updated_at: null,
user: null,
userGroup: null,
},
{
id: 4,
@ -284,6 +299,11 @@ export const makeDefaultUserGroupsRepositoryMockValue =
user_id: 1,
created_by: 'test',
updated_by: 'test',
created_at: new Date(),
deleted_at: null,
updated_at: null,
user: null,
userGroup: null,
},
{
id: 5,
@ -291,6 +311,11 @@ export const makeDefaultUserGroupsRepositoryMockValue =
user_id: 3,
created_by: 'test',
updated_by: 'test',
created_at: new Date(),
deleted_at: null,
updated_at: null,
user: null,
userGroup: null,
},
],
};
@ -307,7 +332,7 @@ export const makeDefaultUsersRepositoryMockValue =
user1.auto_renew = false;
user1.license_alert = false;
user1.notification = false;
user1.deleted_at = undefined;
user1.deleted_at = null;
user1.created_by = 'test';
user1.created_at = new Date();
user1.author_id = 'abcdef';
@ -331,66 +356,82 @@ const defaultTasksRepositoryMockValue: {
status: 'Uploaded',
priority: '00',
created_at: new Date('2023-01-01T01:01:01.000Z'),
finished_at: null,
started_at: null,
typist_user_id: null,
template_file_id: null,
typist_user: null,
template_file: null,
option_items: [
{
id: 1,
audio_file_id: 1,
label: 'label01',
value: 'value01',
task: null,
},
{
id: 2,
audio_file_id: 1,
label: 'label02',
value: 'value02',
task: null,
},
{
id: 3,
audio_file_id: 1,
label: 'label03',
value: 'value03',
task: null,
},
{
id: 4,
audio_file_id: 1,
label: 'label04',
value: 'value04',
task: null,
},
{
id: 5,
audio_file_id: 1,
label: 'label05',
value: 'value05',
task: null,
},
{
id: 6,
audio_file_id: 1,
label: 'label06',
value: 'value06',
task: null,
},
{
id: 7,
audio_file_id: 1,
label: 'label07',
value: 'value07',
task: null,
},
{
id: 8,
audio_file_id: 1,
label: 'label08',
value: 'value08',
task: null,
},
{
id: 9,
audio_file_id: 1,
label: 'label09',
value: 'value09',
task: null,
},
{
id: 10,
audio_file_id: 1,
label: 'label10',
value: 'value10',
task: null,
},
],
file: {
@ -410,6 +451,8 @@ const defaultTasksRepositoryMockValue: {
audio_format: 'DS',
comment: 'comment',
is_encrypted: true,
deleted_at: null,
task: null,
},
},
],
@ -435,7 +478,16 @@ const defaultTasksRepositoryMockValue: {
created_at: new Date(),
updated_by: 'test',
updated_at: new Date(),
account: null,
author_id: null,
deleted_at: null,
encryption_password: null,
license: null,
userGroupMembers: null,
},
task: null,
user_group_id: null,
user_group: null,
},
],
count: 1,

View File

@ -39,10 +39,10 @@ import {
makeNotificationhubServiceMock,
} from './tasks.service.mock';
export const makeTaskTestingModule = async (
export const makeTaskTestingModuleWithNotificaiton = async (
datasource: DataSource,
notificationhubServiceMockValue: NotificationhubServiceMockValue,
): Promise<TestingModule> => {
): Promise<TestingModule | undefined> => {
try {
const module: TestingModule = await Test.createTestingModule({
imports: [
@ -205,7 +205,7 @@ export const createUserGroup = async (
export const getTask = async (
datasource: DataSource,
task_id: number,
): Promise<Task> => {
): Promise<Task | null> => {
const task = await datasource.getRepository(Task).findOne({
where: {
id: task_id,

View File

@ -45,7 +45,7 @@ const createTask = (
const assignees = createAssignees(permissions, b2cUserInfo);
// RepositoryDTO => ControllerDTOに変換
const typist: Typist =
const typist: Typist | undefined =
typist_user != null
? convertUserToTypist(typist_user, b2cUserInfo)
: undefined;
@ -113,7 +113,10 @@ const convertUserToAssignee = (
): Assignee => {
const typistName = b2cUserInfo.find(
(x) => x.id === user.external_id,
).displayName;
)?.displayName;
if (!typistName) {
throw new Error('typistName not found.');
}
return {
typistUserId: user.id,
typistName,
@ -135,7 +138,10 @@ const convertUserToTypist = (
): Typist => {
const typistName = b2cUserInfo.find(
(x) => x.id === user.external_id,
).displayName;
)?.displayName;
if (!typistName) {
throw new Error('typistName not found.');
}
return {
id: user.id,
name: typistName,

View File

@ -66,7 +66,7 @@ describe('getTemplates', () => {
expect(templates[1].id).toBe(template2.id);
expect(templates[1].name).toBe(template2.file_name);
}
}, 6000000);
});
it('テンプレートファイル一覧を取得できる0件', async () => {
if (!source) fail();

View File

@ -23,6 +23,9 @@ export const createTemplateFile = async (
id: template.id,
},
});
if (!templateFile) {
fail();
}
return templateFile;
};

View File

@ -8,7 +8,7 @@ import { HttpException, HttpStatus } from '@nestjs/common';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
describe('利用規約取得', () => {
let source: DataSource = null;
let source: DataSource | null = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
@ -21,12 +21,15 @@ describe('利用規約取得', () => {
});
afterEach(async () => {
if (!source) return;
await source.destroy();
source = null;
});
it('最新の利用規約情報が取得できる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<TermsService>(TermsService);
await createTermInfo(source, 'EULA', 'v1.0');
@ -44,7 +47,9 @@ describe('利用規約取得', () => {
});
it('利用規約情報(EULA、DPA両方)が存在しない場合エラーとなる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<TermsService>(TermsService);
const context = makeContext(uuidv4());
await expect(service.getTermsInfo(context)).rejects.toEqual(
@ -56,7 +61,9 @@ describe('利用規約取得', () => {
});
it('利用規約情報(EULAのみ)が存在しない場合エラーとなる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<TermsService>(TermsService);
await createTermInfo(source, 'DPA', 'v1.0');
const context = makeContext(uuidv4());
@ -69,7 +76,9 @@ describe('利用規約取得', () => {
});
it('利用規約情報(DPAのみ)が存在しない場合エラーとなる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<TermsService>(TermsService);
await createTermInfo(source, 'EULA', 'v1.0');
const context = makeContext(uuidv4());

View File

@ -64,7 +64,7 @@ export type ConfigMockValue = {
export const makeUsersServiceMock = async (
usersRepositoryMockValue: UsersRepositoryMockValue,
licensesRepositoryMockValue: LicensesRepositoryMockValue,
licensesRepositoryMockValue: LicensesRepositoryMockValue | null,
adB2cMockValue: AdB2cMockValue,
sendGridMockValue: SendGridMockValue,
configMockValue: ConfigMockValue,
@ -368,7 +368,7 @@ export const makeDefaultUsersRepositoryMockValue =
user1.created_by = 'test';
user1.created_at = new Date();
user1.updated_by = null;
user1.updated_at = null;
user1.updated_at = new Date();
const user2 = new User();
user2.id = 3;
@ -388,7 +388,7 @@ export const makeDefaultUsersRepositoryMockValue =
user2.created_by = 'test';
user2.created_at = new Date();
user2.updated_by = null;
user2.updated_at = null;
user2.updated_at = new Date();
return {
updateUserVerified: undefined,
@ -406,7 +406,7 @@ const AdB2cMockUsers: AdB2cUser[] = [
displayName: 'test1',
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EAMILADDRESS,
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: 'issuer',
issuerAssignedId: 'test1@mail.com',
},
@ -417,7 +417,7 @@ const AdB2cMockUsers: AdB2cUser[] = [
displayName: 'test2',
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EAMILADDRESS,
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: 'issuer',
issuerAssignedId: 'test2@mail.com',
},
@ -428,7 +428,7 @@ const AdB2cMockUsers: AdB2cUser[] = [
displayName: 'test3',
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EAMILADDRESS,
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: 'issuer',
issuerAssignedId: 'test3@mail.com',
},

View File

@ -107,7 +107,7 @@ export const createLicense = async (
export const makeTestingModuleWithAdb2c = async (
datasource: DataSource,
adB2cMockValue: AdB2cMockValue,
): Promise<TestingModule> => {
): Promise<TestingModule | undefined> => {
try {
const module: TestingModule = await Test.createTestingModule({
imports: [

View File

@ -130,10 +130,23 @@ export class UsersController {
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
@Get()
async getUsers(@Req() req: Request): Promise<GetUsersResponse> {
const accessToken = retrieveAuthorizationToken(req);
const decodedToken = jwt.decode(accessToken, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
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 users = await this.usersService.getUsers(decodedToken.userId);
const users = await this.usersService.getUsers(userId);
return { users };
}
@ -179,15 +192,28 @@ export class UsersController {
prompt,
} = body;
const accessToken = retrieveAuthorizationToken(req);
const payload = jwt.decode(accessToken, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
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(payload.userId);
const context = makeContext(userId);
//ユーザ作成処理
await this.usersService.createUser(
context,
payload,
userId,
name,
role as UserRoles,
email,
@ -225,8 +251,21 @@ export class UsersController {
@UseGuards(AuthGuard)
@Get('relations')
async getRelations(@Req() req: Request): Promise<GetRelationsResponse> {
const token = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
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);
@ -265,8 +304,22 @@ export class UsersController {
@Req() req: Request,
): Promise<PostSortCriteriaResponse> {
const { direction, paramName } = body;
const accessToken = retrieveAuthorizationToken(req);
const decodedToken = jwt.decode(accessToken, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
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;
//型チェック
if (
@ -278,11 +331,7 @@ export class UsersController {
HttpStatus.BAD_REQUEST,
);
}
await this.usersService.updateSortCriteria(
paramName,
direction,
decodedToken,
);
await this.usersService.updateSortCriteria(paramName, direction, userId);
return {};
}
@ -313,11 +362,25 @@ export class UsersController {
@Req() req: Request,
): Promise<GetSortCriteriaResponse> {
const {} = query;
const accessToken = retrieveAuthorizationToken(req);
const decodedToken = jwt.decode(accessToken, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
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 { direction, paramName } = await this.usersService.getSortCriteria(
decodedToken,
userId,
);
return { direction, paramName };
}
@ -367,7 +430,20 @@ export class UsersController {
} = 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);
@ -421,8 +497,21 @@ export class UsersController {
@Body() body: AllocateLicenseRequest,
@Req() req: Request,
): Promise<AllocateLicenseResponse> {
const accessToken = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(accessToken, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
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.usersService.allocateLicense(
@ -467,8 +556,21 @@ export class UsersController {
@Body() body: DeallocateLicenseRequest,
@Req() req: Request,
): Promise<DeallocateLicenseResponse> {
const accessToken = retrieveAuthorizationToken(req);
const { userId } = jwt.decode(accessToken, { json: true }) as AccessToken;
const accessToken = retrieveAuthorizationToken(req) as string;
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

@ -46,7 +46,7 @@ import {
import { v4 as uuidv4 } from 'uuid';
describe('UsersService.confirmUser', () => {
let source: DataSource = null;
let source: DataSource | null = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
@ -57,12 +57,15 @@ describe('UsersService.confirmUser', () => {
return source.initialize();
});
afterEach(async () => {
if (!source) return;
await source.destroy();
source = null;
});
it('ユーザの仮登録時に払い出されるトークンにより、未認証のユーザが認証済みになり、トライアルライセンスが100件作成される', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = (await makeTestAccount(source)).account;
const { id: userId } = await makeTestUser(source, {
account_id: accountId,
@ -110,9 +113,10 @@ describe('UsersService.confirmUser', () => {
allocated_user_id: resultLicenses[0].allocated_user_id,
order_id: resultLicenses[0].order_id,
delete_order_id: resultLicenses[0].delete_order_id,
user: resultLicenses[0].user ?? null,
};
expect(resultUser.email_verified).toBe(true);
expect(resultUser?.email_verified).toBe(true);
expect(resultLicenses.length).toBe(100);
expect(resultLicensesCheckParam).toEqual({
id: 0,
@ -123,11 +127,14 @@ describe('UsersService.confirmUser', () => {
allocated_user_id: null,
order_id: null,
delete_order_id: null,
user: null,
});
});
}, 600000);
it('トークンの形式が不正な場合、形式不正エラーとなる。', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const token = 'invalid.id.token';
const service = module.get<UsersService>(UsersService);
await expect(service.confirmUser(token)).rejects.toEqual(
@ -136,7 +143,9 @@ describe('UsersService.confirmUser', () => {
});
it('ユーザが既に認証済みだった場合、認証済みユーザエラーとなる。', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = (await makeTestAccount(source)).account;
await makeTestUser(source, {
account_id: accountId,
@ -168,7 +177,9 @@ describe('UsersService.confirmUser', () => {
);
});
it('ユーザーが存在しない場合は、想定外のエラーとなる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<UsersService>(UsersService);
const token =
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw';
@ -201,6 +212,12 @@ describe('UsersService.confirmUserAndInitPassword', () => {
notification: true,
encryption: false,
prompt: false,
account: null,
author_id: null,
deleted_at: null,
encryption_password: null,
license: null,
userGroupMembers: null,
};
const licensesRepositoryMockValue = null;
const adb2cParam = makeDefaultAdB2cMockValue();
@ -246,6 +263,12 @@ describe('UsersService.confirmUserAndInitPassword', () => {
notification: true,
encryption: false,
prompt: false,
account: null,
author_id: null,
deleted_at: null,
encryption_password: null,
license: null,
userGroupMembers: null,
};
const licensesRepositoryMockValue = null;
const adb2cParam = makeDefaultAdB2cMockValue();
@ -287,6 +310,12 @@ describe('UsersService.confirmUserAndInitPassword', () => {
notification: true,
encryption: false,
prompt: false,
account: null,
author_id: null,
deleted_at: null,
encryption_password: null,
license: null,
userGroupMembers: null,
};
const licensesRepositoryMockValue = null;
const adb2cParam = makeDefaultAdB2cMockValue();
@ -332,6 +361,12 @@ describe('UsersService.confirmUserAndInitPassword', () => {
notification: true,
encryption: false,
prompt: false,
account: null,
author_id: null,
deleted_at: null,
encryption_password: null,
license: null,
userGroupMembers: null,
};
const licensesRepositoryMockValue = null;
const adb2cParam = makeDefaultAdB2cMockValue();
@ -362,7 +397,7 @@ describe('UsersService.confirmUserAndInitPassword', () => {
});
describe('UsersService.createUser', () => {
let source: DataSource = null;
let source: DataSource | null = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
@ -375,12 +410,15 @@ describe('UsersService.createUser', () => {
});
afterEach(async () => {
if (!source) return;
await source.destroy();
source = null;
});
it('管理者権限のあるアクセストークンを使用して、新規ユーザが追加される(role:None)', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<UsersService>(UsersService);
const adminExternalId = 'ADMIN0001';
@ -433,7 +471,7 @@ describe('UsersService.createUser', () => {
expect(
await service.createUser(
makeContext('trackingId'),
token,
adminExternalId,
name,
role,
email,
@ -446,16 +484,16 @@ describe('UsersService.createUser', () => {
// 追加されたユーザーが正しくDBに登録されていることを確認
const user = await getUserFromExternalId(source, externalId);
expect(user).not.toBeNull();
expect(user.account_id).toEqual(accountId);
expect(user.role).toEqual(role);
expect(user.author_id).toEqual(null);
expect(user.email_verified).toEqual(false);
expect(user.auto_renew).toEqual(autoRenew);
expect(user.license_alert).toEqual(licenseAlert);
expect(user.notification).toEqual(notification);
expect(user.encryption).toEqual(false);
expect(user.encryption_password).toEqual(null);
expect(user.prompt).toEqual(false);
expect(user?.account_id).toEqual(accountId);
expect(user?.role).toEqual(role);
expect(user?.author_id).toEqual(null);
expect(user?.email_verified).toEqual(false);
expect(user?.auto_renew).toEqual(autoRenew);
expect(user?.license_alert).toEqual(licenseAlert);
expect(user?.notification).toEqual(notification);
expect(user?.encryption).toEqual(false);
expect(user?.encryption_password).toEqual(null);
expect(user?.prompt).toEqual(false);
// 他にユーザーが登録されていないことを確認
const users = await getUsers(source);
@ -463,7 +501,9 @@ describe('UsersService.createUser', () => {
});
it('管理者権限のあるアクセストークンを使用して、新規ユーザが追加される(role:Author; 暗号化あり)', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<UsersService>(UsersService);
const adminExternalId = 'ADMIN0001';
const { account, admin } = await makeTestAccount(
@ -519,7 +559,7 @@ describe('UsersService.createUser', () => {
expect(
await service.createUser(
makeContext('trackingId'),
token,
adminExternalId,
name,
role,
email,
@ -536,16 +576,16 @@ describe('UsersService.createUser', () => {
// 追加されたユーザーが正しくDBに登録されていることを確認
const user = await getUserFromExternalId(source, externalId);
expect(user).not.toBeNull();
expect(user.account_id).toEqual(accountId);
expect(user.role).toEqual(role);
expect(user.author_id).toEqual(authorId);
expect(user.email_verified).toEqual(false);
expect(user.auto_renew).toEqual(autoRenew);
expect(user.license_alert).toEqual(licenseAlert);
expect(user.notification).toEqual(notification);
expect(user.encryption).toEqual(encryption);
expect(user.encryption_password).toEqual(encryptionPassword);
expect(user.prompt).toEqual(prompt);
expect(user?.account_id).toEqual(accountId);
expect(user?.role).toEqual(role);
expect(user?.author_id).toEqual(authorId);
expect(user?.email_verified).toEqual(false);
expect(user?.auto_renew).toEqual(autoRenew);
expect(user?.license_alert).toEqual(licenseAlert);
expect(user?.notification).toEqual(notification);
expect(user?.encryption).toEqual(encryption);
expect(user?.encryption_password).toEqual(encryptionPassword);
expect(user?.prompt).toEqual(prompt);
// 他にユーザーが登録されていないことを確認
const users = await getUsers(source);
@ -553,7 +593,9 @@ describe('UsersService.createUser', () => {
});
it('管理者権限のあるアクセストークンを使用して、新規ユーザが追加される(role:Author; 暗号化無し)', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<UsersService>(UsersService);
const adminExternalId = 'ADMIN0001';
const { account, admin } = await makeTestAccount(
@ -608,7 +650,7 @@ describe('UsersService.createUser', () => {
expect(
await service.createUser(
makeContext('trackingId'),
token,
adminExternalId,
name,
role,
email,
@ -625,16 +667,16 @@ describe('UsersService.createUser', () => {
// 追加されたユーザーが正しくDBに登録されていることを確認
const user = await getUserFromExternalId(source, externalId);
expect(user).not.toBeNull();
expect(user.account_id).toEqual(accountId);
expect(user.role).toEqual(role);
expect(user.author_id).toEqual(authorId);
expect(user.email_verified).toEqual(false);
expect(user.auto_renew).toEqual(autoRenew);
expect(user.license_alert).toEqual(licenseAlert);
expect(user.notification).toEqual(notification);
expect(user.encryption).toEqual(encryption);
expect(user.encryption_password).toBeNull();
expect(user.prompt).toEqual(prompt);
expect(user?.account_id).toEqual(accountId);
expect(user?.role).toEqual(role);
expect(user?.author_id).toEqual(authorId);
expect(user?.email_verified).toEqual(false);
expect(user?.auto_renew).toEqual(autoRenew);
expect(user?.license_alert).toEqual(licenseAlert);
expect(user?.notification).toEqual(notification);
expect(user?.encryption).toEqual(encryption);
expect(user?.encryption_password).toBeNull();
expect(user?.prompt).toEqual(prompt);
// 他にユーザーが登録されていないことを確認
const users = await getUsers(source);
@ -642,7 +684,9 @@ describe('UsersService.createUser', () => {
});
it('管理者権限のあるアクセストークンを使用して、新規ユーザが追加される(role:Transcriptioninst)', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<UsersService>(UsersService);
const adminExternalId = 'ADMIN0001';
const { account, admin } = await makeTestAccount(
@ -694,7 +738,7 @@ describe('UsersService.createUser', () => {
expect(
await service.createUser(
makeContext('trackingId'),
token,
adminExternalId,
name,
role,
email,
@ -707,16 +751,16 @@ describe('UsersService.createUser', () => {
// 追加されたユーザーが正しくDBに登録されていることを確認
const user = await getUserFromExternalId(source, externalId);
expect(user).not.toBeNull();
expect(user.account_id).toEqual(accountId);
expect(user.role).toEqual(role);
expect(user.author_id).toBeNull();
expect(user.email_verified).toEqual(false);
expect(user.auto_renew).toEqual(autoRenew);
expect(user.license_alert).toEqual(licenseAlert);
expect(user.notification).toEqual(notification);
expect(user.encryption).toEqual(false);
expect(user.encryption_password).toBeNull();
expect(user.prompt).toEqual(false);
expect(user?.account_id).toEqual(accountId);
expect(user?.role).toEqual(role);
expect(user?.author_id).toBeNull();
expect(user?.email_verified).toEqual(false);
expect(user?.auto_renew).toEqual(autoRenew);
expect(user?.license_alert).toEqual(licenseAlert);
expect(user?.notification).toEqual(notification);
expect(user?.encryption).toEqual(false);
expect(user?.encryption_password).toBeNull();
expect(user?.prompt).toEqual(false);
// 他にユーザーが登録されていないことを確認
const users = await getUsers(source);
@ -724,7 +768,9 @@ describe('UsersService.createUser', () => {
});
it('DBネットワークエラーとなる場合、リカバリ処理を実施し、ADB2Cに作成したユーザーを削除する', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<UsersService>(UsersService);
const b2cService = module.get<AdB2cService>(AdB2cService);
const adminExternalId = 'ADMIN0001';
@ -785,7 +831,7 @@ describe('UsersService.createUser', () => {
try {
await service.createUser(
makeContext('trackingId'),
token,
adminExternalId,
name,
role,
email,
@ -809,7 +855,9 @@ describe('UsersService.createUser', () => {
});
it('DBネットワークエラーとなる場合、リカバリ処理を実施されるが、そのリカバリ処理に失敗した場合、ADB2Cのユーザーは削除されない', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<UsersService>(UsersService);
const b2cService = module.get<AdB2cService>(AdB2cService);
const adminExternalId = 'ADMIN0001';
@ -870,7 +918,7 @@ describe('UsersService.createUser', () => {
try {
await service.createUser(
makeContext('trackingId'),
token,
adminExternalId,
name,
role,
email,
@ -899,7 +947,9 @@ describe('UsersService.createUser', () => {
});
it('Azure ADB2Cでネットワークエラーとなる場合、エラーとなる。', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<UsersService>(UsersService);
const adminExternalId = 'ADMIN0001';
const { account, admin } = await makeTestAccount(
@ -949,7 +999,7 @@ describe('UsersService.createUser', () => {
try {
await service.createUser(
makeContext('trackingId'),
token,
adminExternalId,
name,
role,
email,
@ -972,7 +1022,9 @@ describe('UsersService.createUser', () => {
});
it('Azure AD B2C内でメールアドレスが重複している場合、エラーとなる。', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<UsersService>(UsersService);
const adminExternalId = 'ADMIN0001';
const { account, admin } = await makeTestAccount(
@ -1026,7 +1078,7 @@ describe('UsersService.createUser', () => {
try {
await service.createUser(
makeContext('trackingId'),
token,
adminExternalId,
name,
role,
email,
@ -1049,7 +1101,9 @@ describe('UsersService.createUser', () => {
});
it('AuthorIDが重複している場合、エラーとなる。(AuthorID重複チェックでエラー)', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<UsersService>(UsersService);
const adminExternalId = 'ADMIN0001';
const { account, admin } = await makeTestAccount(
@ -1105,7 +1159,7 @@ describe('UsersService.createUser', () => {
expect(
await service.createUser(
makeContext('trackingId'),
token,
adminExternalId,
name,
role,
email_1,
@ -1146,7 +1200,7 @@ describe('UsersService.createUser', () => {
try {
await service.createUser(
makeContext('trackingId'),
token,
adminExternalId,
name,
role,
email_2,
@ -1175,7 +1229,9 @@ describe('UsersService.createUser', () => {
});
it('AuthorIDが重複している場合、エラー(insert失敗)となり、リカバリ処理が実行され、ADB2Cに追加したユーザーが削除される', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<UsersService>(UsersService);
const b2cService = module.get<AdB2cService>(AdB2cService);
const adminExternalId = 'ADMIN0001';
@ -1240,7 +1296,7 @@ describe('UsersService.createUser', () => {
try {
await service.createUser(
makeContext('trackingId'),
token,
adminExternalId,
name,
role,
email,
@ -1272,7 +1328,9 @@ describe('UsersService.createUser', () => {
});
it('メール送信に失敗した場合、リカバリ処理が実行され、ADB2C,DBのユーザーが削除される', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<UsersService>(UsersService);
const b2cService = module.get<AdB2cService>(AdB2cService);
@ -1327,7 +1385,7 @@ describe('UsersService.createUser', () => {
try {
await service.createUser(
makeContext('trackingId'),
token,
adminExternalId,
name,
role,
email,
@ -1357,7 +1415,9 @@ describe('UsersService.createUser', () => {
});
it('メール送信に失敗した場合、リカバリ処理が実行されるが、そのリカバリ処理に失敗した場合、ADB2C,DBのユーザーが削除されない', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<UsersService>(UsersService);
const b2cService = module.get<AdB2cService>(AdB2cService);
@ -1417,7 +1477,7 @@ describe('UsersService.createUser', () => {
try {
await service.createUser(
makeContext('trackingId'),
token,
adminExternalId,
name,
role,
email,
@ -1446,7 +1506,7 @@ describe('UsersService.createUser', () => {
});
describe('UsersService.getUsers', () => {
let source: DataSource = null;
let source: DataSource | null = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
@ -1459,13 +1519,16 @@ describe('UsersService.getUsers', () => {
});
afterEach(async () => {
if (!source) return;
await source.destroy();
source = null;
});
it('ユーザーの一覧を取得できる(ライセンス未割当)', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
if (!source) fail();
const module = await makeTestingModuleWithAdb2c(source, adb2cParam);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId_author, id: authorUserId } =
@ -1565,7 +1628,9 @@ describe('UsersService.getUsers', () => {
it('ユーザーの一覧を取得できること(ライセンス割当済み)', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
if (!source) fail();
const module = await makeTestingModuleWithAdb2c(source, adb2cParam);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: user1, external_id: external_id1 } = await makeTestUser(
@ -1679,7 +1744,9 @@ describe('UsersService.getUsers', () => {
it('DBからのユーザーの取得に失敗した場合、エラーとなる', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
if (!source) fail();
const module = await makeTestingModuleWithAdb2c(source, adb2cParam);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
await makeTestUser(source, {
@ -1702,7 +1769,9 @@ describe('UsersService.getUsers', () => {
it('ADB2Cからのユーザーの取得に失敗した場合、エラーとなる', async () => {
const adb2cParam = makeDefaultAdB2cMockValue();
adb2cParam.getUsers = new Error('ADB2C error');
if (!source) fail();
const module = await makeTestingModuleWithAdb2c(source, adb2cParam);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: externalId_author } = await makeTestUser(source, {
@ -1742,11 +1811,7 @@ describe('UsersService.updateSortCriteria', () => {
);
expect(
await service.updateSortCriteria('AUTHOR_ID', 'ASC', {
role: 'none admin',
userId: 'xxxxxxxxxxxx',
tier: 5,
}),
await service.updateSortCriteria('AUTHOR_ID', 'ASC', 'external_id'),
).toEqual(undefined);
});
@ -1771,11 +1836,7 @@ describe('UsersService.updateSortCriteria', () => {
);
await expect(
service.updateSortCriteria('AUTHOR_ID', 'ASC', {
role: 'none admin',
userId: 'xxxxxxxxxxxx',
tier: 5,
}),
service.updateSortCriteria('AUTHOR_ID', 'ASC', 'external_id'),
).rejects.toEqual(
new HttpException(
makeErrorResponse('E009999'),
@ -1806,11 +1867,7 @@ describe('UsersService.updateSortCriteria', () => {
);
await expect(
service.updateSortCriteria('AUTHOR_ID', 'ASC', {
role: 'none admin',
userId: 'xxxxxxxxxxxx',
tier: 5,
}),
service.updateSortCriteria('AUTHOR_ID', 'ASC', 'external_id'),
).rejects.toEqual(
new HttpException(
makeErrorResponse('E009999'),
@ -1838,13 +1895,10 @@ describe('UsersService.getSortCriteria', () => {
sortCriteriaRepositoryMockValue,
);
expect(
await service.getSortCriteria({
role: 'none admin',
userId: 'xxxxxxxxxxxx',
tier: 5,
}),
).toEqual({ direction: 'ASC', paramName: 'JOB_NUMBER' });
expect(await service.getSortCriteria('external_id')).toEqual({
direction: 'ASC',
paramName: 'JOB_NUMBER',
});
});
it('ソート条件が存在せず、ソート条件を取得できない', async () => {
@ -1869,13 +1923,7 @@ describe('UsersService.getSortCriteria', () => {
sortCriteriaRepositoryMockValue,
);
await expect(
service.getSortCriteria({
role: 'none admin',
userId: 'xxxxxxxxxxxx',
tier: 5,
}),
).rejects.toEqual(
await expect(service.getSortCriteria('external_id')).rejects.toEqual(
new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
@ -1907,13 +1955,7 @@ describe('UsersService.getSortCriteria', () => {
sortCriteriaRepositoryMockValue,
);
await expect(
service.getSortCriteria({
role: 'none admin',
userId: 'xxxxxxxxxxxx',
tier: 5,
}),
).rejects.toEqual(
await expect(service.getSortCriteria('external_id')).rejects.toEqual(
new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
@ -1923,7 +1965,7 @@ describe('UsersService.getSortCriteria', () => {
});
describe('UsersService.updateUser', () => {
let source: DataSource = null;
let source: DataSource | null = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
@ -1936,12 +1978,15 @@ describe('UsersService.updateUser', () => {
});
afterEach(async () => {
if (!source) return;
await source.destroy();
source = null;
});
it('ユーザー情報を更新できるNone', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: external_id } = await makeTestUser(source, {
@ -1985,19 +2030,21 @@ describe('UsersService.updateUser', () => {
const createdUser = await getUser(source, user1);
expect(createdUser.id).toBe(user1);
expect(createdUser.role).toBe(USER_ROLES.NONE);
expect(createdUser.author_id).toBeNull();
expect(createdUser.auto_renew).toBe(false);
expect(createdUser.license_alert).toBe(false);
expect(createdUser.notification).toBe(false);
expect(createdUser.encryption).toBe(false);
expect(createdUser.encryption_password).toBeNull();
expect(createdUser.prompt).toBe(false);
expect(createdUser?.id).toBe(user1);
expect(createdUser?.role).toBe(USER_ROLES.NONE);
expect(createdUser?.author_id).toBeNull();
expect(createdUser?.auto_renew).toBe(false);
expect(createdUser?.license_alert).toBe(false);
expect(createdUser?.notification).toBe(false);
expect(createdUser?.encryption).toBe(false);
expect(createdUser?.encryption_password).toBeNull();
expect(createdUser?.prompt).toBe(false);
});
it('ユーザー情報を更新できるTypist', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: external_id } = await makeTestUser(source, {
@ -2041,19 +2088,21 @@ describe('UsersService.updateUser', () => {
const createdUser = await getUser(source, user1);
expect(createdUser.id).toBe(user1);
expect(createdUser.role).toBe(USER_ROLES.TYPIST);
expect(createdUser.author_id).toBeNull();
expect(createdUser.auto_renew).toBe(false);
expect(createdUser.license_alert).toBe(false);
expect(createdUser.notification).toBe(false);
expect(createdUser.encryption).toBe(false);
expect(createdUser.encryption_password).toBeNull();
expect(createdUser.prompt).toBe(false);
expect(createdUser?.id).toBe(user1);
expect(createdUser?.role).toBe(USER_ROLES.TYPIST);
expect(createdUser?.author_id).toBeNull();
expect(createdUser?.auto_renew).toBe(false);
expect(createdUser?.license_alert).toBe(false);
expect(createdUser?.notification).toBe(false);
expect(createdUser?.encryption).toBe(false);
expect(createdUser?.encryption_password).toBeNull();
expect(createdUser?.prompt).toBe(false);
});
it('ユーザー情報を更新できるAuthor', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: external_id } = await makeTestUser(source, {
@ -2097,19 +2146,21 @@ describe('UsersService.updateUser', () => {
const createdUser = await getUser(source, user1);
expect(createdUser.id).toBe(user1);
expect(createdUser.role).toBe(USER_ROLES.AUTHOR);
expect(createdUser.author_id).toBe('AUTHOR_ID');
expect(createdUser.auto_renew).toBe(false);
expect(createdUser.license_alert).toBe(false);
expect(createdUser.notification).toBe(false);
expect(createdUser.encryption).toBe(true);
expect(createdUser.encryption_password).toBe('new_password');
expect(createdUser.prompt).toBe(true);
expect(createdUser?.id).toBe(user1);
expect(createdUser?.role).toBe(USER_ROLES.AUTHOR);
expect(createdUser?.author_id).toBe('AUTHOR_ID');
expect(createdUser?.auto_renew).toBe(false);
expect(createdUser?.license_alert).toBe(false);
expect(createdUser?.notification).toBe(false);
expect(createdUser?.encryption).toBe(true);
expect(createdUser?.encryption_password).toBe('new_password');
expect(createdUser?.prompt).toBe(true);
});
it('ユーザーのRoleを更新できるNone⇒Typist', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: external_id } = await makeTestUser(source, {
@ -2153,19 +2204,21 @@ describe('UsersService.updateUser', () => {
const createdUser = await getUser(source, user1);
expect(createdUser.id).toBe(user1);
expect(createdUser.role).toBe(USER_ROLES.TYPIST);
expect(createdUser.author_id).toBeNull();
expect(createdUser.auto_renew).toBe(false);
expect(createdUser.license_alert).toBe(false);
expect(createdUser.notification).toBe(false);
expect(createdUser.encryption).toBe(false);
expect(createdUser.encryption_password).toBeNull();
expect(createdUser.prompt).toBe(false);
expect(createdUser?.id).toBe(user1);
expect(createdUser?.role).toBe(USER_ROLES.TYPIST);
expect(createdUser?.author_id).toBeNull();
expect(createdUser?.auto_renew).toBe(false);
expect(createdUser?.license_alert).toBe(false);
expect(createdUser?.notification).toBe(false);
expect(createdUser?.encryption).toBe(false);
expect(createdUser?.encryption_password).toBeNull();
expect(createdUser?.prompt).toBe(false);
});
it('ユーザーのRoleを更新できるNone⇒Author', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: external_id } = await makeTestUser(source, {
@ -2209,19 +2262,21 @@ describe('UsersService.updateUser', () => {
const createdUser = await getUser(source, user1);
expect(createdUser.id).toBe(user1);
expect(createdUser.role).toBe(USER_ROLES.AUTHOR);
expect(createdUser.author_id).toBe('AUTHOR_ID');
expect(createdUser.auto_renew).toBe(false);
expect(createdUser.license_alert).toBe(false);
expect(createdUser.notification).toBe(false);
expect(createdUser.encryption).toBe(false);
expect(createdUser.encryption_password).toBeNull();
expect(createdUser.prompt).toBe(false);
expect(createdUser?.id).toBe(user1);
expect(createdUser?.role).toBe(USER_ROLES.AUTHOR);
expect(createdUser?.author_id).toBe('AUTHOR_ID');
expect(createdUser?.auto_renew).toBe(false);
expect(createdUser?.license_alert).toBe(false);
expect(createdUser?.notification).toBe(false);
expect(createdUser?.encryption).toBe(false);
expect(createdUser?.encryption_password).toBeNull();
expect(createdUser?.prompt).toBe(false);
});
it('None以外からRoleを変更した場合、エラーとなるTypist⇒None', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: external_id } = await makeTestUser(source, {
@ -2267,7 +2322,9 @@ describe('UsersService.updateUser', () => {
});
it('Authorがパスワードundefinedで渡されたとき、元のパスワードを維持するEncryptionがtrue', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: external_id } = await makeTestUser(source, {
@ -2311,19 +2368,21 @@ describe('UsersService.updateUser', () => {
const createdUser = await getUser(source, user1);
expect(createdUser.id).toBe(user1);
expect(createdUser.role).toBe(USER_ROLES.AUTHOR);
expect(createdUser.author_id).toBe('AUTHOR_ID');
expect(createdUser.auto_renew).toBe(false);
expect(createdUser.license_alert).toBe(false);
expect(createdUser.notification).toBe(false);
expect(createdUser.encryption).toBe(true);
expect(createdUser.encryption_password).toBe('password');
expect(createdUser.prompt).toBe(true);
expect(createdUser?.id).toBe(user1);
expect(createdUser?.role).toBe(USER_ROLES.AUTHOR);
expect(createdUser?.author_id).toBe('AUTHOR_ID');
expect(createdUser?.auto_renew).toBe(false);
expect(createdUser?.license_alert).toBe(false);
expect(createdUser?.notification).toBe(false);
expect(createdUser?.encryption).toBe(true);
expect(createdUser?.encryption_password).toBe('password');
expect(createdUser?.prompt).toBe(true);
});
it('Authorが暗号化なしで更新した場合、パスワードをNULLにするEncryptionがfalse', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: external_id } = await makeTestUser(source, {
@ -2367,19 +2426,21 @@ describe('UsersService.updateUser', () => {
const createdUser = await getUser(source, user1);
expect(createdUser.id).toBe(user1);
expect(createdUser.role).toBe(USER_ROLES.AUTHOR);
expect(createdUser.author_id).toBe('AUTHOR_ID');
expect(createdUser.auto_renew).toBe(false);
expect(createdUser.license_alert).toBe(false);
expect(createdUser.notification).toBe(false);
expect(createdUser.encryption).toBe(false);
expect(createdUser.encryption_password).toBeNull();
expect(createdUser.prompt).toBe(true);
expect(createdUser?.id).toBe(user1);
expect(createdUser?.role).toBe(USER_ROLES.AUTHOR);
expect(createdUser?.author_id).toBe('AUTHOR_ID');
expect(createdUser?.auto_renew).toBe(false);
expect(createdUser?.license_alert).toBe(false);
expect(createdUser?.notification).toBe(false);
expect(createdUser?.encryption).toBe(false);
expect(createdUser?.encryption_password).toBeNull();
expect(createdUser?.prompt).toBe(true);
});
it('AuthorのDBにパスワードが設定されていない場合、パスワードundefinedでわたすとエラーとなるEncryptionがtrue', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: external_id } = await makeTestUser(source, {
@ -2425,7 +2486,9 @@ describe('UsersService.updateUser', () => {
});
it('AuthorIdが既存のユーザーと重複した場合、エラーとなる', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { external_id: external_id } = await makeTestUser(source, {
@ -2483,7 +2546,7 @@ describe('UsersService.updateUser', () => {
});
describe('UsersService.updateAcceptedVersion', () => {
let source: DataSource = null;
let source: DataSource | null = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
@ -2496,12 +2559,15 @@ describe('UsersService.updateAcceptedVersion', () => {
});
afterEach(async () => {
if (!source) return;
await source.destroy();
source = null;
});
it('同意済み利用規約バージョンを更新できる(第五)', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { admin } = await makeTestAccount(source, {
tier: 5,
});
@ -2511,11 +2577,13 @@ describe('UsersService.updateAcceptedVersion', () => {
await service.updateAcceptedVersion(context, admin.external_id, 'v2.0');
const user = await getUser(source, admin.id);
expect(user.accepted_eula_version).toBe('v2.0');
expect(user?.accepted_eula_version).toBe('v2.0');
});
it('同意済み利用規約バージョンを更新できる(第一~第四)', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { admin } = await makeTestAccount(source, {
tier: 4,
});
@ -2530,27 +2598,14 @@ describe('UsersService.updateAcceptedVersion', () => {
);
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),
);
expect(user?.accepted_eula_version).toBe('v2.0');
expect(user?.accepted_dpa_version).toBe('v3.0');
});
it('パラメータが不在のときエラーとなる(第一~第四)', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const { admin } = await makeTestAccount(source, {
tier: 4,
});

View File

@ -48,9 +48,13 @@ import {
LicenseExpiredError,
LicenseUnavailableError,
} from '../../repositories/licenses/errors/types';
import { AccountNotFoundError } from '../../repositories/accounts/errors/types';
@Injectable()
export class UsersService {
private readonly logger = new Logger(UsersService.name);
private readonly mailFrom: string;
private readonly appDomain: string;
constructor(
private readonly usersRepository: UsersRepositoryService,
private readonly licensesRepository: LicensesRepositoryService,
@ -58,8 +62,10 @@ export class UsersService {
private readonly adB2cService: AdB2cService,
private readonly configService: ConfigService,
private readonly sendgridService: SendGridService,
) {}
private readonly logger = new Logger(UsersService.name);
) {
this.mailFrom = this.configService.getOrThrow<string>('MAIL_FROM');
this.appDomain = this.configService.getOrThrow<string>('APP_DOMAIN');
}
/**
* Confirms user
@ -128,7 +134,7 @@ export class UsersService {
*/
async createUser(
context: Context,
accessToken: AccessToken,
externalId: string,
name: string,
role: UserRoles,
email: string,
@ -145,9 +151,7 @@ export class UsersService {
//DBよりアクセス者の所属するアカウントIDを取得する
let adminUser: EntityUser;
try {
adminUser = await this.usersRepository.findUserByExternalId(
accessToken.userId,
);
adminUser = await this.usersRepository.findUserByExternalId(externalId);
} catch (e) {
this.logger.error(`error=${e}`);
throw new HttpException(
@ -254,9 +258,6 @@ export class UsersService {
//Email送信用のコンテンツを作成する
try {
// メールの送信元を取得
const from = this.configService.get<string>('MAIL_FROM') ?? '';
// メールの内容を構成
const { subject, text, html } =
await this.sendgridService.createMailContentFromEmailConfirmForNormalUser(
@ -269,7 +270,7 @@ export class UsersService {
await this.sendgridService.sendMail(
context,
email,
from,
this.mailFrom,
subject,
text,
html,
@ -343,6 +344,12 @@ export class UsersService {
license_alert: licenseAlert,
notification,
role,
accepted_dpa_version: null,
accepted_eula_version: null,
encryption: false,
encryption_password: null,
prompt: false,
author_id: null,
};
case USER_ROLES.AUTHOR:
return {
@ -352,10 +359,12 @@ export class UsersService {
license_alert: licenseAlert,
notification,
role,
author_id: authorId,
encryption,
encryption_password: encryptionPassword,
prompt,
author_id: authorId ?? null,
encryption: encryption ?? false,
encryption_password: encryptionPassword ?? null,
prompt: prompt ?? false,
accepted_dpa_version: null,
accepted_eula_version: null,
};
default:
//不正なroleが指定された場合はログを出力してエラーを返す
@ -405,19 +414,16 @@ export class UsersService {
await this.adB2cService.changePassword(extarnalId, ramdomPassword);
// ユーザを認証済みにする
await this.usersRepository.updateUserVerified(userId);
// メールの送信元を取得
const from = this.configService.get<string>('MAIL_FROM') ?? '';
// TODO [Task2163] ODMS側が正式にメッセージを決めるまで仮のメール内容とする
const subject = 'A temporary password has been issued.';
const text = 'temporary password: ' + ramdomPassword;
const domains = this.configService.get<string>('APP_DOMAIN');
const html = `<p>OMDS TOP PAGE URL.<p><a href="${domains}">${domains}"</a><br>temporary password: ${ramdomPassword}`;
const html = `<p>OMDS TOP PAGE URL.<p><a href="${this.appDomain}">${this.appDomain}"</a><br>temporary password: ${ramdomPassword}`;
// メールを送信
await this.sendgridService.sendMail(
context,
email,
from,
this.mailFrom,
subject,
text,
html,
@ -464,17 +470,29 @@ export class UsersService {
);
// DBから取得した各ユーザーをもとにADB2C情報をマージしライセンス情報を算出
const users = dbUsers.map((x) => {
const users = dbUsers.map((dbUser): User => {
// ユーザーの所属グループ名を取得する
const groupNames =
x.userGroupMembers?.map((group) => group.userGroup?.name) ?? [];
const userGroupMembers =
dbUser.userGroupMembers !== null ? dbUser.userGroupMembers : [];
const adb2cUser = adb2cUsers.find((user) => user.id === x.external_id);
//所属グループ名の配列にする
const groupNames = userGroupMembers.flatMap((userGroupMember) =>
userGroupMember.userGroup ? [userGroupMember.userGroup.name] : [],
);
const adb2cUser = adb2cUsers.find(
(user) => user.id === dbUser.external_id,
);
// メールアドレスを取得する
const mail = adb2cUser.identities.find(
(identity) => identity.signInType === ADB2C_SIGN_IN_TYPE.EAMILADDRESS,
).issuerAssignedId;
const mail = adb2cUser?.identities?.find(
(identity) => identity.signInType === ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
)?.issuerAssignedId;
//メールアドレスが取得できない場合はエラー
if (!mail) {
throw new Error('mail not found.');
}
let status = USER_LICENSE_STATUS.NORMAL;
@ -483,21 +501,30 @@ export class UsersService {
let expiration: string | undefined = undefined;
let remaining: number | undefined = undefined;
if (x.license) {
if (dbUser.license) {
// 有効期限日付 YYYY/MM/DD
const expiry_date = x.license.expiry_date;
expiration = `${expiry_date.getFullYear()}/${
const expiry_date = dbUser.license.expiry_date ?? undefined;
expiration =
expiry_date !== undefined
? `${expiry_date.getFullYear()}/${
expiry_date.getMonth() + 1
}/${expiry_date.getDate()}`;
}/${expiry_date.getDate()}`
: undefined;
const currentDate = new DateWithZeroTime();
// 有効期限までの日数
remaining = Math.floor(
remaining =
expiry_date !== undefined
? Math.floor(
(expiry_date.getTime() - currentDate.getTime()) /
(1000 * 60 * 60 * 24),
);
if (remaining <= LICENSE_EXPIRATION_THRESHOLD_DAYS) {
status = x.auto_renew
)
: undefined;
if (
remaining !== undefined &&
remaining <= LICENSE_EXPIRATION_THRESHOLD_DAYS
) {
status = dbUser.auto_renew
? USER_LICENSE_STATUS.RENEW
: USER_LICENSE_STATUS.ALERT;
}
@ -506,18 +533,18 @@ export class UsersService {
}
return {
id: x.id,
id: dbUser.id,
name: adb2cUser.displayName,
role: x.role,
authorId: x.author_id ?? undefined,
role: dbUser.role,
authorId: dbUser.author_id ?? undefined,
typistGroupName: groupNames,
email: mail,
emailVerified: x.email_verified,
autoRenew: x.auto_renew,
licenseAlert: x.license_alert,
notification: x.notification,
encryption: x.encryption,
prompt: x.prompt,
emailVerified: dbUser.email_verified,
autoRenew: dbUser.auto_renew,
licenseAlert: dbUser.license_alert,
notification: dbUser.notification,
encryption: dbUser.encryption,
prompt: dbUser.prompt,
expiration: expiration,
remaining: remaining,
licenseStatus: status,
@ -545,14 +572,13 @@ export class UsersService {
async updateSortCriteria(
paramName: TaskListSortableAttribute,
direction: SortDirection,
token: AccessToken,
externalId: string,
): Promise<void> {
this.logger.log(`[IN] ${this.updateSortCriteria.name}`);
let user: EntityUser;
try {
// ユーザー情報を取得
const sub = token.userId;
user = await this.usersRepository.findUserByExternalId(sub);
user = await this.usersRepository.findUserByExternalId(externalId);
} catch (e) {
this.logger.error(`error=${e}`);
@ -584,7 +610,7 @@ export class UsersService {
* @param token
* @returns sort criteria
*/
async getSortCriteria(token: AccessToken): Promise<{
async getSortCriteria(externalId: string): Promise<{
paramName: TaskListSortableAttribute;
direction: SortDirection;
}> {
@ -592,8 +618,7 @@ export class UsersService {
let user: EntityUser;
try {
// ユーザー情報を取得
const sub = token.userId;
user = await this.usersRepository.findUserByExternalId(sub);
user = await this.usersRepository.findUserByExternalId(externalId);
} catch (e) {
this.logger.error(`error=${e}`);
@ -643,8 +668,8 @@ export class UsersService {
// TODO: PBI2105 本実装時に修正すること
return {
authorId: user.author_id,
authorIdList: [user.author_id, 'XXX'],
authorId: user.author_id ?? '',
authorIdList: [user.author_id ?? '', 'XXX'],
isEncrypted: true,
encryptionPassword: 'abcd@123?dcba',
audioFormat: 'DS2(QP)',
@ -1004,6 +1029,11 @@ export class UsersService {
makeErrorResponse('E010204'),
HttpStatus.BAD_REQUEST,
);
case AccountNotFoundError:
throw new HttpException(
makeErrorResponse('E010501'),
HttpStatus.BAD_REQUEST,
);
case UpdateTermsVersionNotSetError:
throw new HttpException(
makeErrorResponse('E010001'),

View File

@ -49,7 +49,7 @@ export const getWorkflow = async (
datasource: DataSource,
accountId: number,
id: number,
): Promise<Workflow> => {
): Promise<Workflow | null> => {
return await datasource.getRepository(Workflow).findOne({
where: {
account_id: accountId,

View File

@ -66,7 +66,6 @@ export class WorkflowsController {
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
@Get()
async getWorkflows(@Req() req: Request): Promise<GetWorkflowsResponse> {
// TODO strictNullChecks対応
const accessToken = retrieveAuthorizationToken(req);
if (!accessToken) {
throw new HttpException(
@ -123,7 +122,7 @@ export class WorkflowsController {
@Body() body: CreateWorkflowsRequest,
): Promise<CreateWorkflowsResponse> {
const { authorId, worktypeId, templateId, typists } = body;
// TODO strictNullChecks対応
const accessToken = retrieveAuthorizationToken(req);
if (!accessToken) {
throw new HttpException(
@ -188,7 +187,7 @@ export class WorkflowsController {
): Promise<UpdateWorkflowResponse> {
const { authorId, worktypeId, templateId, typists } = body;
const { workflowId } = param;
// TODO strictNullChecks対応
const accessToken = retrieveAuthorizationToken(req);
if (!accessToken) {
throw new HttpException(
@ -252,7 +251,7 @@ export class WorkflowsController {
@Param() param: DeleteWorkflowRequestParam,
): Promise<DeleteWorkflowResponse> {
const { workflowId } = param;
// TODO strictNullChecks対応
const accessToken = retrieveAuthorizationToken(req);
if (!accessToken) {
throw new HttpException(

View File

@ -2062,7 +2062,7 @@ describe('updateWorkflow', () => {
});
describe('deleteWorkflows', () => {
let source: DataSource = null;
let source: DataSource | null = null;
beforeEach(async () => {
source = new DataSource({
type: 'sqlite',
@ -2074,12 +2074,15 @@ describe('deleteWorkflows', () => {
return source.initialize();
});
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: authorId } = await makeTestUser(source, {
@ -2126,7 +2129,9 @@ describe('deleteWorkflows', () => {
});
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, {
@ -2177,7 +2182,9 @@ describe('deleteWorkflows', () => {
});
it('指定されたワークフローが存在しない場合、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, {
@ -2226,7 +2233,9 @@ describe('deleteWorkflows', () => {
});
it('指定されたワークフローが存在しない場合、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, {
@ -2303,7 +2312,9 @@ describe('deleteWorkflows', () => {
});
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, {

View File

@ -57,8 +57,8 @@ export class WorkflowsService {
return workflowTypists;
});
// externalIdsからundefinedを除外
const filteredExternalIds = externalIds.filter(
(externalId): externalId is string => externalId !== undefined,
const filteredExternalIds = externalIds.flatMap((externalId) =>
externalId ? [externalId] : [],
);
// externalIdsから重複を除外
const distinctedExternalIds = [...new Set(filteredExternalIds)];

View File

@ -40,8 +40,8 @@ export class AdB2cService {
// ADB2Cへの認証情報
const credential = new ClientSecretCredential(
this.configService.getOrThrow<string>('ADB2C_TENANT_ID'),
this.configService.getOrThrow('ADB2C_CLIENT_ID'),
this.configService.getOrThrow('ADB2C_CLIENT_SECRET'),
this.configService.getOrThrow<string>('ADB2C_CLIENT_ID'),
this.configService.getOrThrow<string>('ADB2C_CLIENT_SECRET'),
);
const authProvider = new TokenCredentialAuthenticationProvider(credential, {
scopes: ['https://graph.microsoft.com/.default'],
@ -76,7 +76,7 @@ export class AdB2cService {
},
identities: [
{
signinType: ADB2C_SIGN_IN_TYPE.EAMILADDRESS,
signinType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: `${this.tenantName}.onmicrosoft.com`,
issuerAssignedId: email,
},

View File

@ -8,7 +8,13 @@ import { Context } from '../../common/log';
@Injectable()
export class SendGridService {
private readonly logger = new Logger(SendGridService.name);
private readonly emailConfirmLifetime: number;
private readonly appDomain: string;
constructor(private readonly configService: ConfigService) {
this.appDomain = this.configService.getOrThrow<string>('APP_DOMAIN');
this.emailConfirmLifetime = this.configService.getOrThrow<number>(
'EMAIL_CONFIRM_LIFETIME',
);
const key = this.configService.getOrThrow<string>('SENDGRID_API_KEY');
sendgrid.setApiKey(key);
}
@ -30,8 +36,6 @@ export class SendGridService {
`[IN] [${context.trackingId}] ${this.createMailContentFromEmailConfirm.name}`,
);
const lifetime =
this.configService.get<number>('EMAIL_CONFIRM_LIFETIME') ?? 0;
const privateKey = getPrivateKey(this.configService);
const token = sign<{ accountId: number; userId: number; email: string }>(
{
@ -39,10 +43,9 @@ export class SendGridService {
userId,
email,
},
lifetime,
this.emailConfirmLifetime,
privateKey,
);
const domains = this.configService.get<string>('APP_DOMAIN');
const path = 'mail-confirm/';
this.logger.log(
@ -50,8 +53,8 @@ export class SendGridService {
);
return {
subject: 'Verify your new account',
text: `The verification URL. ${domains}${path}?verify=${token}`,
html: `<p>The verification URL.<p><a href="${domains}${path}?verify=${token}">${domains}${path}?verify=${token}"</a>`,
text: `The verification URL. ${this.appDomain}${path}?verify=${token}`,
html: `<p>The verification URL.<p><a href="${this.appDomain}${path}?verify=${token}">${this.appDomain}${path}?verify=${token}"</a>`,
};
}
@ -68,9 +71,6 @@ export class SendGridService {
userId: number,
email: string,
): Promise<{ subject: string; text: string; html: string }> {
const lifetime =
this.configService.get<number>('EMAIL_CONFIRM_LIFETIME') ?? 0;
const privateKey = getPrivateKey(this.configService);
const token = sign<{ accountId: number; userId: number; email: string }>(
@ -79,16 +79,15 @@ export class SendGridService {
userId,
email,
},
lifetime,
this.emailConfirmLifetime,
privateKey,
);
const domains = this.configService.get<string>('APP_DOMAIN');
const path = 'mail-confirm/user/';
return {
subject: 'Verify your new account',
text: `The verification URL. ${domains}${path}?verify=${token}`,
html: `<p>The verification URL.<p><a href="${domains}${path}?verify=${token}">${domains}${path}?verify=${token}"</a>`,
text: `The verification URL. ${this.appDomain}${path}?verify=${token}`,
html: `<p>The verification URL.<p><a href="${this.appDomain}${path}?verify=${token}">${this.appDomain}${path}?verify=${token}"</a>`,
};
}

View File

@ -13,6 +13,7 @@ import {
import { User, UserArchive } from '../users/entity/user.entity';
import { Account } from './entity/account.entity';
import {
CardLicense,
License,
LicenseAllocationHistory,
LicenseAllocationHistoryArchive,
@ -48,6 +49,14 @@ import {
import { DateWithZeroTime } from '../../features/licenses/types/types';
import { Worktype } from '../worktypes/entity/worktype.entity';
import { WorktypeIdNotFoundError } from '../worktypes/errors/types';
import { OptionItem } from '../worktypes/entity/option_item.entity';
import { Task } from '../tasks/entity/task.entity';
import { CheckoutPermission } from '../checkout_permissions/entity/checkout_permission.entity';
import { AudioFile } from '../audio_files/entity/audio_file.entity';
import { AudioOptionItem } from '../audio_option_items/entity/audio_option_item.entity';
import { UserGroup } from '../user_groups/entity/user_group.entity';
import { UserGroupMember } from '../user_groups/entity/user_group_member.entity';
import { TemplateFile } from '../template_files/entity/template_file.entity';
@Injectable()
export class AccountsRepositoryService {
@ -117,13 +126,13 @@ export class AccountsRepositoryService {
tier: number,
adminExternalUserId: string,
adminUserRole: string,
adminUserAcceptedEulaVersion: string,
adminUserAcceptedDpaVersion: string,
adminUserAcceptedEulaVersion?: string,
adminUserAcceptedDpaVersion?: string,
): Promise<{ newAccount: Account; adminUser: User }> {
return await this.dataSource.transaction(async (entityManager) => {
const account = new Account();
{
account.parent_account_id = dealerAccountId;
account.parent_account_id = dealerAccountId ?? null;
account.company_name = companyName;
account.country = country;
account.tier = tier;
@ -138,8 +147,8 @@ export class AccountsRepositoryService {
user.account_id = persistedAccount.id;
user.external_id = adminExternalUserId;
user.role = adminUserRole;
user.accepted_eula_version = adminUserAcceptedEulaVersion;
user.accepted_dpa_version = adminUserAcceptedDpaVersion;
user.accepted_eula_version = adminUserAcceptedEulaVersion ?? null;
user.accepted_dpa_version = adminUserAcceptedDpaVersion ?? null;
}
const usersRepo = entityManager.getRepository(User);
const newUser = usersRepo.create(user);
@ -490,6 +499,9 @@ export class AccountsRepositoryService {
id: id,
},
});
if (!ownAccount) {
throw new AccountNotFoundError();
}
// 自アカウントのライセンス注文状況を取得する
const ownLicenseOrderStatus = await this.getAccountLicenseOrderStatus(
@ -532,8 +544,8 @@ export class AccountsRepositoryService {
);
// 第五の不足数を算出するためのライセンス数情報を取得する
let expiringSoonLicense: number;
let allocatableLicenseWithMargin: number;
let expiringSoonLicense: number = 0;
let allocatableLicenseWithMargin: number = 0;
if (childAccount.tier === TIERS.TIER5) {
expiringSoonLicense = await this.getExpiringSoonLicense(
entityManager,
@ -606,7 +618,7 @@ export class AccountsRepositoryService {
return await this.dataSource.transaction(async (entityManager) => {
const accountRepository = entityManager.getRepository(Account);
const maxTierDifference = TIERS.TIER5 - TIERS.TIER1;
const parentAccountIds = [];
const parentAccountIds: number[] = [];
let currentAccountId = targetAccountId;
// システム的な最大の階層差異分、親を参照する
@ -619,6 +631,9 @@ export class AccountsRepositoryService {
if (!account) {
break;
}
if (!account.parent_account_id) {
throw new Error("Parent account doesn't exist.");
}
parentAccountIds.push(account.parent_account_id);
currentAccountId = account.parent_account_id;
@ -740,11 +755,13 @@ export class AccountsRepositoryService {
});
// ADB2Cから情報を取得するための外部ユーザIDを取得する念のためプライマリ管理者IDが存在しない場合を考慮
const primaryUserIds = partnerAccounts.map((x) => {
const primaryUserIds = partnerAccounts.flatMap((x) => {
if (x.primary_admin_user_id) {
return x.primary_admin_user_id;
return [x.primary_admin_user_id];
} else if (x.secondary_admin_user_id) {
return x.secondary_admin_user_id;
return [x.secondary_admin_user_id];
} else {
return [];
}
});
const userRepo = entityManager.getRepository(User);
@ -761,15 +778,18 @@ export class AccountsRepositoryService {
user.id === account.primary_admin_user_id ||
user.id === account.secondary_admin_user_id,
);
const primaryAccountExternalId = primaryUser
? primaryUser.external_id
: undefined;
if (!primaryUser) {
throw new AdminUserNotFoundError(
`Primary admin user is not found. id: ${account.primary_admin_user_id}, account_id: ${account.id}`,
);
}
return {
name: account.company_name,
tier: account.tier,
accountId: account.id,
country: account.country,
primaryAccountExternalId: primaryAccountExternalId,
primaryAccountExternalId: primaryUser.external_id,
dealerManagement: account.delegation_permission,
};
});
@ -790,7 +810,7 @@ export class AccountsRepositoryService {
async getOneUpperTierAccount(
accountId: number,
tier: number,
): Promise<Account | undefined> {
): Promise<Account | null> {
return await this.dataSource.transaction(async (entityManager) => {
const accountRepo = entityManager.getRepository(Account);
return await accountRepo.findOne({
@ -869,10 +889,10 @@ export class AccountsRepositoryService {
await accountRepo.update(
{ id: myAccountId },
{
parent_account_id: parentAccountId || null,
parent_account_id: parentAccountId ?? null,
delegation_permission: delegationPermission,
primary_admin_user_id: primaryAdminUserId,
secondary_admin_user_id: secondryAdminUserId || null,
secondary_admin_user_id: secondryAdminUserId ?? null,
},
);
});
@ -966,9 +986,107 @@ export class AccountsRepositoryService {
.execute();
// アカウントを削除
// アカウントを削除することで、外部キー制約がで紐づいている関連テーブルのデータも削除される
const accountRepo = entityManager.getRepository(Account);
await accountRepo.delete({ id: accountId });
// ライセンス系(card_license_issue以外)のテーブルのレコードを削除する
const orderRepo = entityManager.getRepository(LicenseOrder);
await orderRepo.delete({
from_account_id: accountId,
});
const licenseRepo = entityManager.getRepository(License);
const targetLicenses = await licenseRepo.find({
where: {
account_id: accountId,
},
});
const cardLicenseRepo = entityManager.getRepository(CardLicense);
await cardLicenseRepo.delete({
license_id: In(targetLicenses.map((license) => license.id)),
});
await licenseRepo.delete({
account_id: accountId,
});
const LicenseAllocationHistoryRepo = entityManager.getRepository(
LicenseAllocationHistory,
);
await LicenseAllocationHistoryRepo.delete({
account_id: accountId,
});
// ワークタイプ系のテーブルのレコードを削除する
const worktypeRepo = entityManager.getRepository(Worktype);
const taggerWorktypes = await worktypeRepo.find({
where: { account_id: accountId },
});
const optionItemRepo = entityManager.getRepository(OptionItem);
await optionItemRepo.delete({
worktype_id: In(taggerWorktypes.map((worktype) => worktype.id)),
});
await worktypeRepo.delete({ account_id: accountId });
// タスク系のテーブルのレコードを削除する
const taskRepo = entityManager.getRepository(Task);
const targetTasks = await taskRepo.find({
where: {
account_id: accountId,
},
});
const checkoutPermissionRepo =
entityManager.getRepository(CheckoutPermission);
await checkoutPermissionRepo.delete({
task_id: In(targetTasks.map((task) => task.id)),
});
await taskRepo.delete({
account_id: accountId,
});
// オーディオファイル系のテーブルのレコードを削除する
const audioFileRepo = entityManager.getRepository(AudioFile);
const targetaudioFiles = await audioFileRepo.find({
where: {
account_id: accountId,
},
});
const audioOptionItemsRepo = entityManager.getRepository(AudioOptionItem);
await audioOptionItemsRepo.delete({
audio_file_id: In(targetaudioFiles.map((audioFile) => audioFile.id)),
});
await audioFileRepo.delete({
account_id: accountId,
});
// ユーザーグループ系のテーブルのレコードを削除する
const userGroupRepo = entityManager.getRepository(UserGroup);
const targetUserGroup = await userGroupRepo.find({
where: {
account_id: accountId,
},
});
const userGroupMemberRepo = entityManager.getRepository(UserGroupMember);
await userGroupMemberRepo.delete({
user_group_id: In(targetUserGroup.map((userGroup) => userGroup.id)),
});
await userGroupRepo.delete({
account_id: accountId,
});
// テンプレートファイルテーブルのレコードを削除する
const templateFileRepo = entityManager.getRepository(TemplateFile);
await templateFileRepo.delete({ account_id: accountId });
// ユーザテーブルのレコードを削除する
const userRepo = entityManager.getRepository(User);
await userRepo.delete({
account_id: accountId,
});
// ソート条件のテーブルのレコードを削除する
const sortCriteriaRepo = entityManager.getRepository(SortCriteria);
await sortCriteriaRepo.delete({
user_id: In(users.map((user) => user.id)),
});
return users;
});
}

View File

@ -13,8 +13,8 @@ export class Account {
@PrimaryGeneratedColumn()
id: number;
@Column({ nullable: true })
parent_account_id?: number;
@Column({ nullable: true, type: 'unsigned big int' })
parent_account_id: number | null;
@Column()
tier: number;
@ -34,30 +34,36 @@ export class Account {
@Column({ default: false })
verified: boolean;
@Column({ nullable: true })
primary_admin_user_id?: number;
@Column({ nullable: true, type: 'unsigned big int' })
primary_admin_user_id: number | null;
@Column({ nullable: true })
secondary_admin_user_id?: number;
@Column({ nullable: true, type: 'unsigned big int' })
secondary_admin_user_id: number | null;
@Column({ nullable: true })
active_worktype_id?: number;
@Column({ nullable: true, type: 'unsigned big int' })
active_worktype_id: number | null;
@Column({ nullable: true })
deleted_at?: Date;
@Column({ nullable: true, type: 'datetime' })
deleted_at: Date | null;
@Column({ nullable: true })
created_by?: string;
@Column({ nullable: true, type: 'datetime' })
created_by: string | null;
@CreateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
@CreateDateColumn({
default: () => "datetime('now', 'localtime')",
type: 'datetime',
}) // defaultはSQLite用設定値.本番用は別途migrationで設定
created_at: Date;
@Column({ nullable: true })
updated_by?: string;
@Column({ nullable: true, type: 'datetime' })
updated_by: string | null;
@UpdateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
@UpdateDateColumn({
default: () => "datetime('now', 'localtime')",
type: 'datetime',
}) // defaultはSQLite用設定値.本番用は別途migrationで設定
updated_at: Date;
@OneToMany(() => User, (user) => user.id)
user?: User[];
user: User[] | null;
}

View File

@ -32,12 +32,12 @@ export class AudioFile {
priority: string;
@Column()
audio_format: string;
@Column({ nullable: true })
comment?: string;
@Column({ nullable: true })
deleted_at?: Date;
@Column({ nullable: true, type: 'varchar' })
comment: string | null;
@Column({ nullable: true, type: 'datetime' })
deleted_at: Date | null;
@Column()
is_encrypted: boolean;
@OneToOne(() => Task, (task) => task.file)
task?: Task;
task: Task | null;
}

View File

@ -19,5 +19,5 @@ export class AudioOptionItem {
value: string;
@ManyToOne(() => Task, (task) => task.audio_file_id)
@JoinColumn({ name: 'audio_file_id' })
task?: Task;
task: Task | null;
}

View File

@ -18,21 +18,21 @@ export class CheckoutPermission {
@Column({})
task_id: number;
@Column({ nullable: true })
user_id?: number;
@Column({ nullable: true, type: 'unsigned big int' })
user_id: number | null;
@Column({ nullable: true })
user_group_id?: number;
@Column({ nullable: true, type: 'unsigned big int' })
user_group_id: number | null;
@OneToOne(() => User, (user) => user.id)
@JoinColumn({ name: 'user_id' })
user?: User;
user: User | null;
@OneToOne(() => UserGroup, (group) => group.id)
@JoinColumn({ name: 'user_group_id' })
user_group?: UserGroup;
user_group: UserGroup | null;
@ManyToOne(() => Task, (task) => task.id)
@JoinColumn({ name: 'task_id' })
task?: Task;
task: Task | null;
}

View File

@ -25,11 +25,14 @@ export class LicenseOrder {
@Column()
to_account_id: number;
@CreateDateColumn()
@CreateDateColumn({
default: () => "datetime('now', 'localtime')",
type: 'datetime',
})
ordered_at: Date;
@Column({ nullable: true })
issued_at?: Date;
@Column({ nullable: true, type: 'datetime' })
issued_at: Date | null;
@Column()
quantity: number;
@ -37,19 +40,25 @@ export class LicenseOrder {
@Column()
status: string;
@Column({ nullable: true })
canceled_at?: Date;
@Column({ nullable: true, type: 'datetime' })
canceled_at: Date | null;
@Column({ nullable: true })
created_by: string;
@Column({ nullable: true, type: 'datetime' })
created_by: string | null;
@CreateDateColumn()
@CreateDateColumn({
default: () => "datetime('now', 'localtime')",
type: 'datetime',
})
created_at: Date;
@Column({ nullable: true })
updated_by: string;
@Column({ nullable: true, type: 'datetime' })
updated_by: string | null;
@UpdateDateColumn()
@UpdateDateColumn({
default: () => "datetime('now', 'localtime')",
type: 'datetime',
})
updated_at: Date;
}
@ -58,8 +67,8 @@ export class License {
@PrimaryGeneratedColumn()
id: number;
@Column({ nullable: true })
expiry_date: Date;
@Column({ nullable: true, type: 'datetime' })
expiry_date: Date | null;
@Column()
account_id: number;
@ -70,33 +79,41 @@ export class License {
@Column()
status: string;
@Column({ nullable: true })
allocated_user_id: number;
@Column({ nullable: true, type: 'unsigned big int' })
allocated_user_id: number | null;
@Column({ nullable: true })
order_id: number;
@Column({ nullable: true, type: 'unsigned big int' })
order_id: number | null;
@Column({ nullable: true })
deleted_at: Date;
@Column({ nullable: true, type: 'datetime' })
deleted_at: Date | null;
@Column({ nullable: true })
delete_order_id: number;
@Column({ nullable: true, type: 'unsigned big int' })
delete_order_id: number | null;
@Column({ nullable: true })
created_by: string;
@Column({ nullable: true, type: 'datetime' })
created_by: string | null;
@CreateDateColumn()
@CreateDateColumn({
default: () => "datetime('now', 'localtime')",
type: 'datetime',
})
created_at: Date;
@Column({ nullable: true })
updated_by: string;
@Column({ nullable: true, type: 'datetime' })
updated_by: string | null;
@UpdateDateColumn()
@UpdateDateColumn({
default: () => "datetime('now', 'localtime')",
type: 'datetime',
})
updated_at: Date;
@OneToOne(() => User, (user) => user.license)
@OneToOne(() => User, (user) => user.license, {
createForeignKeyConstraints: false,
}) // createForeignKeyConstraintsはSQLite用設定値.本番用は別途migrationで設定
@JoinColumn({ name: 'allocated_user_id' })
user?: User;
user: User | null;
}
@Entity({ name: 'card_license_issue' })
@ -107,16 +124,22 @@ export class CardLicenseIssue {
@Column()
issued_at: Date;
@Column({ nullable: true })
created_by: string;
@Column({ nullable: true, type: 'datetime' })
created_by: string | null;
@CreateDateColumn()
@CreateDateColumn({
default: () => "datetime('now', 'localtime')",
type: 'datetime',
})
created_at: Date;
@Column({ nullable: true })
updated_by: string;
@Column({ nullable: true, type: 'datetime' })
updated_by: string | null;
@UpdateDateColumn()
@UpdateDateColumn({
default: () => "datetime('now', 'localtime')",
type: 'datetime',
})
updated_at: Date;
}
@ -131,19 +154,25 @@ export class CardLicense {
@Column()
card_license_key: string;
@Column({ nullable: true })
activated_at: Date;
@Column({ nullable: true, type: 'datetime' })
activated_at: Date | null;
@Column({ nullable: true })
created_by: string;
@Column({ nullable: true, type: 'datetime' })
created_by: string | null;
@CreateDateColumn()
@CreateDateColumn({
default: () => "datetime('now', 'localtime')",
type: 'datetime',
})
created_at: Date;
@Column({ nullable: true })
updated_by: string;
@Column({ nullable: true, type: 'datetime' })
updated_by: string | null;
@UpdateDateColumn({})
@UpdateDateColumn({
default: () => "datetime('now', 'localtime')",
type: 'datetime',
})
updated_at: Date;
}
@ -170,24 +199,32 @@ export class LicenseAllocationHistory {
@Column()
switch_from_type: string;
@Column({ nullable: true })
deleted_at: Date;
@Column({ nullable: true, type: 'datetime' })
deleted_at: Date | null;
@Column({ nullable: true })
created_by: string;
@Column({ nullable: true, type: 'datetime' })
created_by: string | null;
@CreateDateColumn()
@CreateDateColumn({
default: () => "datetime('now', 'localtime')",
type: 'datetime',
})
created_at: Date;
@Column({ nullable: true })
updated_by: string;
@Column({ nullable: true, type: 'datetime' })
updated_by: string | null;
@UpdateDateColumn()
@UpdateDateColumn({
default: () => "datetime('now', 'localtime')",
type: 'datetime',
})
updated_at: Date;
@ManyToOne(() => License, (licenses) => licenses.id)
@ManyToOne(() => License, (licenses) => licenses.id, {
createForeignKeyConstraints: false,
}) // createForeignKeyConstraintsはSQLite用設定値.本番用は別途migrationで設定
@JoinColumn({ name: 'license_id' })
license?: License;
license: License | null;
}
@Entity({ name: 'licenses_archive' })
@ -195,8 +232,8 @@ export class LicenseArchive {
@PrimaryColumn()
id: number;
@Column({ nullable: true })
expiry_date: Date;
@Column({ nullable: true, type: 'datetime' })
expiry_date: Date | null;
@Column()
account_id: number;
@ -207,31 +244,34 @@ export class LicenseArchive {
@Column()
status: string;
@Column({ nullable: true })
allocated_user_id: number;
@Column({ nullable: true, type: 'unsigned big int' })
allocated_user_id: number | null;
@Column({ nullable: true })
order_id: number;
@Column({ nullable: true, type: 'unsigned big int' })
order_id: number | null;
@Column({ nullable: true })
deleted_at: Date;
@Column({ nullable: true, type: 'datetime' })
deleted_at: Date | null;
@Column({ nullable: true })
delete_order_id: number;
@Column({ nullable: true, type: 'unsigned big int' })
delete_order_id: number | null;
@Column({ nullable: true })
created_by: string;
@Column({ nullable: true, type: 'datetime' })
created_by: string | null;
@Column()
created_at: Date;
@Column({ nullable: true })
updated_by: string;
@Column({ nullable: true, type: 'datetime' })
updated_by: string | null;
@Column()
updated_at: Date;
@CreateDateColumn()
@CreateDateColumn({
default: () => "datetime('now', 'localtime')",
type: 'datetime',
})
archived_at: Date;
}
@ -258,21 +298,24 @@ export class LicenseAllocationHistoryArchive {
@Column()
switch_from_type: string;
@Column({ nullable: true })
deleted_at: Date;
@Column({ nullable: true, type: 'datetime' })
deleted_at: Date | null;
@Column({ nullable: true })
created_by: string;
@Column({ nullable: true, type: 'datetime' })
created_by: string | null;
@Column()
created_at: Date;
@Column({ nullable: true })
updated_by: string;
@Column({ nullable: true, type: 'datetime' })
updated_by: string | null;
@Column()
updated_at: Date;
@CreateDateColumn()
@CreateDateColumn({
default: () => "datetime('now', 'localtime')",
type: 'datetime',
})
archived_at: Date;
}

View File

@ -444,7 +444,7 @@ export class LicensesRepositoryService {
const allocatableLicenses = await queryBuilder.getMany();
return allocatableLicenses.map((license) => ({
licenseId: license.id,
expiryDate: license.expiry_date,
expiryDate: license.expiry_date ?? undefined,
}));
}
/**
@ -469,6 +469,13 @@ export class LicensesRepositoryService {
},
});
// ライセンスが存在しない場合はエラー
if (!targetLicense) {
throw new LicenseNotExistError(
`License not exist. licenseId: ${newLicenseId}`,
);
}
// 期限切れの場合はエラー
if (targetLicense.expiry_date) {
const currentDay = new Date();
@ -533,7 +540,7 @@ export class LicensesRepositoryService {
});
let switchFromType = '';
if (oldLicenseType) {
if (oldLicenseType && oldLicenseType.license) {
switch (oldLicenseType.license.type) {
case LICENSE_TYPE.CARD:
switchFromType = SWITCH_FROM_TYPE.CARD;

View File

@ -20,33 +20,33 @@ export class Task {
job_number: string;
@Column()
account_id: number;
@Column({ nullable: true })
is_job_number_enabled?: boolean;
@Column({ nullable: true, type: 'tinyint' })
is_job_number_enabled: boolean | null;
@Column()
audio_file_id: number;
@Column()
status: string;
@Column({ nullable: true })
typist_user_id?: number;
@Column({ nullable: true, type: 'unsigned big int' })
typist_user_id: number | null;
@Column()
priority: string;
@Column({ nullable: true })
template_file_id?: number;
@Column({ nullable: true })
started_at?: Date;
@Column({ nullable: true })
finished_at?: Date;
@Column({ nullable: true, type: 'unsigned big int' })
template_file_id: number | null;
@Column({ nullable: true, type: 'datetime' })
started_at: Date | null;
@Column({ nullable: true, type: 'datetime' })
finished_at: Date | null;
@Column({})
created_at: Date;
@OneToOne(() => AudioFile, (audiofile) => audiofile.task)
@JoinColumn({ name: 'audio_file_id' })
file?: AudioFile;
file: AudioFile | null;
@OneToMany(() => AudioOptionItem, (option) => option.task)
option_items?: AudioOptionItem[];
option_items: AudioOptionItem[] | null;
@OneToOne(() => User, (user) => user.id)
@JoinColumn({ name: 'typist_user_id' })
typist_user?: User;
typist_user: User | null;
@ManyToOne(() => TemplateFile, (templateFile) => templateFile.id)
@JoinColumn({ name: 'template_file_id' })
template_file?: TemplateFile;
template_file: TemplateFile | null;
}

View File

@ -757,7 +757,7 @@ export class TasksRepositoryService {
*/
async changeCheckoutPermission(
audio_file_id: number,
author_id: string,
author_id: string | undefined,
account_id: number,
roles: Roles[],
assignees: Assignee[],
@ -844,8 +844,8 @@ export class TasksRepositoryService {
(assignee) => {
const checkoutPermission = new CheckoutPermission();
checkoutPermission.task_id = taskRecord.id;
checkoutPermission.user_id = assignee.typistUserId;
checkoutPermission.user_group_id = assignee.typistGroupId;
checkoutPermission.user_id = assignee.typistUserId ?? null;
checkoutPermission.user_group_id = assignee.typistGroupId ?? null;
return checkoutPermission;
},
);

View File

@ -18,11 +18,11 @@ export class TemplateFile {
url: string;
@Column()
file_name: string;
@Column({ nullable: true })
@Column({ nullable: true, type: 'datetime' })
created_by: string | null;
@CreateDateColumn()
created_at: Date;
@Column({ nullable: true })
@Column({ nullable: true, type: 'datetime' })
updated_by: string | null;
@UpdateDateColumn()
updated_at: Date;

View File

@ -17,15 +17,21 @@ export class Term {
@Column()
version: string;
@Column({ nullable: true })
created_by: string;
@Column({ nullable: true, type: 'datetime' })
created_by: string | null;
@CreateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
@CreateDateColumn({
default: () => "datetime('now', 'localtime')",
type: 'datetime',
}) // defaultはSQLite用設定値.本番用は別途migrationで設定
created_at: Date;
@Column({ nullable: true })
updated_by?: string;
@Column({ nullable: true, type: 'varchar' })
updated_by: string | null;
@UpdateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
@UpdateDateColumn({
default: () => "datetime('now', 'localtime')",
type: 'datetime',
}) // defaultはSQLite用設定値.本番用は別途migrationで設定
updated_at: Date;
}

Some files were not shown because too many files have changed in this diff Show More