diff --git a/dictation_client/.env b/dictation_client/.env index e69de29..485b347 100644 --- a/dictation_client/.env +++ b/dictation_client/.env @@ -0,0 +1,4 @@ +VITE_STAGE=local +VITE_B2C_CLIENTID=5eb34cba-84b6-46f9-a0ea-bc5c41157d63 +VITE_B2C_AUTHORITY=https://adb2codmsdev.b2clogin.com/adb2codmsdev.onmicrosoft.com/b2c_1_signin_dev +VITE_B2C_KNOWNAUTHORITIES=adb2codmsdev.b2clogin.com diff --git a/dictation_client/.env.local.example b/dictation_client/.env.local.example index 52a3024..48a7afa 100644 --- a/dictation_client/.env.local.example +++ b/dictation_client/.env.local.example @@ -1 +1,4 @@ -REACT_APP_STAGE=local +VITE_STAGE=local +VITE_B2C_CLIENTID=XXXX-XXXX-XXXXX-XXXX +VITE_B2C_AUTHORITY=https://adb2XXXX.XXXX.com/adb2XXXX.onmicrosoft.com/XXXX +VITE_B2C_KNOWNAUTHORITIES=adb2cXXXX.XXXx.com diff --git a/dictation_client/.eslintignore b/dictation_client/.eslintignore index d27de88..cba808a 100644 --- a/dictation_client/.eslintignore +++ b/dictation_client/.eslintignore @@ -3,3 +3,4 @@ build/ .eslintrc.js jest.config.js vite.config.ts +.env.local diff --git a/dictation_client/src/App.tsx b/dictation_client/src/App.tsx index fe311e6..f9a8653 100644 --- a/dictation_client/src/App.tsx +++ b/dictation_client/src/App.tsx @@ -7,6 +7,7 @@ import { msalConfig } from "common/auth/msalConfig"; const App = (): JSX.Element => { const pca = new PublicClientApplication(msalConfig); + return ( <> diff --git a/dictation_client/src/AppRouter.tsx b/dictation_client/src/AppRouter.tsx index 8cc88dc..107a3c8 100644 --- a/dictation_client/src/AppRouter.tsx +++ b/dictation_client/src/AppRouter.tsx @@ -3,6 +3,7 @@ import styled from "styled-components"; import TopPage from "pages/TopPage"; import LoginPage from "pages/LoginPage"; import SamplePage from "pages/SamplePage"; +import { AuthErrorPage } from "pages/ErrorPage"; const AppRouter: React.FC = () => ( @@ -10,6 +11,7 @@ const AppRouter: React.FC = () => ( } /> } /> } /> + } /> ); diff --git a/dictation_client/src/api/.gitignore b/dictation_client/src/api/.gitignore new file mode 100644 index 0000000..149b576 --- /dev/null +++ b/dictation_client/src/api/.gitignore @@ -0,0 +1,4 @@ +wwwroot/*.js +node_modules +typings +dist diff --git a/dictation_client/src/api/.npmignore b/dictation_client/src/api/.npmignore new file mode 100644 index 0000000..999d88d --- /dev/null +++ b/dictation_client/src/api/.npmignore @@ -0,0 +1 @@ +# empty npmignore to ensure all required files (e.g., in the dist folder) are published by npm \ No newline at end of file diff --git a/dictation_client/src/api/.openapi-generator-ignore b/dictation_client/src/api/.openapi-generator-ignore new file mode 100644 index 0000000..7484ee5 --- /dev/null +++ b/dictation_client/src/api/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/dictation_client/src/api/.openapi-generator/FILES b/dictation_client/src/api/.openapi-generator/FILES new file mode 100644 index 0000000..16b445e --- /dev/null +++ b/dictation_client/src/api/.openapi-generator/FILES @@ -0,0 +1,9 @@ +.gitignore +.npmignore +.openapi-generator-ignore +api.ts +base.ts +common.ts +configuration.ts +git_push.sh +index.ts diff --git a/dictation_client/src/api/.openapi-generator/VERSION b/dictation_client/src/api/.openapi-generator/VERSION new file mode 100644 index 0000000..c0be8a7 --- /dev/null +++ b/dictation_client/src/api/.openapi-generator/VERSION @@ -0,0 +1 @@ +6.4.0 \ No newline at end of file diff --git a/dictation_client/src/api/api.ts b/dictation_client/src/api/api.ts new file mode 100644 index 0000000..9fec2f9 --- /dev/null +++ b/dictation_client/src/api/api.ts @@ -0,0 +1,365 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * ODMSOpenAPI + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import type { Configuration } from './configuration'; +import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios'; +import globalAxios from 'axios'; +// Some imports not used depending on template conditions +// @ts-ignore +import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from './common'; +import type { RequestArgs } from './base'; +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS, BaseAPI, RequiredError } from './base'; + +/** + * + * @export + * @interface AccessTokenResponse + */ +export interface AccessTokenResponse { + /** + * + * @type {string} + * @memberof AccessTokenResponse + */ + 'accessToken': string; +} +/** + * + * @export + * @interface ErrorResponse + */ +export interface ErrorResponse { + /** + * + * @type {string} + * @memberof ErrorResponse + */ + 'message': string; + /** + * + * @type {string} + * @memberof ErrorResponse + */ + 'code': string; +} +/** + * + * @export + * @interface TokenRequest + */ +export interface TokenRequest { + /** + * + * @type {string} + * @memberof TokenRequest + */ + 'idToken': string; + /** + * web or mobile or desktop + * @type {string} + * @memberof TokenRequest + */ + 'type': string; +} +/** + * + * @export + * @interface TokenResponse + */ +export interface TokenResponse { + /** + * + * @type {string} + * @memberof TokenResponse + */ + 'refreshToken': string; + /** + * + * @type {string} + * @memberof TokenResponse + */ + 'accessToken': string; +} + +/** + * AuthApi - axios parameter creator + * @export + */ +export const AuthApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + accessToken: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/auth/accessToken`; + // 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 + * @param {TokenRequest} tokenRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + token: async (tokenRequest: TokenRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'tokenRequest' is not null or undefined + assertParamExists('token', 'tokenRequest', tokenRequest) + const localVarPath = `/auth/token`; + // 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(tokenRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * AuthApi - functional programming interface + * @export + */ +export const AuthApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = AuthApiAxiosParamCreator(configuration) + return { + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async accessToken(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.accessToken(options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @summary + * @param {TokenRequest} tokenRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async token(tokenRequest: TokenRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.token(tokenRequest, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + } +}; + +/** + * AuthApi - factory interface + * @export + */ +export const AuthApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = AuthApiFp(configuration) + return { + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + accessToken(options?: any): AxiosPromise { + return localVarFp.accessToken(options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {TokenRequest} tokenRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + token(tokenRequest: TokenRequest, options?: any): AxiosPromise { + return localVarFp.token(tokenRequest, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * AuthApi - object-oriented interface + * @export + * @class AuthApi + * @extends {BaseAPI} + */ +export class AuthApi extends BaseAPI { + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AuthApi + */ + public accessToken(options?: AxiosRequestConfig) { + return AuthApiFp(this.configuration).accessToken(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {TokenRequest} tokenRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AuthApi + */ + public token(tokenRequest: TokenRequest, options?: AxiosRequestConfig) { + return AuthApiFp(this.configuration).token(tokenRequest, options).then((request) => request(this.axios, this.basePath)); + } +} + + +/** + * DefaultApi - axios parameter creator + * @export + */ +export const DefaultApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + checkHealth: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/health`; + // 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; + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * DefaultApi - functional programming interface + * @export + */ +export const DefaultApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = DefaultApiAxiosParamCreator(configuration) + return { + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async checkHealth(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.checkHealth(options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + } +}; + +/** + * DefaultApi - factory interface + * @export + */ +export const DefaultApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = DefaultApiFp(configuration) + return { + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + checkHealth(options?: any): AxiosPromise { + return localVarFp.checkHealth(options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * DefaultApi - object-oriented interface + * @export + * @class DefaultApi + * @extends {BaseAPI} + */ +export class DefaultApi extends BaseAPI { + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public checkHealth(options?: AxiosRequestConfig) { + return DefaultApiFp(this.configuration).checkHealth(options).then((request) => request(this.axios, this.basePath)); + } +} + + diff --git a/dictation_client/src/api/base.ts b/dictation_client/src/api/base.ts new file mode 100644 index 0000000..0a09f9c --- /dev/null +++ b/dictation_client/src/api/base.ts @@ -0,0 +1,72 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * ODMSOpenAPI + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import type { Configuration } from './configuration'; +// Some imports not used depending on template conditions +// @ts-ignore +import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios'; +import globalAxios from 'axios'; + +export const BASE_PATH = "http://localhost".replace(/\/+$/, ""); + +/** + * + * @export + */ +export const COLLECTION_FORMATS = { + csv: ",", + ssv: " ", + tsv: "\t", + pipes: "|", +}; + +/** + * + * @export + * @interface RequestArgs + */ +export interface RequestArgs { + url: string; + options: AxiosRequestConfig; +} + +/** + * + * @export + * @class BaseAPI + */ +export class BaseAPI { + protected configuration: Configuration | undefined; + + constructor(configuration?: Configuration, protected basePath: string = BASE_PATH, protected axios: AxiosInstance = globalAxios) { + if (configuration) { + this.configuration = configuration; + this.basePath = configuration.basePath || this.basePath; + } + } +}; + +/** + * + * @export + * @class RequiredError + * @extends {Error} + */ +export class RequiredError extends Error { + constructor(public field: string, msg?: string) { + super(msg); + this.name = "RequiredError" + } +} diff --git a/dictation_client/src/api/common.ts b/dictation_client/src/api/common.ts new file mode 100644 index 0000000..c6c9897 --- /dev/null +++ b/dictation_client/src/api/common.ts @@ -0,0 +1,150 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * ODMSOpenAPI + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import type { Configuration } from "./configuration"; +import type { RequestArgs } from "./base"; +import type { AxiosInstance, AxiosResponse } from 'axios'; +import { RequiredError } from "./base"; + +/** + * + * @export + */ +export const DUMMY_BASE_URL = 'https://example.com' + +/** + * + * @throws {RequiredError} + * @export + */ +export const assertParamExists = function (functionName: string, paramName: string, paramValue: unknown) { + if (paramValue === null || paramValue === undefined) { + throw new RequiredError(paramName, `Required parameter ${paramName} was null or undefined when calling ${functionName}.`); + } +} + +/** + * + * @export + */ +export const setApiKeyToObject = async function (object: any, keyParamName: string, configuration?: Configuration) { + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? await configuration.apiKey(keyParamName) + : await configuration.apiKey; + object[keyParamName] = localVarApiKeyValue; + } +} + +/** + * + * @export + */ +export const setBasicAuthToObject = function (object: any, configuration?: Configuration) { + if (configuration && (configuration.username || configuration.password)) { + object["auth"] = { username: configuration.username, password: configuration.password }; + } +} + +/** + * + * @export + */ +export const setBearerAuthToObject = async function (object: any, configuration?: Configuration) { + if (configuration && configuration.accessToken) { + const accessToken = typeof configuration.accessToken === 'function' + ? await configuration.accessToken() + : await configuration.accessToken; + object["Authorization"] = "Bearer " + accessToken; + } +} + +/** + * + * @export + */ +export const setOAuthToObject = async function (object: any, name: string, scopes: string[], configuration?: Configuration) { + if (configuration && configuration.accessToken) { + const localVarAccessTokenValue = typeof configuration.accessToken === 'function' + ? await configuration.accessToken(name, scopes) + : await configuration.accessToken; + object["Authorization"] = "Bearer " + localVarAccessTokenValue; + } +} + +function setFlattenedQueryParams(urlSearchParams: URLSearchParams, parameter: any, key: string = ""): void { + if (parameter == null) return; + if (typeof parameter === "object") { + if (Array.isArray(parameter)) { + (parameter as any[]).forEach(item => setFlattenedQueryParams(urlSearchParams, item, key)); + } + else { + Object.keys(parameter).forEach(currentKey => + setFlattenedQueryParams(urlSearchParams, parameter[currentKey], `${key}${key !== '' ? '.' : ''}${currentKey}`) + ); + } + } + else { + if (urlSearchParams.has(key)) { + urlSearchParams.append(key, parameter); + } + else { + urlSearchParams.set(key, parameter); + } + } +} + +/** + * + * @export + */ +export const setSearchParams = function (url: URL, ...objects: any[]) { + const searchParams = new URLSearchParams(url.search); + setFlattenedQueryParams(searchParams, objects); + url.search = searchParams.toString(); +} + +/** + * + * @export + */ +export const serializeDataIfNeeded = function (value: any, requestOptions: any, configuration?: Configuration) { + const nonString = typeof value !== 'string'; + const needsSerialization = nonString && configuration && configuration.isJsonMime + ? configuration.isJsonMime(requestOptions.headers['Content-Type']) + : nonString; + return needsSerialization + ? JSON.stringify(value !== undefined ? value : {}) + : (value || ""); +} + +/** + * + * @export + */ +export const toPathString = function (url: URL) { + return url.pathname + url.search + url.hash +} + +/** + * + * @export + */ +export const createRequestFunction = function (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) { + return >(axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { + const axiosRequestArgs = {...axiosArgs.options, url: (configuration?.basePath || basePath) + axiosArgs.url}; + return axios.request(axiosRequestArgs); + }; +} diff --git a/dictation_client/src/api/configuration.ts b/dictation_client/src/api/configuration.ts new file mode 100644 index 0000000..eabe78f --- /dev/null +++ b/dictation_client/src/api/configuration.ts @@ -0,0 +1,101 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * ODMSOpenAPI + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface ConfigurationParameters { + apiKey?: string | Promise | ((name: string) => string) | ((name: string) => Promise); + username?: string; + password?: string; + accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise); + basePath?: string; + baseOptions?: any; + formDataCtor?: new () => any; +} + +export class Configuration { + /** + * parameter for apiKey security + * @param name security name + * @memberof Configuration + */ + apiKey?: string | Promise | ((name: string) => string) | ((name: string) => Promise); + /** + * parameter for basic security + * + * @type {string} + * @memberof Configuration + */ + username?: string; + /** + * parameter for basic security + * + * @type {string} + * @memberof Configuration + */ + password?: string; + /** + * parameter for oauth2 security + * @param name security name + * @param scopes oauth2 scope + * @memberof Configuration + */ + accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise); + /** + * override base path + * + * @type {string} + * @memberof Configuration + */ + basePath?: string; + /** + * base options for axios calls + * + * @type {any} + * @memberof Configuration + */ + baseOptions?: any; + /** + * The FormData constructor that will be used to create multipart form data + * requests. You can inject this here so that execution environments that + * do not support the FormData class can still run the generated client. + * + * @type {new () => FormData} + */ + formDataCtor?: new () => any; + + constructor(param: ConfigurationParameters = {}) { + this.apiKey = param.apiKey; + this.username = param.username; + this.password = param.password; + this.accessToken = param.accessToken; + this.basePath = param.basePath; + this.baseOptions = param.baseOptions; + this.formDataCtor = param.formDataCtor; + } + + /** + * Check if the given MIME is a JSON MIME. + * JSON MIME examples: + * application/json + * application/json; charset=UTF8 + * APPLICATION/JSON + * application/vnd.company+json + * @param mime - MIME (Multipurpose Internet Mail Extensions) + * @return True if the given MIME is JSON, false otherwise. + */ + public isJsonMime(mime: string): boolean { + const jsonMime: RegExp = new RegExp('^(application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(;.*)?$', 'i'); + return mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json'); + } +} diff --git a/dictation_client/src/api/git_push.sh b/dictation_client/src/api/git_push.sh new file mode 100644 index 0000000..f53a75d --- /dev/null +++ b/dictation_client/src/api/git_push.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ +# +# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com" + +git_user_id=$1 +git_repo_id=$2 +release_note=$3 +git_host=$4 + +if [ "$git_host" = "" ]; then + git_host="github.com" + echo "[INFO] No command line input provided. Set \$git_host to $git_host" +fi + +if [ "$git_user_id" = "" ]; then + git_user_id="GIT_USER_ID" + echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" +fi + +if [ "$git_repo_id" = "" ]; then + git_repo_id="GIT_REPO_ID" + echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" +fi + +if [ "$release_note" = "" ]; then + release_note="Minor update" + echo "[INFO] No command line input provided. Set \$release_note to $release_note" +fi + +# Initialize the local directory as a Git repository +git init + +# Adds the files in the local repository and stages them for commit. +git add . + +# Commits the tracked changes and prepares them to be pushed to a remote repository. +git commit -m "$release_note" + +# Sets the new remote +git_remote=$(git remote) +if [ "$git_remote" = "" ]; then # git remote not defined + + if [ "$GIT_TOKEN" = "" ]; then + echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." + git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git + else + git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git + fi + +fi + +git pull origin master + +# Pushes (Forces) the changes in the local repository up to the remote repository +echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" +git push origin master 2>&1 | grep -v 'To https' diff --git a/dictation_client/src/api/index.ts b/dictation_client/src/api/index.ts new file mode 100644 index 0000000..c982723 --- /dev/null +++ b/dictation_client/src/api/index.ts @@ -0,0 +1,18 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * ODMSOpenAPI + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export * from "./api"; +export * from "./configuration"; + diff --git a/dictation_client/src/app/store.ts b/dictation_client/src/app/store.ts index 826f785..cbfa897 100644 --- a/dictation_client/src/app/store.ts +++ b/dictation_client/src/app/store.ts @@ -1,7 +1,12 @@ import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit"; +import login from "features/login/loginSlice"; +import auth from "features/auth/authSlice"; export const store = configureStore({ - reducer: {}, + reducer: { + login, + auth, + }, }); export type RootState = ReturnType; diff --git a/dictation_client/src/common/auth/msalConfig.ts b/dictation_client/src/common/auth/msalConfig.ts index 3dbc362..e90e744 100644 --- a/dictation_client/src/common/auth/msalConfig.ts +++ b/dictation_client/src/common/auth/msalConfig.ts @@ -2,14 +2,16 @@ import { Configuration, RedirectRequest } from "@azure/msal-browser"; export const msalConfig: Configuration = { auth: { - clientId: "5eb34cba-84b6-46f9-a0ea-bc5c41157d63", - authority: - "https://adb2codmsdev.b2clogin.com/adb2codmsdev.onmicrosoft.com/b2c_1_signin_dev", - knownAuthorities: ["adb2codmsdev.b2clogin.com"], + 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`, - postLogoutRedirectUri: "/", navigateToLoginRequestUrl: false, }, + cache: { + cacheLocation: "localStorage", + storeAuthStateInCookie: false, + }, }; export const loginRequest: RedirectRequest = { diff --git a/dictation_client/src/features/auth/authSlice.ts b/dictation_client/src/features/auth/authSlice.ts new file mode 100644 index 0000000..b88c705 --- /dev/null +++ b/dictation_client/src/features/auth/authSlice.ts @@ -0,0 +1,49 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { + removeAccessToken, + initialConfig, + loadAccessToken, + saveAccessToken, + loadRefreshToken, + saveRefreshToken, + removeRefreshToken, +} from "./utils"; +import { AuthState } from "./state"; + +const initialState: AuthState = { + configuration: initialConfig(), + accessToken: loadAccessToken(), + refreshToken: loadRefreshToken(), +}; + +export const authSlice = createSlice({ + name: "auth", + initialState, + reducers: { + setToken: ( + state, + action: PayloadAction> + ) => { + const { accessToken, refreshToken } = action.payload; + if (accessToken && refreshToken) { + state.configuration.accessToken = accessToken; + state.accessToken = accessToken; + saveAccessToken(accessToken); + + state.refreshToken = refreshToken; + saveRefreshToken(refreshToken); + } + }, + clearToken: (state) => { + state.configuration.accessToken = undefined; + state.accessToken = null; + state.refreshToken = null; + removeAccessToken(); + removeRefreshToken(); + }, + }, +}); + +export const { setToken, clearToken } = authSlice.actions; + +export default authSlice.reducer; diff --git a/dictation_client/src/features/auth/index.ts b/dictation_client/src/features/auth/index.ts new file mode 100644 index 0000000..cf146e7 --- /dev/null +++ b/dictation_client/src/features/auth/index.ts @@ -0,0 +1 @@ +export { authSlice, clearToken, setToken } from "./authSlice"; diff --git a/dictation_client/src/features/auth/state.ts b/dictation_client/src/features/auth/state.ts new file mode 100644 index 0000000..8fbfd8a --- /dev/null +++ b/dictation_client/src/features/auth/state.ts @@ -0,0 +1,7 @@ +import { ConfigurationParameters } from "api"; + +export interface AuthState { + configuration: ConfigurationParameters; + accessToken: string | null; + refreshToken: string | null; +} diff --git a/dictation_client/src/features/auth/utils.ts b/dictation_client/src/features/auth/utils.ts new file mode 100644 index 0000000..8923183 --- /dev/null +++ b/dictation_client/src/features/auth/utils.ts @@ -0,0 +1,59 @@ +import { ConfigurationParameters } from "api"; + +/** + * Get access token + * @returns access token + */ +export const loadAccessToken = (): string | null => + localStorage.getItem("accessToken"); +/** + * Set access token + * @param accessToken + */ +export const saveAccessToken = (accessToken: string): void => { + localStorage.setItem("accessToken", accessToken); +}; +/** + * Remove access token + */ +export const removeAccessToken = (): void => { + localStorage.removeItem("accessToken"); +}; + +/** + * Get refresh token + * @returns refresh token + */ +export const loadRefreshToken = (): string | null => + localStorage.getItem("refreshToken"); +/** + * Set refresh token + * @param refreshToken + */ +export const saveRefreshToken = (refreshToken: string): void => { + localStorage.setItem("refreshToken", refreshToken); +}; +/** + * Remove refresh token + */ +export const removeRefreshToken = (): void => { + localStorage.removeItem("refreshToken"); +}; + +// 初期状態のAPI Config +export const initialConfig = (): ConfigurationParameters => { + const config: ConfigurationParameters = {}; + if (import.meta.env.VITE_STAGE === "local") { + config.basePath = "http://localhost"; + } else { + config.basePath = window.location.origin; + } + + // localStorageからAccessTokenを復元する + const accessToken = loadAccessToken(); + if (accessToken) { + config.accessToken = accessToken; + } + + return config; +}; diff --git a/dictation_client/src/features/login/index.ts b/dictation_client/src/features/login/index.ts new file mode 100644 index 0000000..fe55245 --- /dev/null +++ b/dictation_client/src/features/login/index.ts @@ -0,0 +1,5 @@ +export * from "./loginSlice"; +export * from "./state"; +export * from "./selectors"; +export * from "./operations"; +export * from "./types"; diff --git a/dictation_client/src/features/login/loginSlice.ts b/dictation_client/src/features/login/loginSlice.ts new file mode 100644 index 0000000..2fea776 --- /dev/null +++ b/dictation_client/src/features/login/loginSlice.ts @@ -0,0 +1,28 @@ +import { createSlice } from "@reduxjs/toolkit"; +import { loginAsync } from "./operations"; +import { LoginState } from "./state"; + +const initialState: LoginState = { + apps: { + loadState: "Loading", + }, +}; + +export const loginSlice = createSlice({ + name: "login", + initialState, + reducers: {}, + extraReducers: (builder) => { + builder.addCase(loginAsync.pending, (state) => { + state.apps.loadState = "Loading"; + }); + builder.addCase(loginAsync.fulfilled, (state) => { + state.apps.loadState = "Loaded"; + }); + builder.addCase(loginAsync.rejected, (state) => { + state.apps.loadState = "LoadError"; + }); + }, +}); + +export default loginSlice.reducer; diff --git a/dictation_client/src/features/login/operations.ts b/dictation_client/src/features/login/operations.ts new file mode 100644 index 0000000..a711f01 --- /dev/null +++ b/dictation_client/src/features/login/operations.ts @@ -0,0 +1,50 @@ +import { IPublicClientApplication, SilentRequest } from "@azure/msal-browser"; +import { createAsyncThunk } from "@reduxjs/toolkit"; +import type { RootState } from "app/store"; +import { setToken } from "features/auth/authSlice"; +import { AuthApi } from "../../api/api"; +import { Configuration } from "../../api/configuration"; + +export const loginAsync = createAsyncThunk< + { + /* Empty Object */ + }, + { + instance: IPublicClientApplication; + request: SilentRequest; + }, + { + // rejectした時の返却値の型 + rejectValue: { + /* Empty Object */ + }; + } +>("login/loginAsync", async (args, thunkApi) => { + const { instance, request } = args; + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const config = new Configuration(configuration); + const authApi = new AuthApi(config); + + try { + // B2CからIDトークンを取得 + const b2cToken = await instance.acquireTokenSilent(request); + const { data } = await authApi.token({ + idToken: b2cToken.idToken, + type: "web", + }); + // アクセストークン・リフレッシュトークンをlocalStorageに保存 + thunkApi.dispatch( + setToken({ + accessToken: data.accessToken, + refreshToken: data.refreshToken, + }) + ); + + return data; + } catch (e) { + return thunkApi.rejectWithValue({}); + } +}); diff --git a/dictation_client/src/features/login/selectors.ts b/dictation_client/src/features/login/selectors.ts new file mode 100644 index 0000000..9a3f444 --- /dev/null +++ b/dictation_client/src/features/login/selectors.ts @@ -0,0 +1,3 @@ +import { RootState } from "app/store"; + +export const selectLoadState = (state: RootState) => state.login.apps.loadState; diff --git a/dictation_client/src/features/login/state.ts b/dictation_client/src/features/login/state.ts new file mode 100644 index 0000000..caed902 --- /dev/null +++ b/dictation_client/src/features/login/state.ts @@ -0,0 +1,9 @@ +import { LoadState } from "./types"; + +export interface LoginState { + apps: Apps; +} + +export interface Apps { + loadState: LoadState; +} diff --git a/dictation_client/src/features/login/types.ts b/dictation_client/src/features/login/types.ts new file mode 100644 index 0000000..c335bd5 --- /dev/null +++ b/dictation_client/src/features/login/types.ts @@ -0,0 +1 @@ +export type LoadState = "Loading" | "Loaded" | "LoadError"; diff --git a/dictation_client/src/pages/ErrorPage/index.tsx b/dictation_client/src/pages/ErrorPage/index.tsx new file mode 100644 index 0000000..4592ea0 --- /dev/null +++ b/dictation_client/src/pages/ErrorPage/index.tsx @@ -0,0 +1,8 @@ +import React from "react"; + +export const AuthErrorPage = (): JSX.Element => ( +
+

ログインに失敗しました

+
+
+); diff --git a/dictation_client/src/pages/LoginPage/index.tsx b/dictation_client/src/pages/LoginPage/index.tsx index 2b78f82..4eb5ed4 100644 --- a/dictation_client/src/pages/LoginPage/index.tsx +++ b/dictation_client/src/pages/LoginPage/index.tsx @@ -1,28 +1,43 @@ -import { SilentRequest } from "@azure/msal-browser"; +import { InteractionStatus, SilentRequest } from "@azure/msal-browser"; import { useMsal } from "@azure/msal-react"; -import React, { useState } from "react"; +import { AppDispatch } from "app/store"; +import { loginAsync } from "features/login"; +import React, { useCallback, useEffect } from "react"; +import { useDispatch } from "react-redux"; +import { useNavigate } from "react-router-dom"; const LoginPage: React.FC = () => { - const { accounts, instance } = useMsal(); - const [idToken, setIdToken] = useState(null); + const { accounts, instance, inProgress } = useMsal(); + const dispatch: AppDispatch = useDispatch(); + const navigate = useNavigate(); - const request: SilentRequest = { - scopes: ["openid"], - account: accounts[0], - }; - // TODO store側でこの処理を行うべきなので、Task1361で移植する - instance.acquireTokenSilent(request).then((response) => { - setIdToken(response.idToken); - }); - return ( - <> -
{idToken}
-
- - - ); + const login = useCallback(async () => { + const request: SilentRequest = { + scopes: ["openid"], + account: accounts[0], + }; + // ログイン処理呼び出し + const { meta } = await dispatch(loginAsync({ instance, request })); + + // ログイン失敗した場合、B2Cをログアウトしてからエラーページに遷移する + if (meta.requestStatus === "rejected") { + instance.logout({ + postLogoutRedirectUri: "/AuthError", + }); + } + if (meta.requestStatus === "fulfilled") { + navigate("/XXX"); + } + }, [accounts, dispatch, instance, navigate]); + + useEffect(() => { + // B2CからリダイレクトされてB2Cへのログインが完了してからAPIを呼ぶ + if (inProgress === InteractionStatus.None) { + login(); + } + }, [accounts, dispatch, inProgress, instance, login]); + + return

loading ...

; }; export default LoginPage; diff --git a/dictation_client/src/pages/SamplePage/index.tsx b/dictation_client/src/pages/SamplePage/index.tsx index 754ba8e..f93f218 100644 --- a/dictation_client/src/pages/SamplePage/index.tsx +++ b/dictation_client/src/pages/SamplePage/index.tsx @@ -1,9 +1,19 @@ +import { useMsal } from "@azure/msal-react"; import React from "react"; -const SamplePage: React.FC = () => ( -
-

hello world!!

-
-); +const SamplePage: React.FC = () => { + const { instance } = useMsal(); + return ( +
+

hello world!!

+ +
+ ); +}; export default SamplePage; diff --git a/dictation_client/src/pages/TopPage/index.tsx b/dictation_client/src/pages/TopPage/index.tsx index d6440c0..40e0b80 100644 --- a/dictation_client/src/pages/TopPage/index.tsx +++ b/dictation_client/src/pages/TopPage/index.tsx @@ -1,6 +1,6 @@ -import React from "react"; import { useMsal } from "@azure/msal-react"; import { loginRequest } from "common/auth/msalConfig"; +import React from "react"; const TopPage: React.FC = () => { const { instance } = useMsal(); diff --git a/dictation_client/src/react-app-env.d.ts b/dictation_client/src/react-app-env.d.ts index 6431bc5..c981c95 100644 --- a/dictation_client/src/react-app-env.d.ts +++ b/dictation_client/src/react-app-env.d.ts @@ -1 +1,13 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +// 環境変数のコード補完 /// +interface ImportMetaEnv { + readonly VITE_STAGE: string; + readonly VITE_B2C_CLIENTID: string; + readonly VITE_B2C_AUTHORITY: string; + readonly VITE_B2C_KNOWNAUTHORITIES: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/dictation_client/vite.config.ts b/dictation_client/vite.config.ts index c132c49..3918d41 100644 --- a/dictation_client/vite.config.ts +++ b/dictation_client/vite.config.ts @@ -14,5 +14,5 @@ export default defineConfig({ sourcemap: true, minify: false, }, - plugins: [env({ prefix: "REACT_APP_" }), tsconfigPaths(), react()], + plugins: [env(), tsconfigPaths(), react()], });