From a2a0778dfb49846edb8c2893be2d9bc01a39fbbb Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Wed, 19 Apr 2023 00:52:47 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=2081:=20=E7=94=BB=E9=9D=A2=E5=AE=9F?= =?UTF-8?q?=E8=A3=85=EF=BC=88=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC=E8=AA=8D?= =?UTF-8?q?=E8=A8=BC=E7=94=BB=E9=9D=A2/=E8=AA=8D=E8=A8=BC=E5=AE=8C?= =?UTF-8?q?=E4=BA=86=E7=94=BB=E9=9D=A2=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task1597: 画面実装(ユーザー認証画面/認証完了画面)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/1597) - アカウントへのユーザ追加でメール認証URLから認証を実行した際の画面を実装しました。 - 完了後の画面はアカウント登録のものをそのまま流用しています。 ## レビューポイント - 認証APIからのレスポンスはアカウント登録と同様のものを想定して、完了画面をそのまま流用していますが問題ないでしょうか。 - 画面のパスを`/mail-confirm/user`としましたが問題ないでしょうか? ## UIの変更 - アカウント登録の認証完了画面と同様 ## 動作確認状況 - ローカルで確認 - 認証APIでアカウント登録と同様のレスポンスを想定 --- dictation_client/src/AppRouter.tsx | 2 + dictation_client/src/api/api.ts | 537 ++++++++++++++++++ .../src/features/verify/operations.ts | 33 ++ .../src/features/verify/verifySlice.ts | 20 +- .../src/pages/UserVerifyPage/index.tsx | 55 ++ 5 files changed, 646 insertions(+), 1 deletion(-) create mode 100644 dictation_client/src/pages/UserVerifyPage/index.tsx diff --git a/dictation_client/src/AppRouter.tsx b/dictation_client/src/AppRouter.tsx index f02d082..f95adfc 100644 --- a/dictation_client/src/AppRouter.tsx +++ b/dictation_client/src/AppRouter.tsx @@ -7,6 +7,7 @@ import { NotFoundPage } from "pages/ErrorPage/notFound"; import { RouteAuthGuard } from "components/auth/routeAuthGuard"; import SignupPage from "pages/SignupPage"; import VerifyPage from "pages/VerifyPage"; +import UserVerifyPage from "pages/UserVerifyPage"; import VerifySuccessPage from "pages/VerifySuccessPage"; import VerifyFailedPage from "pages/VerifyFailedPage"; import VerifyAlreadyExistPage from "pages/VerifyAlreadyExistPage"; @@ -23,6 +24,7 @@ const AppRouter: React.FC = () => ( /> } /> } /> + } /> } /> } /> } + * @memberof GetUsersResponse + */ + 'users': Array; +} /** * * @export @@ -142,6 +181,61 @@ export interface RegisterRequest { */ 'handler': string; } +/** + * + * @export + * @interface SignupRequest + */ +export interface SignupRequest { + /** + * + * @type {string} + * @memberof SignupRequest + */ + 'name': string; + /** + * none/author/typist + * @type {string} + * @memberof SignupRequest + */ + 'role': string; + /** + * + * @type {string} + * @memberof SignupRequest + */ + 'authorId'?: string; + /** + * + * @type {number} + * @memberof SignupRequest + */ + 'typistGroupId'?: number; + /** + * + * @type {string} + * @memberof SignupRequest + */ + 'email': string; + /** + * + * @type {boolean} + * @memberof SignupRequest + */ + 'autoRenew': boolean; + /** + * + * @type {boolean} + * @memberof SignupRequest + */ + 'licenseAlert': boolean; + /** + * + * @type {boolean} + * @memberof SignupRequest + */ + 'notification': boolean; +} /** * * @export @@ -180,6 +274,67 @@ export interface TokenResponse { */ 'accessToken': string; } +/** + * + * @export + * @interface User + */ +export interface User { + /** + * + * @type {string} + * @memberof User + */ + 'name': string; + /** + * none/author/typist + * @type {string} + * @memberof User + */ + 'role': string; + /** + * + * @type {string} + * @memberof User + */ + 'authorId': string | null; + /** + * + * @type {string} + * @memberof User + */ + 'typistGroupName': string | null; + /** + * + * @type {string} + * @memberof User + */ + 'email': string; + /** + * + * @type {boolean} + * @memberof User + */ + 'emailVerified': boolean; + /** + * + * @type {boolean} + * @memberof User + */ + 'autoRenew': boolean; + /** + * + * @type {boolean} + * @memberof User + */ + 'licenseAlert': boolean; + /** + * + * @type {boolean} + * @memberof User + */ + 'notification': boolean; +} /** * AccountsApi - axios parameter creator @@ -557,6 +712,182 @@ export class DefaultApi extends BaseAPI { } +/** + * FilesApi - axios parameter creator + * @export + */ +export const FilesApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @summary + * @param {string} id 音声ファイル情報をDBから取得するためのID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + downloadLocation: async (id: string, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('downloadLocation', 'id', id) + const localVarPath = `/files/audio/download-location`; + // 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: 'GET', ...baseOptions, ...options }; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (id !== undefined) { + localVarQueryParameter['id'] = id; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = { ...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers }; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + uploadLocation: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/files/audio/upload-location`; + // 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: 'GET', ...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, + }; + }, + } +}; + +/** + * FilesApi - functional programming interface + * @export + */ +export const FilesApiFp = function (configuration?: Configuration) { + const localVarAxiosParamCreator = FilesApiAxiosParamCreator(configuration) + return { + /** + * + * @summary + * @param {string} id 音声ファイル情報をDBから取得するためのID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async downloadLocation(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.downloadLocation(id, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async uploadLocation(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.uploadLocation(options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + } +}; + +/** + * FilesApi - factory interface + * @export + */ +export const FilesApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = FilesApiFp(configuration) + return { + /** + * + * @summary + * @param {string} id 音声ファイル情報をDBから取得するためのID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + downloadLocation(id: string, options?: any): AxiosPromise { + return localVarFp.downloadLocation(id, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + uploadLocation(options?: any): AxiosPromise { + return localVarFp.uploadLocation(options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * FilesApi - object-oriented interface + * @export + * @class FilesApi + * @extends {BaseAPI} + */ +export class FilesApi extends BaseAPI { + /** + * + * @summary + * @param {string} id 音声ファイル情報をDBから取得するためのID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof FilesApi + */ + public downloadLocation(id: string, options?: AxiosRequestConfig) { + return FilesApiFp(this.configuration).downloadLocation(id, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof FilesApi + */ + public uploadLocation(options?: AxiosRequestConfig) { + return FilesApiFp(this.configuration).uploadLocation(options).then((request) => request(this.axios, this.basePath)); + } +} + + /** * NotificationApi - axios parameter creator * @export @@ -705,6 +1036,116 @@ export const UsersApiAxiosParamCreator = function (configuration?: Configuration localVarRequestOptions.headers = { ...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers }; localVarRequestOptions.data = serializeDataIfNeeded(confirmRequest, localVarRequestOptions, configuration) + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {ConfirmRequest} confirmRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + confirmUserAndInitPassword: async (confirmRequest: ConfirmRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'confirmRequest' is not null or undefined + assertParamExists('confirmUserAndInitPassword', 'confirmRequest', confirmRequest) + const localVarPath = `/users/confirm/initpassword`; + // 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; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = { ...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers }; + localVarRequestOptions.data = serializeDataIfNeeded(confirmRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getUsers: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/users`; + // 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: 'GET', ...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 + * @param {SignupRequest} signupRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + signup: async (signupRequest: SignupRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'signupRequest' is not null or undefined + assertParamExists('signup', 'signupRequest', signupRequest) + const localVarPath = `/users/signup`; + // 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(signupRequest, localVarRequestOptions, configuration) + return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, @@ -731,6 +1172,38 @@ export const UsersApiFp = function (configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.confirmUser(confirmRequest, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @summary + * @param {ConfirmRequest} confirmRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async confirmUserAndInitPassword(confirmRequest: ConfirmRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.confirmUserAndInitPassword(confirmRequest, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getUsers(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getUsers(options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @summary + * @param {SignupRequest} signupRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async signup(signupRequest: SignupRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.signup(signupRequest, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, } }; @@ -751,6 +1224,35 @@ export const UsersApiFactory = function (configuration?: Configuration, basePath confirmUser(confirmRequest: ConfirmRequest, options?: any): AxiosPromise { return localVarFp.confirmUser(confirmRequest, options).then((request) => request(axios, basePath)); }, + /** + * + * @summary + * @param {ConfirmRequest} confirmRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + confirmUserAndInitPassword(confirmRequest: ConfirmRequest, options?: any): AxiosPromise { + return localVarFp.confirmUserAndInitPassword(confirmRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getUsers(options?: any): AxiosPromise { + return localVarFp.getUsers(options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {SignupRequest} signupRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + signup(signupRequest: SignupRequest, options?: any): AxiosPromise { + return localVarFp.signup(signupRequest, options).then((request) => request(axios, basePath)); + }, }; }; @@ -772,6 +1274,41 @@ export class UsersApi extends BaseAPI { public confirmUser(confirmRequest: ConfirmRequest, options?: AxiosRequestConfig) { return UsersApiFp(this.configuration).confirmUser(confirmRequest, options).then((request) => request(this.axios, this.basePath)); } + + /** + * + * @summary + * @param {ConfirmRequest} confirmRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public confirmUserAndInitPassword(confirmRequest: ConfirmRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).confirmUserAndInitPassword(confirmRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public getUsers(options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).getUsers(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {SignupRequest} signupRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public signup(signupRequest: SignupRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).signup(signupRequest, options).then((request) => request(this.axios, this.basePath)); + } } diff --git a/dictation_client/src/features/verify/operations.ts b/dictation_client/src/features/verify/operations.ts index 8ccc42d..71ae1a9 100644 --- a/dictation_client/src/features/verify/operations.ts +++ b/dictation_client/src/features/verify/operations.ts @@ -36,3 +36,36 @@ export const verifyAsync = createAsyncThunk< return thunkApi.rejectWithValue({ error }); } }); + +export const userVerifyAsync = createAsyncThunk< + { + /* Empty Object */ + }, + { + jwt: string; + }, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("verify/userVerifyAsync", async (args, thunkApi) => { + const { jwt } = args; + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const config = new Configuration(configuration); + const usersApi = new UsersApi(config); + + try { + await usersApi.confirmUserAndInitPassword({ token: jwt }); + return {}; + } catch (e) { + // e ⇒ errorObjectに変換 + const error = createErrorObject(e); + + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/features/verify/verifySlice.ts b/dictation_client/src/features/verify/verifySlice.ts index 1039f64..0f1f231 100644 --- a/dictation_client/src/features/verify/verifySlice.ts +++ b/dictation_client/src/features/verify/verifySlice.ts @@ -1,6 +1,6 @@ import { createSlice } from "@reduxjs/toolkit"; import { VerifyState } from "./state"; -import { verifyAsync } from "./operations"; +import { userVerifyAsync, verifyAsync } from "./operations"; const initialState: VerifyState = { apps: { @@ -13,6 +13,7 @@ export const verifySlice = createSlice({ initialState, reducers: {}, extraReducers: (builder) => { + // アカウント登録 builder.addCase(verifyAsync.pending, (state) => { state.apps.VerifyState = "duringVerify"; }); @@ -22,6 +23,23 @@ export const verifySlice = createSlice({ builder.addCase(verifyAsync.rejected, (state, action) => { const { payload } = action; + // メール認証済みかをエラーコードから判定 + if (payload?.error.code === "E010202") { + state.apps.VerifyState = "alreadySuccess"; + } else { + state.apps.VerifyState = "failed"; + } + }); + // ユーザ追加 + builder.addCase(userVerifyAsync.pending, (state) => { + state.apps.VerifyState = "duringVerify"; + }); + builder.addCase(userVerifyAsync.fulfilled, (state) => { + state.apps.VerifyState = "success"; + }); + builder.addCase(userVerifyAsync.rejected, (state, action) => { + const { payload } = action; + // メール認証済みかをエラーコードから判定 if (payload?.error.code === "E010202") { state.apps.VerifyState = "alreadySuccess"; diff --git a/dictation_client/src/pages/UserVerifyPage/index.tsx b/dictation_client/src/pages/UserVerifyPage/index.tsx new file mode 100644 index 0000000..f573ffb --- /dev/null +++ b/dictation_client/src/pages/UserVerifyPage/index.tsx @@ -0,0 +1,55 @@ +import { AppDispatch } from "app/store"; +import Footer from "components/footer"; +import Header from "components/header"; +import React, { useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { useLocation, useNavigate } from "react-router-dom"; +import { userVerifyAsync, VerifyStateSelector } from "features/verify"; + +const UserVerifyPage: React.FC = (): JSX.Element => { + const dispatch: AppDispatch = useDispatch(); + const navigate = useNavigate(); + + const { search } = useLocation(); + const query = new URLSearchParams(search); + const jwt = query.get("verify") ?? ""; + + useEffect(() => { + if (!jwt) { + navigate("/mail-confirm/failed"); + } + dispatch(userVerifyAsync({ jwt })); + }, [navigate, dispatch, jwt]); + + const verifyState = useSelector(VerifyStateSelector); + + useEffect(() => { + switch (verifyState) { + case "duringVerify": + // 認証中は処理なし + break; + case "success": + navigate("/mail-confirm/success"); + break; + case "alreadySuccess": + navigate("/mail-confirm/alreadyExist"); + break; + case "failed": + navigate("/mail-confirm/failed"); + break; + default: + // verifystateが列挙型のため到達しない + break; + } + }, [verifyState, navigate]); + + return ( + <> +
+

loading ...

+