Merged PR 301: ユーザー追加修正(API/画面)

## 概要
[Task2327: ユーザー追加修正(API/画面)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2327)

- ユーザー追加API
  - リクエストにencryption,encryptionPassword,promptを追加
  - リクエストからtypistGroupIdを削除
  - ロールに応じてDBに保存するデータを作成する処理を追加
  - リクエストパラメータのバリデーションチェックを追加
- ユーザー追加画面
  - TypistGroupの選択欄を削除
  - RoleがAuthorの場合、encryption,encryptionPassword,promptを追加

## レビューポイント
- 修正に不足はないか
- 画面のユーザー追加処理を引数を渡さずにstoreから取得するようにしたが問題ないか
- 画面に必要な値をまとめて取るようにしたが問題ないか
- デザインに差異はないか

## UIの変更
- https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task2327?csf=1&web=1&e=uvTYlb

## 動作確認状況
- ローカルで確認

## 補足
- 相談、参考資料などがあれば
This commit is contained in:
saito.k 2023-08-07 00:11:54 +00:00
parent 338d6b88a9
commit 4565d59a51
23 changed files with 856 additions and 255 deletions

View File

@ -880,6 +880,67 @@ export interface PostSortCriteriaRequest {
*/
'paramName': string;
}
/**
*
* @export
* @interface PostUpdateUserRequest
*/
export interface PostUpdateUserRequest {
/**
*
* @type {number}
* @memberof PostUpdateUserRequest
*/
'id': number;
/**
* none/author/typist
* @type {string}
* @memberof PostUpdateUserRequest
*/
'role': string;
/**
*
* @type {string}
* @memberof PostUpdateUserRequest
*/
'authorId'?: string;
/**
*
* @type {boolean}
* @memberof PostUpdateUserRequest
*/
'autoRenew': boolean;
/**
*
* @type {boolean}
* @memberof PostUpdateUserRequest
*/
'licenseAlart': boolean;
/**
*
* @type {boolean}
* @memberof PostUpdateUserRequest
*/
'notification': boolean;
/**
*
* @type {boolean}
* @memberof PostUpdateUserRequest
*/
'encryption'?: boolean;
/**
*
* @type {string}
* @memberof PostUpdateUserRequest
*/
'encryptionPassword'?: string;
/**
*
* @type {boolean}
* @memberof PostUpdateUserRequest
*/
'prompt'?: boolean;
}
/**
*
* @export
@ -923,12 +984,6 @@ export interface SignupRequest {
* @memberof SignupRequest
*/
'authorId'?: string;
/**
*
* @type {number}
* @memberof SignupRequest
*/
'typistGroupId'?: number;
/**
*
* @type {string}
@ -953,6 +1008,24 @@ export interface SignupRequest {
* @memberof SignupRequest
*/
'notification': boolean;
/**
*
* @type {boolean}
* @memberof SignupRequest
*/
'encryption'?: boolean;
/**
*
* @type {string}
* @memberof SignupRequest
*/
'encryptionPassword'?: string;
/**
*
* @type {boolean}
* @memberof SignupRequest
*/
'prompt'?: boolean;
}
/**
*
@ -3852,6 +3925,46 @@ export const UsersApiAxiosParamCreator = function (configuration?: Configuration
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(postSortCriteriaRequest, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @summary
* @param {PostUpdateUserRequest} postUpdateUserRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateUser: async (postUpdateUserRequest: PostUpdateUserRequest, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'postUpdateUserRequest' is not null or undefined
assertParamExists('updateUser', 'postUpdateUserRequest', postUpdateUserRequest)
const localVarPath = `/users/update`;
// 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)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(postUpdateUserRequest, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
@ -3941,6 +4054,17 @@ export const UsersApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.updateSortCriteria(postSortCriteriaRequest, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @summary
* @param {PostUpdateUserRequest} postUpdateUserRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async updateUser(postUpdateUserRequest: PostUpdateUserRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.updateUser(postUpdateUserRequest, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
}
};
@ -4018,6 +4142,16 @@ export const UsersApiFactory = function (configuration?: Configuration, basePath
updateSortCriteria(postSortCriteriaRequest: PostSortCriteriaRequest, options?: any): AxiosPromise<object> {
return localVarFp.updateSortCriteria(postSortCriteriaRequest, options).then((request) => request(axios, basePath));
},
/**
*
* @summary
* @param {PostUpdateUserRequest} postUpdateUserRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateUser(postUpdateUserRequest: PostUpdateUserRequest, options?: any): AxiosPromise<object> {
return localVarFp.updateUser(postUpdateUserRequest, options).then((request) => request(axios, basePath));
},
};
};
@ -4108,6 +4242,18 @@ export class UsersApi extends BaseAPI {
public updateSortCriteria(postSortCriteriaRequest: PostSortCriteriaRequest, options?: AxiosRequestConfig) {
return UsersApiFp(this.configuration).updateSortCriteria(postSortCriteriaRequest, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary
* @param {PostUpdateUserRequest} postUpdateUserRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof UsersApi
*/
public updateUser(postUpdateUserRequest: PostUpdateUserRequest, options?: AxiosRequestConfig) {
return UsersApiFp(this.configuration).updateUser(postUpdateUserRequest, options).then((request) => request(this.axios, this.basePath));
}
}

View File

@ -1,8 +1,9 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import type { RootState } from "app/store";
import { getTranslationID } from "translation";
import { USER_ROLES } from "components/auth/constants";
import { openSnackbar } from "features/ui/uiSlice";
import { SignupRequest, UsersApi, GetUsersResponse } from "../../api/api";
import { getTranslationID } from "translation";
import { GetUsersResponse, UsersApi } from "../../api/api";
import { Configuration } from "../../api/configuration";
import { ErrorObject, createErrorObject } from "../../common/errors";
@ -43,7 +44,7 @@ export const addUserAsync = createAsyncThunk<
{
/* Empty Object */
},
SignupRequest,
void,
{
// rejectした時の返却値の型
rejectValue: {
@ -51,40 +52,25 @@ export const addUserAsync = createAsyncThunk<
};
}
>("users/addUserAsync", async (args, thunkApi) => {
const {
name,
email,
role,
authorId,
typistGroupId,
autoRenew,
licenseAlert,
notification,
} = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration, accessToken } = state.auth;
const config = new Configuration(configuration);
const usersApi = new UsersApi(config);
const { addUser } = state.user.apps;
// roleがAUTHOR以外の場合、不要なプロパティをundefinedにする
if (addUser.role !== USER_ROLES.AUTHOR) {
addUser.authorId = undefined;
addUser.encryption = undefined;
addUser.prompt = undefined;
addUser.encryptionPassword = undefined;
}
try {
await usersApi.signup(
{
name,
email,
role,
authorId,
typistGroupId,
autoRenew,
licenseAlert,
notification,
},
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
await usersApi.signup(addUser, {
headers: { authorization: `Bearer ${accessToken}` },
});
thunkApi.dispatch(
openSnackbar({
level: "info",

View File

@ -1,15 +1,24 @@
import { RootState } from "app/store";
import { USER_ROLES } from "components/auth/constants";
import { RoleType, UserView, isLicenseStatusType, isRoleType } from "./types";
import {
AddUser,
RoleType,
UserView,
isLicenseStatusType,
isRoleType,
} from "./types";
import { LICENSE_STATUS } from "./constants";
export const selectInputValidationErrors = (state: RootState) => {
const { name, email, role, authorId } = state.user.apps.addUser;
const { name, email, role, authorId, encryption, encryptionPassword } =
state.user.apps.addUser;
// 必須項目のチェック
const hasErrorEmptyName = name === "";
const hasErrorEmptyEmail = email === "";
const hasErrorEmptyAuthorId = role === USER_ROLES.AUTHOR && authorId === "";
// Authorの場合、AuthorIDが必須(空文字,undefinedは不可)
const hasErrorEmptyAuthorId =
role === USER_ROLES.AUTHOR && (authorId === "" || !authorId);
const hasErrorIncorrectAuthorId = checkErrorIncorrectAuthorId(
authorId ?? undefined,
@ -18,14 +27,46 @@ export const selectInputValidationErrors = (state: RootState) => {
const hasErrorIncorrectEmail = email.match(/^[^@]+@[^@]+$/)?.length !== 1;
const hasErrorIncorrectEncryptionPassword =
checkErrorIncorrectEncryptionPassword(encryptionPassword, role, encryption);
return {
hasErrorEmptyName,
hasErrorEmptyEmail,
hasErrorEmptyAuthorId,
hasErrorIncorrectEmail,
hasErrorIncorrectAuthorId,
hasErrorIncorrectEncryptionPassword,
};
};
// encreyptionPasswordのチェック
const checkErrorIncorrectEncryptionPassword = (
encryptionPassword: string | undefined,
role: RoleType,
encryption: boolean | undefined
): boolean => {
// roleがAuthor以外の場合、チェックしない
if (role !== USER_ROLES.AUTHOR) {
return false;
}
// roleがAuthorかつencryptionがfalseの場合、チェックしない
if (!encryption) {
return false;
}
// encryptionPasswordがundefined,空文字の場合、エラー
if (!encryptionPassword || encryptionPassword === "") {
return true;
}
// encryptionPasswordがルールに則していない場合、エラー
const regex = /^[!-~]{4,16}$/;
if (!regex.test(encryptionPassword)) {
return true;
}
// チェックを通ったらエラーではない
return false;
};
export const checkErrorIncorrectAuthorId = (
authorId: string | undefined,
role: string
@ -46,14 +87,15 @@ export const selectEmail = (state: RootState) => state.user.apps.addUser.email;
export const selectRole = (state: RootState) => state.user.apps.addUser.role;
export const selectAuthorId = (state: RootState) =>
state.user.apps.addUser.authorId;
export const selectTypistGroupId = (state: RootState) =>
state.user.apps.addUser.typistGroupId;
export const selectAutoRenew = (state: RootState) =>
state.user.apps.addUser.autoRenew;
export const selectLicenseAlert = (state: RootState) =>
state.user.apps.addUser.licenseAlert;
export const selectNtotification = (state: RootState) =>
export const selectNotification = (state: RootState) =>
state.user.apps.addUser.notification;
// AddUserを返却する
export const selectAddUser = (state: RootState): AddUser =>
state.user.apps.addUser;
// usersからUserViewに変換して返却する
export const selectUserViews = (state: RootState): UserView[] => {
const { users } = state.user.domain;

View File

@ -1,4 +1,5 @@
import { User } from "../../api/api";
import { AddUser } from "./types";
export interface UsersState {
domain: Domain;
@ -13,7 +14,3 @@ export interface Apps {
addUser: AddUser;
isLoading: boolean;
}
export interface AddUser extends Omit<User, "id"> {
typistGroupId?: number | undefined;
}

View File

@ -23,6 +23,18 @@ export interface UserView
expiration: string;
remaining: number | string;
}
export interface AddUser {
name: string;
role: RoleType;
email: string;
autoRenew: boolean;
licenseAlert: boolean;
notification: boolean;
authorId?: string;
encryption?: boolean;
encryptionPassword?: string;
prompt?: boolean;
}
export type RoleType = typeof USER_ROLES[keyof typeof USER_ROLES];

View File

@ -10,16 +10,14 @@ const initialState: UsersState = {
addUser: {
name: "",
role: USER_ROLES.NONE,
authorId: "",
typistGroupName: [],
email: "",
emailVerified: true,
autoRenew: true,
licenseAlert: true,
notification: true,
encryption: true,
licenseStatus: "",
authorId: "",
encryption: false,
prompt: false,
encryptionPassword: "",
},
isLoading: false,
},
@ -48,13 +46,6 @@ export const userSlice = createSlice({
const { authorId } = action.payload;
state.apps.addUser.authorId = authorId;
},
changeTypistGroupId: (
state,
action: PayloadAction<{ typistGroupId: number | undefined }>
) => {
const { typistGroupId } = action.payload;
state.apps.addUser.typistGroupId = typistGroupId;
},
changeAutoRenew: (state, action: PayloadAction<{ autoRenew: boolean }>) => {
const { autoRenew } = action.payload;
state.apps.addUser.autoRenew = autoRenew;
@ -66,6 +57,24 @@ export const userSlice = createSlice({
const { licenseAlert } = action.payload;
state.apps.addUser.licenseAlert = licenseAlert;
},
changeEncryption: (
state,
action: PayloadAction<{ encryption: boolean }>
) => {
const { encryption } = action.payload;
state.apps.addUser.encryption = encryption;
},
changePrompt: (state, action: PayloadAction<{ prompt: boolean }>) => {
const { prompt } = action.payload;
state.apps.addUser.prompt = prompt;
},
changeEncryptionPassword: (
state,
action: PayloadAction<{ encryptionPassword: string }>
) => {
const { encryptionPassword } = action.payload;
state.apps.addUser.encryptionPassword = encryptionPassword;
},
changeNotification: (
state,
action: PayloadAction<{ notification: boolean }>
@ -105,11 +114,13 @@ export const {
changeEmail,
changeRole,
changeAuthorId,
changeTypistGroupId,
changeAutoRenew,
changeLicenseAlert,
changeNotification,
cleanupAddUser,
changeEncryption,
changePrompt,
changeEncryptionPassword,
} = userSlice.actions;
export default userSlice.reducer;

View File

@ -9,22 +9,17 @@ import {
changeName,
changeRole,
changeAuthorId,
changeTypistGroupId,
changeAutoRenew,
changeLicenseAlert,
changeNotification,
cleanupAddUser,
selectName,
selectEmail,
selectRole,
selectAuthorId,
selectTypistGroupId,
selectInputValidationErrors,
selectAutoRenew,
selectLicenseAlert,
selectNtotification,
addUserAsync,
selectAddUser,
selectInputValidationErrors,
selectIsLoading,
changePrompt,
changeEncryption,
changeEncryptionPassword,
} from "features/user";
import { USER_ROLES } from "components/auth/constants";
import close from "../../assets/images/close.svg";
@ -39,6 +34,9 @@ export const UserAddPopup: React.FC<UserAddPopupProps> = (props) => {
const { isOpen, onClose } = props;
const dispatch: AppDispatch = useDispatch();
const { t } = useTranslation();
const [isPasswordHide, setIsPasswordHide] = useState<boolean>(true);
// AddUserの情報を取得
const addUser = useSelector(selectAddUser);
const closePopup = useCallback(() => {
setIsPushCreateButton(false);
@ -52,17 +50,9 @@ export const UserAddPopup: React.FC<UserAddPopupProps> = (props) => {
hasErrorEmptyAuthorId,
hasErrorIncorrectEmail,
hasErrorIncorrectAuthorId,
hasErrorIncorrectEncryptionPassword,
} = useSelector(selectInputValidationErrors);
const name = useSelector(selectName);
const email = useSelector(selectEmail);
const role = useSelector(selectRole);
const authorId = useSelector(selectAuthorId);
const typistGroupId = useSelector(selectTypistGroupId);
const autoRenew = useSelector(selectAutoRenew);
const licenseAlert = useSelector(selectLicenseAlert);
const notification = useSelector(selectNtotification);
const [isPushCreateButton, setIsPushCreateButton] = useState<boolean>(false);
const isLoading = useSelector(selectIsLoading);
@ -74,51 +64,27 @@ export const UserAddPopup: React.FC<UserAddPopupProps> = (props) => {
hasErrorEmptyEmail ||
hasErrorEmptyAuthorId ||
hasErrorIncorrectEmail ||
hasErrorIncorrectAuthorId
hasErrorIncorrectAuthorId ||
hasErrorIncorrectEncryptionPassword
) {
return;
}
if (role !== USER_ROLES.AUTHOR) {
changeAuthorId({ authorId: undefined });
}
if (role !== USER_ROLES.TYPIST) {
changeTypistGroupId({ typistGroupId: undefined });
}
const { meta } = await dispatch(
addUserAsync({
name,
email,
role,
authorId:
role === USER_ROLES.AUTHOR ? authorId ?? undefined : undefined,
typistGroupId: role === USER_ROLES.TYPIST ? typistGroupId : undefined,
autoRenew,
licenseAlert,
notification,
})
);
const { meta } = await dispatch(addUserAsync());
setIsPushCreateButton(false);
if (meta.requestStatus === "fulfilled") {
closePopup();
}
}, [
dispatch,
closePopup,
hasErrorEmptyName,
hasErrorEmptyEmail,
hasErrorEmptyAuthorId,
hasErrorIncorrectEmail,
hasErrorIncorrectAuthorId,
name,
email,
role,
authorId,
typistGroupId,
autoRenew,
licenseAlert,
notification,
hasErrorIncorrectEncryptionPassword,
dispatch,
closePopup,
]);
return (
@ -142,7 +108,7 @@ export const UserAddPopup: React.FC<UserAddPopupProps> = (props) => {
size={40}
maxLength={255}
className={styles.formInput}
value={name}
value={addUser.name}
onChange={(e) => {
dispatch(changeName({ name: e.target.value }));
}}
@ -160,7 +126,7 @@ export const UserAddPopup: React.FC<UserAddPopupProps> = (props) => {
size={40}
maxLength={255}
className={styles.formInput}
value={email}
value={addUser.email}
onChange={(e) => {
dispatch(changeEmail({ email: e.target.value }));
}}
@ -182,10 +148,11 @@ export const UserAddPopup: React.FC<UserAddPopupProps> = (props) => {
<dd>
<label htmlFor={USER_ROLES.AUTHOR}>
<input
id={USER_ROLES.AUTHOR}
type="radio"
name="role"
className={styles.formRadio}
checked={role === USER_ROLES.AUTHOR}
checked={addUser.role === USER_ROLES.AUTHOR}
onChange={() => {
dispatch(changeRole({ role: USER_ROLES.AUTHOR }));
}}
@ -194,10 +161,11 @@ export const UserAddPopup: React.FC<UserAddPopupProps> = (props) => {
</label>
<label htmlFor={USER_ROLES.TYPIST}>
<input
id={USER_ROLES.TYPIST}
type="radio"
name="role"
className={styles.formRadio}
checked={role === USER_ROLES.TYPIST}
checked={addUser.role === USER_ROLES.TYPIST}
onChange={() => {
dispatch(changeRole({ role: USER_ROLES.TYPIST }));
}}
@ -206,10 +174,11 @@ export const UserAddPopup: React.FC<UserAddPopupProps> = (props) => {
</label>
<label htmlFor={USER_ROLES.NONE}>
<input
id={USER_ROLES.NONE}
type="radio"
name="role"
className={styles.formRadio}
checked={role === USER_ROLES.NONE}
checked={addUser.role === USER_ROLES.NONE}
onChange={() => {
dispatch(changeRole({ role: USER_ROLES.NONE }));
}}
@ -218,7 +187,7 @@ export const UserAddPopup: React.FC<UserAddPopupProps> = (props) => {
</label>
</dd>
{/** Author 選択時に表示 */}
{role === USER_ROLES.AUTHOR && (
{addUser.role === USER_ROLES.AUTHOR && (
<div className={styles.slideSet} id="">
<dt>{t(getTranslationID("userListPage.label.authorID"))}</dt>
<dd>
@ -227,7 +196,7 @@ export const UserAddPopup: React.FC<UserAddPopupProps> = (props) => {
size={40}
maxLength={16}
className={styles.formInput}
value={authorId ?? undefined}
value={addUser.authorId ?? undefined}
onChange={(e) => {
dispatch(changeAuthorId({ authorId: e.target.value }));
}}
@ -249,38 +218,88 @@ export const UserAddPopup: React.FC<UserAddPopupProps> = (props) => {
</span>
)}
</dd>
</div>
)}
{/** Transcriptionist 選択時に表示 */}
{role === USER_ROLES.TYPIST && (
<div className={styles.slideSet} id="">
<dt className={`${styles.overLine} ${styles.marginBtm0}`}>
{t(getTranslationID("userListPage.label.addToGroup"))}
</dt>
<dt>{t(getTranslationID("userListPage.label.encryption"))}</dt>
<dd>
<select
className={styles.formInput}
onChange={(e) => {
dispatch(
changeTypistGroupId({
typistGroupId: Number(e.target.value),
})
);
}}
>
{/** XXX: タイピストグループは後ほど差し替える */}
<option value={0}>
{t(getTranslationID("userListPage.label.selectGroup"))}
</option>
<option value={1}>Tgroup A</option>
<option value={2}>Tgroup B</option>
<option value={3}>Tgroup C</option>
</select>
<label htmlFor="encryption">
<input
type="checkbox"
className={styles.formCheck}
checked={addUser.encryption}
onChange={(e) => {
dispatch(
changeEncryption({ encryption: e.target.checked })
);
}}
/>
</label>
{addUser.encryption && (
<p className={`${styles.encryptionPass} ${styles.isShow}`}>
{t(
getTranslationID(
"userListPage.label.encryptionPassword"
)
)}
<input
type={isPasswordHide ? "password" : "text"}
size={40}
maxLength={16}
className={`${styles.formInput} ${styles.password}`}
value={addUser.encryptionPassword ?? undefined}
onChange={(e) => {
dispatch(
changeEncryptionPassword({
encryptionPassword: e.target.value,
})
);
}}
/>
<span
className={styles.formIconEye}
onClick={() => {
setIsPasswordHide(!isPasswordHide);
}}
onKeyDown={() => {
setIsPasswordHide(!isPasswordHide);
}}
role="button"
tabIndex={0}
aria-label="IconEye"
/>
{isPushCreateButton &&
hasErrorIncorrectEncryptionPassword && (
<span className={styles.formError}>
{t(
getTranslationID(
"userListPage.message.encryptionPasswordCorrectError"
)
)}
</span>
)}
<span className={styles.formComment}>
{t(
getTranslationID(
"userListPage.label.encryptionPasswordTerm"
)
)}
</span>
</p>
)}
</dd>
<dt>{t(getTranslationID("userListPage.label.prompt"))}</dt>
<dd>
<label htmlFor="prompt">
<input
type="checkbox"
className={styles.formCheck}
checked={addUser.prompt}
onChange={(e) => {
dispatch(changePrompt({ prompt: e.target.checked }));
}}
/>
</label>
</dd>
</div>
)}
<dt>{t(getTranslationID("userListPage.label.setting"))}</dt>
<dd className={styles.last}>
<p>
@ -288,7 +307,7 @@ export const UserAddPopup: React.FC<UserAddPopupProps> = (props) => {
<input
type="checkbox"
className={styles.formCheck}
checked={autoRenew}
checked={addUser.autoRenew}
onChange={(e) => {
dispatch(
changeAutoRenew({ autoRenew: e.target.checked })
@ -302,7 +321,7 @@ export const UserAddPopup: React.FC<UserAddPopupProps> = (props) => {
<label htmlFor="License Alert">
<input
type="checkbox"
checked={licenseAlert}
checked={addUser.licenseAlert}
className={styles.formCheck}
onChange={(e) => {
dispatch(
@ -317,7 +336,7 @@ export const UserAddPopup: React.FC<UserAddPopupProps> = (props) => {
<label htmlFor="Notification">
<input
type="checkbox"
checked={notification}
checked={addUser.notification}
className={styles.formCheck}
onChange={(e) => {
dispatch(

View File

@ -942,7 +942,7 @@ h3 .brCrumb .tlIcon {
}
.pageTitle {
display: inline-block;
padding-right: 1rem;
padding-right: 1.5rem;
font-size: 1.4rem;
line-height: 1.4rem;
letter-spacing: 0.07rem;
@ -950,6 +950,22 @@ h3 .brCrumb .tlIcon {
}
.pageTx {
display: inline-block;
padding-left: 2rem;
font-size: 1.4rem;
line-height: 1.4rem;
letter-spacing: 0.07rem;
font-weight: 500;
position: relative;
}
.pageTx::before {
content: "";
border-top: 6px transparent solid;
border-bottom: 6px transparent solid;
border-left: 8px #ffffff solid;
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
}
.pagenation {
@ -988,6 +1004,15 @@ h3 .brCrumb .tlIcon {
.pagenationTotal {
color: #999999;
}
.pagenation.widthMid {
width: 1000px;
}
.pagenation.widthSml {
width: 750px;
}
.pagenation.widthMin {
width: 600px;
}
_:-ms-lang(x)::-ms-backdrop,
.pagenationNav a {
@ -1262,7 +1287,8 @@ _:-ms-lang(x)::-ms-backdrop,
.user > div,
.license > div,
.dictation > div,
.partners > div {
.partners > div,
.workflow > div {
padding: 0 2rem;
position: relative;
}
@ -1270,7 +1296,8 @@ _:-ms-lang(x)::-ms-backdrop,
.user > div .icLoading,
.license > div .icLoading,
.dictation > div .icLoading,
.partners > div .icLoading {
.partners > div .icLoading,
.workflow > div .icLoading {
top: 5.5rem;
left: calc(50% - 25px);
}
@ -1278,7 +1305,8 @@ _:-ms-lang(x)::-ms-backdrop,
.user .table tr.tableHeader th.clm0,
.license .table tr.tableHeader th.clm0,
.dictation .table tr.tableHeader th.clm0,
.partners .table tr.tableHeader th.clm0 {
.partners .table tr.tableHeader th.clm0,
.workflow .table tr.tableHeader th.clm0 {
width: 0px;
padding: 0 0;
}
@ -1286,21 +1314,24 @@ _:-ms-lang(x)::-ms-backdrop,
.user .table tr:not(.tableHeader),
.license .table tr:not(.tableHeader),
.dictation .table tr:not(.tableHeader),
.partners .table tr:not(.tableHeader) {
.partners .table tr:not(.tableHeader),
.workflow .table tr:not(.tableHeader) {
position: relative;
}
.account .table tr:not(.tableHeader):hover .menuInTable,
.user .table tr:not(.tableHeader):hover .menuInTable,
.license .table tr:not(.tableHeader):hover .menuInTable,
.dictation .table tr:not(.tableHeader):hover .menuInTable,
.partners .table tr:not(.tableHeader):hover .menuInTable {
.partners .table tr:not(.tableHeader):hover .menuInTable,
.workflow .table tr:not(.tableHeader):hover .menuInTable {
opacity: 1;
}
.account .table tr:not(.tableHeader).isSelected,
.user .table tr:not(.tableHeader).isSelected,
.license .table tr:not(.tableHeader).isSelected,
.dictation .table tr:not(.tableHeader).isSelected,
.partners .table tr:not(.tableHeader).isSelected {
.partners .table tr:not(.tableHeader).isSelected,
.workflow .table tr:not(.tableHeader).isSelected {
background: #0084b2;
color: #ffffff;
}
@ -1308,21 +1339,24 @@ _:-ms-lang(x)::-ms-backdrop,
.user .table tr:not(.tableHeader).isSelected:hover,
.license .table tr:not(.tableHeader).isSelected:hover,
.dictation .table tr:not(.tableHeader).isSelected:hover,
.partners .table tr:not(.tableHeader).isSelected:hover {
.partners .table tr:not(.tableHeader).isSelected:hover,
.workflow .table tr:not(.tableHeader).isSelected:hover {
color: #ffffff;
}
.account .table tr:not(.tableHeader).isSelected .menuInTable,
.user .table tr:not(.tableHeader).isSelected .menuInTable,
.license .table tr:not(.tableHeader).isSelected .menuInTable,
.dictation .table tr:not(.tableHeader).isSelected .menuInTable,
.partners .table tr:not(.tableHeader).isSelected .menuInTable {
.partners .table tr:not(.tableHeader).isSelected .menuInTable,
.workflow .table tr:not(.tableHeader).isSelected .menuInTable {
display: block;
}
.account .table td,
.user .table td,
.license .table td,
.dictation .table td,
.partners .table td {
.partners .table td,
.workflow .table td {
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
@ -1333,7 +1367,8 @@ _:-ms-lang(x)::-ms-backdrop,
.user .table td.clm0,
.license .table td.clm0,
.dictation .table td.clm0,
.partners .table td.clm0 {
.partners .table td.clm0,
.workflow .table td.clm0 {
width: 0px;
padding: 0 0;
overflow: visible;
@ -1345,14 +1380,24 @@ _:-ms-lang(x)::-ms-backdrop,
.user .table.user,
.license .table.user,
.dictation .table.user,
.partners .table.user {
.partners .table.user,
.workflow .table.user {
margin-bottom: 5rem;
}
.account .table.user th::after,
.user .table.user th::after,
.license .table.user th::after,
.dictation .table.user th::after,
.partners .table.user th::after,
.workflow .table.user th::after {
display: none;
}
.account .table.user tr:not(.tableHeader) td,
.user .table.user tr:not(.tableHeader) td,
.license .table.user tr:not(.tableHeader) td,
.dictation .table.user tr:not(.tableHeader) td,
.partners .table.user tr:not(.tableHeader) td {
.partners .table.user tr:not(.tableHeader) td,
.workflow .table.user tr:not(.tableHeader) td {
padding-bottom: 2rem;
vertical-align: top;
}
@ -1360,21 +1405,24 @@ _:-ms-lang(x)::-ms-backdrop,
.user.account .listVertical,
.license.account .listVertical,
.dictation.account .listVertical,
.partners.account .listVertical {
.partners.account .listVertical,
.workflow.account .listVertical {
margin-bottom: 3rem;
}
.account.account .listVertical dd .formInput,
.user.account .listVertical dd .formInput,
.license.account .listVertical dd .formInput,
.dictation.account .listVertical dd .formInput,
.partners.account .listVertical dd .formInput {
.partners.account .listVertical dd .formInput,
.workflow.account .listVertical dd .formInput {
max-width: 100%;
}
.account.account .listVertical dd .formCheckToggle,
.user.account .listVertical dd .formCheckToggle,
.license.account .listVertical dd .formCheckToggle,
.dictation.account .listVertical dd .formCheckToggle,
.partners.account .listVertical dd .formCheckToggle {
.partners.account .listVertical dd .formCheckToggle,
.workflow.account .listVertical dd .formCheckToggle {
position: relative;
cursor: pointer;
}
@ -1382,7 +1430,8 @@ _:-ms-lang(x)::-ms-backdrop,
.user.account .listVertical dd .formCheckToggle .toggleBase,
.license.account .listVertical dd .formCheckToggle .toggleBase,
.dictation.account .listVertical dd .formCheckToggle .toggleBase,
.partners.account .listVertical dd .formCheckToggle .toggleBase {
.partners.account .listVertical dd .formCheckToggle .toggleBase,
.workflow.account .listVertical dd .formCheckToggle .toggleBase {
display: inline-block;
width: 2.8rem;
height: 1.6rem;
@ -1400,7 +1449,8 @@ _:-ms-lang(x)::-ms-backdrop,
.user.account .listVertical dd .formCheckToggle .toggleBase::after,
.license.account .listVertical dd .formCheckToggle .toggleBase::after,
.dictation.account .listVertical dd .formCheckToggle .toggleBase::after,
.partners.account .listVertical dd .formCheckToggle .toggleBase::after {
.partners.account .listVertical dd .formCheckToggle .toggleBase::after,
.workflow.account .listVertical dd .formCheckToggle .toggleBase::after {
content: "";
width: 1.3rem;
height: 1.3rem;
@ -1418,7 +1468,8 @@ _:-ms-lang(x)::-ms-backdrop,
.user.account .listVertical dd .formCheckToggle input,
.license.account .listVertical dd .formCheckToggle input,
.dictation.account .listVertical dd .formCheckToggle input,
.partners.account .listVertical dd .formCheckToggle input {
.partners.account .listVertical dd .formCheckToggle input,
.workflow.account .listVertical dd .formCheckToggle input {
position: absolute;
width: 0;
heigh: 0;
@ -1432,7 +1483,8 @@ _:-ms-lang(x)::-ms-backdrop,
.formCheckToggle
input:checked
~ .toggleBase,
.partners.account
.partners.account .listVertical dd .formCheckToggle input:checked ~ .toggleBase,
.workflow.account
.listVertical
dd
.formCheckToggle
@ -1465,6 +1517,12 @@ _:-ms-lang(x)::-ms-backdrop,
input:checked
~ .toggleBase::after,
.partners.account
.listVertical
dd
.formCheckToggle
input:checked
~ .toggleBase::after,
.workflow.account
.listVertical
dd
.formCheckToggle
@ -1482,6 +1540,9 @@ _:-ms-lang(x)::-ms-backdrop,
display: inline-block;
margin-right: 0.5rem;
}
.menuAction li:last-child {
margin-right: 0;
}
.menuAction.inTable {
margin-bottom: 0;
}
@ -1521,9 +1582,6 @@ _:-ms-lang(x)::-ms-backdrop,
.menuAction.inTable .colorLink.isActive:hover {
background: rgba(0, 94, 184, 0.7);
}
.menuAction.alignRight {
margin-top: -1rem;
}
.menuLink {
display: block;
padding: 0.3rem 0.5rem 0.3rem 0.3rem;
@ -1697,6 +1755,9 @@ tr.isSelected .menuInTable li a {
text-align: right;
}
.dictation .menuAction {
margin-top: -1rem;
}
.dictation .displayOptions {
display: none;
margin-bottom: 0.6rem;
@ -2077,6 +2138,9 @@ tr.isSelected .menuInTable li a {
background: #282828;
z-index: 1;
}
.partners .table.partner tr.tableHeader th::after {
display: none;
}
.partners .table.partner td {
padding-bottom: 2rem;
vertical-align: top;
@ -2098,6 +2162,113 @@ tr.isSelected .menuInTable li a {
display: none;
}
.workflow .table {
margin-bottom: 0;
}
.workflow .table.workflow {
min-width: 100%;
}
.workflow .table.workflow tr {
position: relative;
}
.workflow .table.workflow th::after {
display: none;
}
.workflow .table.workflow td {
padding-bottom: 2rem;
vertical-align: top;
}
.workflow .table.workflow td.txWsline {
white-space: pre;
}
.workflow .table.group {
width: 600px;
}
.workflow .table.group td:last-child {
text-align: right;
}
.formList dd.formChange {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
width: 92%;
padding: 0 4%;
}
.formList dd.formChange ul.chooseMember,
.formList dd.formChange ul.holdMember {
width: calc(40% - 2px);
border: 1px #999999 solid;
}
.formChange ul.chooseMember,
.formChange ul.holdMember {
height: 250px;
overflow-y: scroll;
padding: 0.5rem;
background: #ffffff;
}
.formChange ul.chooseMember li.changeTitle,
.formChange ul.holdMember li.changeTitle {
font-weight: 600;
}
.formChange ul.chooseMember li .formCheck,
.formChange ul.holdMember li .formCheck {
display: none;
}
.formChange ul.chooseMember li input + label,
.formChange ul.holdMember li input + label {
display: block;
padding: 0.2rem 0 0.2rem 1.5rem;
margin-right: 0;
background: url(../assets/images/circle.svg) no-repeat left center;
background-size: 1.3rem;
}
.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-size: 1.3rem;
}
.formChange ul.chooseMember li input:checked + label,
.formChange ul.holdMember li input:checked + label {
padding: 0.2rem 1rem 0.2rem 0;
background: url(../assets/images/check_circle_fill.svg) no-repeat right center;
background-size: 1.3rem;
}
.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-size: 1.3rem;
}
.formChange > p {
width: 6%;
height: 20px;
background: #e6e6e6;
position: relative;
}
.formChange > p::before {
content: "";
border-top: 20px transparent solid;
border-right: 20px #e6e6e6 solid;
border-bottom: 20px transparent solid;
position: absolute;
top: 50%;
left: -15px;
transform: translateY(-50%);
}
.formChange > p::after {
content: "";
border-top: 20px transparent solid;
border-bottom: 20px transparent solid;
border-left: 20px #e6e6e6 solid;
position: absolute;
top: 50%;
right: -15px;
transform: translateY(-50%);
}
.alignCenter {
text-align: center;
}
@ -2108,6 +2279,16 @@ tr.isSelected .menuInTable li a {
text-align: right;
}
.floatNone {
float: none;
}
.floatLeft {
float: left;
}
.floatRight {
float: right;
}
.linkTx {
color: #0084b2;
text-decoration: none;

View File

@ -67,6 +67,9 @@ declare const classNames: {
readonly pagenation: "pagenation";
readonly pagenationNav: "pagenationNav";
readonly pagenationTotal: "pagenationTotal";
readonly widthMid: "widthMid";
readonly widthSml: "widthSml";
readonly widthMin: "widthMin";
readonly snackbar: "snackbar";
readonly isAlert: "isAlert";
readonly snackbarMessage: "snackbarMessage";
@ -88,6 +91,7 @@ declare const classNames: {
readonly license: "license";
readonly dictation: "dictation";
readonly partners: "partners";
readonly workflow: "workflow";
readonly clm0: "clm0";
readonly menuInTable: "menuInTable";
readonly isSelected: "isSelected";
@ -97,7 +101,6 @@ declare const classNames: {
readonly inTable: "inTable";
readonly menuLink: "menuLink";
readonly colorLink: "colorLink";
readonly alignRight: "alignRight";
readonly menuIcon: "menuIcon";
readonly isDisable: "isDisable";
readonly icCheckCircle: "icCheckCircle";
@ -174,8 +177,13 @@ declare const classNames: {
readonly holdMember: "holdMember";
readonly changeTitle: "changeTitle";
readonly role4: "role4";
readonly group: "group";
readonly alignCenter: "alignCenter";
readonly alignLeft: "alignLeft";
readonly alignRight: "alignRight";
readonly floatNone: "floatNone";
readonly floatLeft: "floatLeft";
readonly floatRight: "floatRight";
readonly linkTx: "linkTx";
readonly linkBottom: "linkBottom";
readonly borderTop: "borderTop";

View File

@ -110,7 +110,8 @@
"message": {
"addUserSuccess": "(de)メールアドレス宛に認証用メールを送信しました。",
"authorIdConflictError": "(de)このAuthor IDは既に登録されています。他のAuthor IDで登録してください。",
"authorIdIncorrectError": "(de)Author IDの形式が不正です。Author IDは半角英数字(大文字)と\"_\"のみ入力可能です。"
"authorIdIncorrectError": "(de)Author IDの形式が不正です。Author IDは半角英数字(大文字)と\"_\"のみ入力可能です。",
"encryptionPasswordCorrectError": "(de)EncryptionPasswordがルールを満たしていません。"
},
"label": {
"title": "(de)User",
@ -143,7 +144,9 @@
"editUser": "(de)Edit User",
"licenseDeallocation": "(de)License Deallocation",
"deleteUser": "(de)Delete User",
"none": "(de)None"
"none": "(de)None",
"encryptionPassword": "(de)Password:",
"encryptionPasswordTerm": "(de)Please set your password using 4 to 16 ASCII characters.\nYour password may include letters, numbers, and symbols."
}
},
"LicenseSummaryPage": {

View File

@ -110,7 +110,8 @@
"message": {
"addUserSuccess": "メールアドレス宛に認証用メールを送信しました。",
"authorIdConflictError": "このAuthor IDは既に登録されています。他のAuthor IDで登録してください。",
"authorIdIncorrectError": "Author IDの形式が不正です。Author IDは半角英数字(大文字)と\"_\"のみ入力可能です。"
"authorIdIncorrectError": "Author IDの形式が不正です。Author IDは半角英数字(大文字)と\"_\"のみ入力可能です。",
"encryptionPasswordCorrectError": "EncryptionPasswordがルールを満たしていません。"
},
"label": {
"title": "User",
@ -143,7 +144,9 @@
"editUser": "Edit User",
"licenseDeallocation": "License Deallocation",
"deleteUser": "Delete User",
"none": "None"
"none": "None",
"encryptionPassword": "Password:",
"encryptionPasswordTerm": "Please set your password using 4 to 16 ASCII characters.\nYour password may include letters, numbers, and symbols."
}
},
"LicenseSummaryPage": {

View File

@ -110,7 +110,8 @@
"message": {
"addUserSuccess": "(es)メールアドレス宛に認証用メールを送信しました。",
"authorIdConflictError": "(es)このAuthor IDは既に登録されています。他のAuthor IDで登録してください。",
"authorIdIncorrectError": "(es)Author IDの形式が不正です。Author IDは半角英数字(大文字)と\"_\"のみ入力可能です。"
"authorIdIncorrectError": "(es)Author IDの形式が不正です。Author IDは半角英数字(大文字)と\"_\"のみ入力可能です。",
"encryptionPasswordCorrectError": "(es)EncryptionPasswordがルールを満たしていません。"
},
"label": {
"title": "(es)User",
@ -143,7 +144,9 @@
"editUser": "(es)Edit User",
"licenseDeallocation": "(es)License Deallocation",
"deleteUser": "(es)Delete User",
"none": "(es)None"
"none": "(es)None",
"encryptionPassword": "(es)Password:",
"encryptionPasswordTerm": "(es)Please set your password using 4 to 16 ASCII characters.\nYour password may include letters, numbers, and symbols."
}
},
"LicenseSummaryPage": {

View File

@ -110,7 +110,8 @@
"message": {
"addUserSuccess": "(fr)メールアドレス宛に認証用メールを送信しました。",
"authorIdConflictError": "(fr)このAuthor IDは既に登録されています。他のAuthor IDで登録してください。",
"authorIdIncorrectError": "(fr)Author IDの形式が不正です。Author IDは半角英数字(大文字)と\"_\"のみ入力可能です。"
"authorIdIncorrectError": "(fr)Author IDの形式が不正です。Author IDは半角英数字(大文字)と\"_\"のみ入力可能です。",
"encryptionPasswordCorrectError": "(fr)EncryptionPasswordがルールを満たしていません。"
},
"label": {
"title": "(fr)User",
@ -143,7 +144,9 @@
"editUser": "(fr)Edit User",
"licenseDeallocation": "(fr)License Deallocation",
"deleteUser": "(fr)Delete User",
"none": "(fr)None"
"none": "(fr)None",
"encryptionPassword": "(fr)Password:",
"encryptionPasswordTerm": "(fr)Please set your password using 4 to 16 ASCII characters.\nYour password may include letters, numbers, and symbols."
}
},
"LicenseSummaryPage": {

View File

@ -2179,11 +2179,13 @@
"name": { "type": "string" },
"role": { "type": "string", "description": "none/author/typist" },
"authorId": { "type": "string" },
"typistGroupId": { "type": "number" },
"email": { "type": "string" },
"autoRenew": { "type": "boolean" },
"licenseAlert": { "type": "boolean" },
"notification": { "type": "boolean" }
"notification": { "type": "boolean" },
"encryption": { "type": "boolean" },
"encryptionPassword": { "type": "string" },
"prompt": { "type": "boolean" }
},
"required": [
"name",

View File

@ -6,3 +6,5 @@ import { ADMIN_ROLES, USER_ROLES } from '../../../constants';
export type Roles =
| (typeof ADMIN_ROLES)[keyof typeof ADMIN_ROLES]
| (typeof USER_ROLES)[keyof typeof USER_ROLES];
export type UserRoles = (typeof USER_ROLES)[keyof typeof USER_ROLES];

View File

@ -1,4 +1,9 @@
import { registerDecorator, ValidationOptions } from 'class-validator';
import {
registerDecorator,
ValidationArguments,
ValidationOptions,
} from 'class-validator';
import { SignupRequest } from '../../features/users/types/types';
export const IsPasswordvalid = (validationOptions?: ValidationOptions) => {
return (object: any, propertyName: string) => {
@ -30,3 +35,29 @@ export const IsPasswordvalid = (validationOptions?: ValidationOptions) => {
});
};
};
export const IsEncryptionPasswordPresent = (
validationOptions?: ValidationOptions,
) => {
return (object: SignupRequest, propertyName: string) => {
registerDecorator({
name: 'IsEncryptionPasswordValid',
target: object.constructor,
propertyName: propertyName,
constraints: [],
options: validationOptions,
validator: {
validate: (value: string | undefined, args: ValidationArguments) => {
const { encryption } = args.object as SignupRequest;
if (encryption === true && !value) {
return false;
}
return true;
},
defaultMessage: () => {
return 'Encryption password is required when encryption is enabled';
},
},
});
};
};

View File

@ -1,13 +1,13 @@
import {
registerDecorator,
ValidationOptions,
ValidationArguments,
ValidationOptions,
} from 'class-validator';
import { USER_ROLES } from '../../constants';
import {
SignupRequest,
PostUpdateUserRequest,
SignupRequest,
} from '../../features/users/types/types';
import { USER_ROLES } from '../../constants';
export const IsRoleAuthorDataValid = <
T extends SignupRequest | PostUpdateUserRequest,
@ -22,7 +22,7 @@ export const IsRoleAuthorDataValid = <
constraints: [],
options: validationOptions,
validator: {
validate: (value: boolean | undefined, args: ValidationArguments) => {
validate: (value: any, args: ValidationArguments) => {
const request = args.object as T;
const { role } = request;
if (role === USER_ROLES.AUTHOR && value === undefined) {

View File

@ -5,8 +5,11 @@ import {
USER_LICENSE_STATUS,
} from '../../../constants';
import { USER_ROLES } from '../../../constants';
import {
IsEncryptionPasswordPresent,
IsPasswordvalid,
} from '../../../common/validators/encryptionPassword.validator';
import { IsRoleAuthorDataValid } from '../../../common/validators/roleAuthor.validator';
import { IsPasswordvalid } from '../../../common/validators/encryptionPassword.validator';
export class ConfirmRequest {
@ApiProperty()
@ -81,11 +84,9 @@ export class SignupRequest {
role: string;
@ApiProperty({ required: false })
@IsRoleAuthorDataValid()
authorId?: string | undefined;
@ApiProperty({ required: false })
typistGroupId?: number | undefined;
@ApiProperty()
email: string;
@ -97,6 +98,19 @@ export class SignupRequest {
@ApiProperty()
notification: boolean;
@ApiProperty({ required: false })
@IsRoleAuthorDataValid()
encryption?: boolean | undefined;
@ApiProperty({ required: false })
@IsPasswordvalid()
@IsEncryptionPasswordPresent()
encryptionPassword?: string | undefined;
@ApiProperty({ required: false })
@IsRoleAuthorDataValid()
prompt?: boolean | undefined;
}
export class SignupResponse {}

View File

@ -44,6 +44,7 @@ import {
import { ADMIN_ROLES } from '../../constants';
import { RoleGuard } from '../../common/guards/role/roleguards';
import { makeContext } from '../../common/log';
import { UserRoles } from '../../common/types/role';
@ApiTags('users')
@Controller('users')
@ -162,7 +163,9 @@ export class UsersController {
licenseAlert,
notification,
authorId,
typistGroupId,
encryption,
encryptionPassword,
prompt,
} = body;
const accessToken = retrieveAuthorizationToken(req);
@ -172,13 +175,15 @@ export class UsersController {
await this.usersService.createUser(
payload,
name,
role,
role as UserRoles,
email,
autoRenew,
licenseAlert,
notification,
authorId,
typistGroupId,
encryption,
encryptionPassword,
prompt,
);
return {};
}

View File

@ -285,7 +285,7 @@ describe('UsersService.confirmUser', () => {
sortCriteriaRepositoryMockValue,
);
const name = 'test_user1';
const role = 'None';
const role = USER_ROLES.NONE;
const email = 'test1@example.co.jp';
const autoRenew = true;
const licenseAlert = true;
@ -306,7 +306,7 @@ describe('UsersService.confirmUser', () => {
});
describe('UsersService.createUser', () => {
it('管理者権限のあるアクセストークンを使用して、新規ユーザが追加される(role:Author)', async () => {
it('管理者権限のあるアクセストークンを使用して、新規ユーザが追加される(role:Author; 暗号化あり)', async () => {
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const adb2cParam = makeDefaultAdB2cMockValue();
const sendgridMockValue = makeDefaultSendGridlValue();
@ -321,12 +321,15 @@ describe('UsersService.createUser', () => {
sortCriteriaRepositoryMockValue,
);
const name = 'test_user2';
const role = 'Author';
const role = USER_ROLES.AUTHOR;
const email = 'test2@example.co.jp';
const autoRenew = true;
const licenseAlert = true;
const notification = true;
const authorId = 'testID';
const encryption = true;
const prompt = true;
const encryptionPassword = 'testPassword';
const token: AccessToken = { userId: '0001', role: '', tier: 5 };
expect(
await service.createUser(
@ -338,6 +341,51 @@ describe('UsersService.createUser', () => {
licenseAlert,
notification,
authorId,
encryption,
encryptionPassword,
prompt,
),
).toEqual(undefined);
});
it('管理者権限のあるアクセストークンを使用して、新規ユーザが追加される(role:Author; 暗号化無し)', async () => {
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
const adb2cParam = makeDefaultAdB2cMockValue();
const sendgridMockValue = makeDefaultSendGridlValue();
const configMockValue = makeDefaultConfigValue();
const sortCriteriaRepositoryMockValue =
makeDefaultSortCriteriaRepositoryMockValue();
const service = await makeUsersServiceMock(
usersRepositoryMockValue,
adb2cParam,
sendgridMockValue,
configMockValue,
sortCriteriaRepositoryMockValue,
);
const name = 'test_user2';
const role = USER_ROLES.AUTHOR;
const email = 'test2@example.co.jp';
const autoRenew = true;
const licenseAlert = true;
const notification = true;
const authorId = 'testID';
const encryption = false;
const prompt = true;
const encryptionPassword = undefined;
const token: AccessToken = { userId: '0001', role: '', tier: 5 };
expect(
await service.createUser(
token,
name,
role,
email,
autoRenew,
licenseAlert,
notification,
authorId,
encryption,
encryptionPassword,
prompt,
),
).toEqual(undefined);
});
@ -357,12 +405,11 @@ describe('UsersService.createUser', () => {
sortCriteriaRepositoryMockValue,
);
const name = 'test_user3';
const role = 'Transcriptioninst';
const role = USER_ROLES.TYPIST;
const email = 'test3@example.co.jp';
const autoRenew = true;
const licenseAlert = true;
const notification = true;
const typistGroupId = 111;
const token: AccessToken = { userId: '0001', role: '', tier: 5 };
expect(
await service.createUser(
@ -374,7 +421,6 @@ describe('UsersService.createUser', () => {
licenseAlert,
notification,
undefined,
typistGroupId,
),
).toEqual(undefined);
});
@ -394,7 +440,7 @@ describe('UsersService.createUser', () => {
sortCriteriaRepositoryMockValue,
);
const name = 'test_user5';
const role = 'Transcriptioninst';
const role = USER_ROLES.TYPIST;
const email = 'test5@example.co.jp';
const autoRenew = true;
const licenseAlert = true;
@ -433,7 +479,7 @@ describe('UsersService.createUser', () => {
sortCriteriaRepositoryMockValue,
);
const name = 'test_user6';
const role = 'Transcriptioninst';
const role = USER_ROLES.TYPIST;
const email = 'test6@example.co.jp';
const autoRenew = true;
const licenseAlert = true;
@ -472,7 +518,7 @@ describe('UsersService.createUser', () => {
sortCriteriaRepositoryMockValue,
);
const name = 'test_user7';
const role = 'Transcriptioninst';
const role = USER_ROLES.TYPIST;
const email = 'test7@example.co.jp';
const autoRenew = true;
const licenseAlert = true;
@ -510,7 +556,7 @@ describe('UsersService.createUser', () => {
sortCriteriaRepositoryMockValue,
);
const name = 'test_user8';
const role = 'Author';
const role = USER_ROLES.AUTHOR;
const email = 'test8@example.co.jp';
const autoRenew = true;
const licenseAlert = true;
@ -550,7 +596,7 @@ describe('UsersService.createUser', () => {
sortCriteriaRepositoryMockValue,
);
const name = 'test_user9';
const role = 'Author';
const role = USER_ROLES.AUTHOR;
const email = 'test9@example.co.jp';
const autoRenew = true;
const licenseAlert = true;

View File

@ -18,7 +18,10 @@ import {
} from '../../gateways/adb2c/adb2c.service';
import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
import { SortCriteriaRepositoryService } from '../../repositories/sort_criteria/sort_criteria.repository.service';
import { User as EntityUser } from '../../repositories/users/entity/user.entity';
import {
User as EntityUser,
newUser,
} from '../../repositories/users/entity/user.entity';
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
import { GetRelationsResponse, User } from './types/types';
import {
@ -31,9 +34,11 @@ import {
import {
LICENSE_EXPIRATION_THRESHOLD_DAYS,
USER_LICENSE_STATUS,
USER_ROLES,
} from '../../constants';
import { DateWithZeroTime } from '../licenses/types/types';
import { Context } from '../../common/log';
import { UserRoles } from '../../common/types/role';
@Injectable()
export class UsersService {
@ -71,7 +76,7 @@ export class UsersService {
const userId = decodedToken.userId;
await this.usersRepository.updateUserVerified(userId);
} catch (e) {
console.log(e);
this.logger.error(e);
if (e instanceof Error) {
switch (e.constructor) {
case EmailAlreadyVerifiedError:
@ -87,7 +92,6 @@ export class UsersService {
}
}
this.logger.error(`error=${e}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
@ -96,20 +100,32 @@ export class UsersService {
}
/**
*
* @param token
* @returns account
* Creates user
* @param accessToken
* @param name
* @param role
* @param email
* @param autoRenew
* @param licenseAlert
* @param notification
* @param [authorId]
* @param [encryption]
* @param [encryptionPassword]
* @param [prompt]
* @returns void
*/
async createUser(
accessToken: AccessToken,
name: string,
role: string,
role: UserRoles,
email: string,
autoRenew: boolean,
licenseAlert: boolean,
notification: boolean,
authorId?: string | undefined,
groupID?: number | undefined,
encryption?: boolean | undefined,
encryptionPassword?: string | undefined,
prompt?: boolean | undefined,
): Promise<void> {
this.logger.log(`[IN] ${this.createUser.name}`);
@ -120,6 +136,7 @@ export class UsersService {
accessToken.userId,
);
} catch (e) {
this.logger.error(`error=${e}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
@ -137,6 +154,7 @@ export class UsersService {
authorId,
);
} catch (e) {
this.logger.error(`error=${e}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
@ -163,7 +181,8 @@ export class UsersService {
name,
);
} catch (e) {
console.log('create externalUser failed');
this.logger.error(`error=${e}`);
this.logger.error('create externalUser failed');
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
@ -180,22 +199,26 @@ export class UsersService {
//Azure AD B2Cに登録したユーザー情報のID(sub)と受け取った情報を使ってDBにユーザーを登録する
let newUser: EntityUser;
// TODO [Task2246] 本来はNULLだが、テーブル定義に誤ってNOTNULLが付いているため、一時的に適当な値を設定
const accepted_terms_version = 'xxx';
try {
// ユーザ作成
newUser = await this.usersRepository.createNormalUser(
//roleに応じてユーザー情報を作成する
const newUserInfo = this.createNewUserInfo(
role,
accountId,
externalUser.sub,
role,
autoRenew,
licenseAlert,
notification,
authorId,
accepted_terms_version,
encryption,
encryptionPassword,
prompt,
);
// ユーザ作成
newUser = await this.usersRepository.createNormalUser(newUserInfo);
} catch (e) {
console.log('create user failed');
this.logger.error(`error=${e}`);
this.logger.error('create user failed');
switch (e.code) {
case 'ER_DUP_ENTRY':
//AuthorID重複エラー
@ -227,8 +250,9 @@ export class UsersService {
//SendGridAPIを呼び出してメールを送信する
await this.sendgridService.sendMail(email, from, subject, text, html);
} catch (e) {
console.log('create user failed');
console.log(`[NOT IMPLEMENT] [RECOVER] delete user: ${newUser.id}`);
this.logger.error(`error=${e}`);
this.logger.error('create user failed');
this.logger.error(`[NOT IMPLEMENT] [RECOVER] delete user: ${newUser.id}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
@ -237,6 +261,54 @@ export class UsersService {
this.logger.log(`[OUT] ${this.createUser.name}`);
return;
}
// roleを受け取って、roleに応じたnewUserを作成して返却する
private createNewUserInfo(
role: UserRoles,
accountId: number,
externalId: string,
autoRenew: boolean,
licenseAlert: boolean,
notification: boolean,
authorId?: string | undefined,
encryption?: boolean | undefined,
encryptionPassword?: string | undefined,
prompt?: boolean | undefined,
): newUser {
switch (role) {
case USER_ROLES.NONE:
case USER_ROLES.TYPIST:
return {
account_id: accountId,
external_id: externalId,
auto_renew: autoRenew,
license_alert: licenseAlert,
notification,
role,
};
case USER_ROLES.AUTHOR:
return {
account_id: accountId,
external_id: externalId,
auto_renew: autoRenew,
license_alert: licenseAlert,
notification,
role,
author_id: authorId,
encryption,
encryption_password: encryptionPassword,
prompt,
};
default:
//不正なroleが指定された場合はログを出力してエラーを返す
this.logger.error(`[NOT IMPLEMENT] [RECOVER] role: ${role}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
/**
* confirm User And Init Password
* @param token

View File

@ -30,8 +30,8 @@ export class User {
@Column({ nullable: true })
author_id?: string;
@Column()
accepted_terms_version: string;
@Column({ nullable: true })
accepted_terms_version?: string;
@Column()
email_verified: boolean;
@ -45,14 +45,14 @@ export class User {
@Column()
notification: boolean;
@Column()
encryption: boolean;
@Column({ nullable: true })
encryption?: boolean;
@Column({ nullable: true })
encryption_password?: string;
@Column()
prompt: boolean;
@Column({ nullable: true })
prompt?: boolean;
@Column({ nullable: true })
deleted_at?: Date;
@ -79,3 +79,17 @@ export class User {
@OneToMany(() => UserGroupMember, (userGroupMember) => userGroupMember.user)
userGroupMembers?: UserGroupMember[];
}
export type newUser = Omit<
User,
| 'id'
| 'deleted_at'
| 'created_at'
| 'updated_at'
| 'updated_by'
| 'created_by'
| 'account'
| 'license'
| 'userGroupMembers'
| 'email_verified'
>;

View File

@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { User, newUser } from './entity/user.entity';
import { DataSource, IsNull, Not, UpdateResult } from 'typeorm';
import { User } from './entity/user.entity';
import { SortCriteria } from '../sort_criteria/entity/sort_criteria.entity';
import {
getDirection,
@ -45,41 +45,42 @@ export class UsersRepositoryService {
}
/**
*
* @param account_id
* @param external_id
* @param role
* @param autoRenew
* @param licenseAlert
* @param notification
* @param authorId
*
* @param user
* @returns User
*/
async createNormalUser(
accountId: number,
externalUserId: string,
role: string,
auto_renew: boolean,
license_alert: boolean,
notification: boolean,
author_id: string,
accepted_terms_version: string,
): Promise<User> {
const user = new User();
async createNormalUser(user: newUser): Promise<User> {
const {
account_id: accountId,
external_id: externalUserId,
role,
auto_renew,
license_alert,
notification,
author_id,
accepted_terms_version,
encryption,
encryption_password: encryptionPassword,
prompt,
} = user;
const userEntity = new User();
user.role = role;
user.account_id = accountId;
user.external_id = externalUserId;
user.auto_renew = auto_renew;
user.license_alert = license_alert;
user.notification = notification;
user.author_id = author_id;
user.accepted_terms_version = accepted_terms_version;
userEntity.role = role;
userEntity.account_id = accountId;
userEntity.external_id = externalUserId;
userEntity.auto_renew = auto_renew;
userEntity.license_alert = license_alert;
userEntity.notification = notification;
userEntity.author_id = author_id;
userEntity.accepted_terms_version = accepted_terms_version;
userEntity.encryption = encryption;
userEntity.encryption_password = encryptionPassword;
userEntity.prompt = prompt;
const createdEntity = await this.dataSource.transaction(
async (entityManager) => {
const repo = entityManager.getRepository(User);
const newUser = repo.create(user);
const newUser = repo.create(userEntity);
const persisted = await repo.save(newUser);
// ユーザーのタスクソート条件を作成