From f3edbb0bf36717690ad7e7fd10d9b97f8193a803 Mon Sep 17 00:00:00 2001 From: Hubert Bukowski Date: Tue, 28 Dec 2021 20:31:10 +0100 Subject: [PATCH] Feature: Added possibility to start session using social OAuth providers. --- .github/workflows/build.yml | 12 +- .gitignore | 2 + .vscode/extensions.json | 7 +- .vscode/settings.json | 9 ++ calculate-version.js | 14 +- oidc/B2C_1A_Common.xml | 145 ++++++++++++++++++++ oidc/B2C_1A_Facebook.xml | 85 ++++++++++++ oidc/B2C_1A_GitHub.xml | 87 ++++++++++++ oidc/B2C_1A_Google.xml | 83 +++++++++++ package.json | 18 ++- src/App.tsx | 69 +++++++--- src/Types.ts | 8 +- src/components/Authenticated.tsx | 20 +++ src/components/IdentityProviderButton.tsx | 24 ++++ src/components/IdentityProviderCallback.tsx | 43 ++++++ src/components/SessionLabel.tsx | 14 ++ src/components/SignOutButton.tsx | 22 +++ src/components/Unauthenticated.tsx | 20 +++ src/index.tsx | 2 +- src/openid/Facebook.svg | 4 + src/openid/Google.svg | 8 ++ src/openid/openid.config.json | 8 ++ src/openid/openid.ts | 103 ++++++++++++++ src/store/configureStore.ts | 42 ------ src/store/index.ts | 21 --- staticwebapp.config.json | 8 +- 26 files changed, 765 insertions(+), 113 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 oidc/B2C_1A_Common.xml create mode 100644 oidc/B2C_1A_Facebook.xml create mode 100644 oidc/B2C_1A_GitHub.xml create mode 100644 oidc/B2C_1A_Google.xml create mode 100644 src/components/Authenticated.tsx create mode 100644 src/components/IdentityProviderButton.tsx create mode 100644 src/components/IdentityProviderCallback.tsx create mode 100644 src/components/SessionLabel.tsx create mode 100644 src/components/SignOutButton.tsx create mode 100644 src/components/Unauthenticated.tsx create mode 100644 src/openid/Facebook.svg create mode 100644 src/openid/Google.svg create mode 100644 src/openid/openid.config.json create mode 100644 src/openid/openid.ts delete mode 100644 src/store/configureStore.ts delete mode 100644 src/store/index.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 85e00de..cfbf2ed 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,11 +11,12 @@ on: jobs: build_and_deploy_job: + name: Build and Deploy Job if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') runs-on: ubuntu-latest - name: Build and Deploy Job steps: - - uses: actions/checkout@v2 + - name: Checkout repository + uses: actions/checkout@v2 with: submodules: true fetch-depth: 0 @@ -45,17 +46,16 @@ jobs: }) - name: Fetch tag + if: github.event_name == 'push' run: git fetch --tags - name: Cache node modules uses: actions/cache@v2 - env: - cache-name: cache-node-modules with: path: ./node_modules - key: ${{ env.cache-name }}-${{ hashFiles('./yarn.json') }} + key: app-node-modules-${{ hashFiles('./yarn.json') }} restore-keys: | - ${{ env.cache-name }}- + app-node-modules- - name: Build And Deploy uses: Azure/static-web-apps-deploy@v1 diff --git a/.gitignore b/.gitignore index 59b7a92..b275fec 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ # Visual Studo cache/options directory. #################################################################### +local.settings.json + #################################################################### # Autogenerated files. #################################################################### diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 309fcf8..fbcf50f 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,6 +1,11 @@ { "recommendations": [ "ms-azuretools.vscode-azurestaticwebapps", - "editorconfig.editorconfig" + "editorconfig.editorconfig", + "ms-vscode.azure-account", + "azureadb2ctools.aadb2c", + "ms-vscode.vscode-node-azure-pack", + "streetsidesoftware.code-spell-checker", + "npxms.hide-gitignored" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6e516cb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "cSpell.words": [ + "browserslist", + "dgtiles", + "jwks", + "leetabit", + "reduxjs" + ] +} diff --git a/calculate-version.js b/calculate-version.js index 01734f6..65bd724 100644 --- a/calculate-version.js +++ b/calculate-version.js @@ -65,13 +65,13 @@ module.exports = async function() { version.commitCount = commits.length; const dateTime = new Date(); - const year = dateTime.getFullYear(); - const month = `${dateTime.getMonth() + 1}`.padStart(2, '0'); - const day =`${dateTime.getDate()}`.padStart(2, '0'); - const hours =`${dateTime.getHours()}`.padStart(2, '0'); - const minutes =`${dateTime.getMinutes()}`.padStart(2, '0'); - const seconds =`${dateTime.getSeconds()}`.padStart(2, '0'); - const milliseconds =`${dateTime.getMilliseconds()}`.padStart(2, '0'); + const year = dateTime.getUTCFullYear(); + const month = `${dateTime.getUTCMonth() + 1}`.padStart(2, '0'); + const day =`${dateTime.getUTCDate()}`.padStart(2, '0'); + const hours =`${dateTime.getUTCHours()}`.padStart(2, '0'); + const minutes =`${dateTime.getUTCMinutes()}`.padStart(2, '0'); + const seconds =`${dateTime.getUTCSeconds()}`.padStart(2, '0'); + const milliseconds =`${dateTime.getUTCMilliseconds()}`.padStart(2, '0'); try { await executeCommandAsync('git diff-index --quiet HEAD --'); } diff --git a/oidc/B2C_1A_Common.xml b/oidc/B2C_1A_Common.xml new file mode 100644 index 0000000..f3f6ee8 --- /dev/null +++ b/oidc/B2C_1A_Common.xml @@ -0,0 +1,145 @@ + + + + + + + + string + + + + Subject + string + + + + + + + + subjectWithIssuerCollection + stringCollection + + + + Numeric user Identifier + long + + + + subjectWithIssuer + string + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Trustframework Policy Engine TechnicalProfiles + + + Trustframework Policy Engine Default Technical Profile + + + {service:te} + + + + + + + + Token Issuer Technical Profiles + + + JWT Issuer + + JWT + + {service:te} + true + sub + + + + + + + + + + + diff --git a/oidc/B2C_1A_Facebook.xml b/oidc/B2C_1A_Facebook.xml new file mode 100644 index 0000000..c81685d --- /dev/null +++ b/oidc/B2C_1A_Facebook.xml @@ -0,0 +1,85 @@ + + + + + dgtiles.onmicrosoft.com + B2C_1A_Common + + + + + facebook.com + Facebook + + + + Facebook + + + facebook.com + https://www.facebook.com/dialog/oauth + https://graph.facebook.com/oauth/access_token + GET + 0 + 300225215495897 + reauthenticate + public_profile + select_account + https://graph.facebook.com/me?fields=id + json + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Facebook Policy Profile + + + + + + + + diff --git a/oidc/B2C_1A_GitHub.xml b/oidc/B2C_1A_GitHub.xml new file mode 100644 index 0000000..6e6d3d6 --- /dev/null +++ b/oidc/B2C_1A_GitHub.xml @@ -0,0 +1,87 @@ + + + + + dgtiles.onmicrosoft.com + B2C_1A_Common + + + + + github.com + GitHub + + + + GitHub + + + github.com + https://github.com/login/oauth/authorize + https://github.com/login/oauth/access_token + https://api.github.com/user + GET + read:user + select_account + 0 + AuthorizationHeader + CPIM-Basic/{tenant}/{policy} + 496f81afc422a2de27db + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + GitHub Policy Profile + + + + + + + + + diff --git a/oidc/B2C_1A_Google.xml b/oidc/B2C_1A_Google.xml new file mode 100644 index 0000000..34deab5 --- /dev/null +++ b/oidc/B2C_1A_Google.xml @@ -0,0 +1,83 @@ + + + + + dgtiles.onmicrosoft.com + B2C_1A_Common + + + + + google.com + Google + + + + Google + + + https://accounts.google.com/.well-known/openid-configuration + openid + id_token + form_post + select_account + POST + false + 843350363689-4nq0rjr3f8t47lklnqoqhvk12sgdip00.apps.googleusercontent.com + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Google Policy Profile + + + + + + + + + diff --git a/package.json b/package.json index f45ae4b..0bf6fd1 100644 --- a/package.json +++ b/package.json @@ -5,20 +5,18 @@ "typescript": "4.3.5", "react": "17.0.2", "react-dom": "17.0.2", - "react-router-dom": "5.3.0", - "connected-react-router": "6.9.1", - "react-redux": "7.2.5", - "redux-saga": "1.1.3", - "@reduxjs/toolkit": "1.6.1", - "history": "4.10.1", - "@emotion/react": "11.4.1" + "react-router-dom": "6.2.1", + "js-cookie": "3.0.1", + + "@emotion/react": "11.4.1", + "jose": "4.3.7" }, "devDependencies": { "@types/react": "17.0.2", "@types/react-dom": "17.0.2", - "@types/react-router-dom": "5.1.2", - "@types/react-redux": "7.1.18", - "@types/history": "4.7.9", + "@types/react-router-dom": "5.3.2", + "@types/js-cookie": "3.0.1", + "yarn-or-npm": "3.0.1", "rimraf": "3.0.2", "react-scripts": "4.0.3", diff --git a/src/App.tsx b/src/App.tsx index 21325fd..598308f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,28 +4,61 @@ // // @jsxImportSource @emotion/react -import { Component } from 'react'; -import { Provider } from 'react-redux'; -import { ConnectedRouter } from 'connected-react-router'; -import configureStore from './store/configureStore'; -import { createBrowserHistory } from 'history'; +import React from 'react'; +import { useState } from "react"; +import { useEffect } from 'react'; +import { Routes, Route, BrowserRouter } from "react-router-dom"; +import Cookies from 'js-cookie'; import { Optional } from './Types'; import { VersionLabel } from './components/VersionLabel'; +import { IdentityProviderButton } from './components/IdentityProviderButton'; +import { IdentityProviderCallback } from './components/IdentityProviderCallback'; +import { getIdentityProviderOptions } from './openid/openid' +import { Unauthenticated } from './components/Unauthenticated'; +import { Authenticated } from './components/Authenticated'; +import { SignOutButton } from './components/SignOutButton'; +import { SessionLabel } from './components/SessionLabel'; -type AppProps = { +export interface AppProps { basename: Optional } -export default class App extends Component { - render() { - const history = createBrowserHistory({ basename: this.props.basename }); - const store = configureStore(history); - return ( - - - - - - ); - } +export const AuthenticationContext = React.createContext({ + session: Cookies.get("session"), + setSession: (session: Optional) => {} +}); + +export const App : React.FC = (props: AppProps) => { + const [session, setSession] = useState(Cookies.get("session")); + const value = { session, setSession }; + const identityProviders = getIdentityProviderOptions(); + + useEffect(() => { + if (session !== undefined) { + Cookies.set("session", session, {expires: 7, secure: true}); + } + else { + Cookies.remove("session"); + } + }); + + return ( + + + + {identityProviders.map(identityProvider => }/>)} + + + +
+ {identityProviders.map(identityProvider => )} +
+
+ + + + +
+
+ ); } diff --git a/src/Types.ts b/src/Types.ts index 128565c..ce00be9 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -4,6 +4,8 @@ // // @jsxImportSource @emotion/react -export type Nullable = T | null -export type Optional = T | undefined -export type OptionalNullable = T | null | undefined +export type Nullable = T | null; + +export type Optional = T | undefined; + +export type OptionalNullable = T | null | undefined; diff --git a/src/components/Authenticated.tsx b/src/components/Authenticated.tsx new file mode 100644 index 0000000..d490a98 --- /dev/null +++ b/src/components/Authenticated.tsx @@ -0,0 +1,20 @@ +// Copyright (c) Hubert Bukowski. All rights reserved. +// Licensed under the MIT License. +// See LICENSE file in the project root for full license information. +// +// @jsxImportSource @emotion/react + +import React from "react"; +import { useContext } from "react"; +import { AuthenticationContext } from "../App"; + +export interface AuthenticatedProps { +} + +export const Authenticated : React.FC> = (props: React.PropsWithChildren) => { + const isAuthenticated = useContext(AuthenticationContext); + + return isAuthenticated.session !== undefined + ? <>{props.children} + : null; +} diff --git a/src/components/IdentityProviderButton.tsx b/src/components/IdentityProviderButton.tsx new file mode 100644 index 0000000..0c66cb8 --- /dev/null +++ b/src/components/IdentityProviderButton.tsx @@ -0,0 +1,24 @@ +// Copyright (c) Hubert Bukowski. All rights reserved. +// Licensed under the MIT License. +// See LICENSE file in the project root for full license information. +// +// @jsxImportSource @emotion/react + +import React from "react"; +import { IdentityProviderOptions, generateNonce, getAuthorizationUrl } from "../openid/openid"; + +export interface IdentityProviderButtonProps { + options: IdentityProviderOptions, +} + +export const IdentityProviderButton : React.FC = (props: IdentityProviderButtonProps) => { + const nonce = generateNonce(); + const url = getAuthorizationUrl(props.options, nonce); + + function storeNonceAndRedirectToProvider() { + localStorage.setItem('nonce', nonce); + window.location.assign(url.toString()); + } + + return +} diff --git a/src/components/IdentityProviderCallback.tsx b/src/components/IdentityProviderCallback.tsx new file mode 100644 index 0000000..9f5f52f --- /dev/null +++ b/src/components/IdentityProviderCallback.tsx @@ -0,0 +1,43 @@ +// Copyright (c) Hubert Bukowski. All rights reserved. +// Licensed under the MIT License. +// See LICENSE file in the project root for full license information. +// +// @jsxImportSource @emotion/react + +import React, { useContext } from "react"; +import { useEffect } from 'react'; +import { useNavigate } from "react-router-dom"; +import { IdentityProviderOptions, verifyIdToken } from "../openid/openid"; +import { AuthenticationContext } from "../App"; + +export interface IdentityProviderCallbackProps { + options: IdentityProviderOptions +} + +export const IdentityProviderCallback : React.FC = (props: IdentityProviderCallbackProps) => { + const isAuthenticated = useContext(AuthenticationContext); + const navigate = useNavigate(); + const nonce = localStorage.getItem("nonce"); + + useEffect(() => { + async function verify() { + try { + if (nonce === null) { + throw new Error("Could not find authentication request nonce."); + } + + const idToken = await verifyIdToken(props.options, nonce); + if (idToken !== undefined) { + isAuthenticated.setSession(idToken); + } + } + finally { + navigate("/", {replace: true}); + } + } + + verify(); + }, [nonce, props.options, isAuthenticated, navigate]); + + return null; +} diff --git a/src/components/SessionLabel.tsx b/src/components/SessionLabel.tsx new file mode 100644 index 0000000..6dae366 --- /dev/null +++ b/src/components/SessionLabel.tsx @@ -0,0 +1,14 @@ +// Copyright (c) Hubert Bukowski. All rights reserved. +// Licensed under the MIT License. +// See LICENSE file in the project root for full license information. +// +// @jsxImportSource @emotion/react + +import React, { useContext } from "react"; +import { AuthenticationContext } from "../App"; + +export const SessionLabel : React.FC = () => { + const isAuthenticated = useContext(AuthenticationContext); + + return

Session (you can copy this value to https://jwt.io to check what data has been gathered from your social login): {isAuthenticated.session !== undefined ? isAuthenticated.session : ""}

+} diff --git a/src/components/SignOutButton.tsx b/src/components/SignOutButton.tsx new file mode 100644 index 0000000..fcaa830 --- /dev/null +++ b/src/components/SignOutButton.tsx @@ -0,0 +1,22 @@ +// Copyright (c) Hubert Bukowski. All rights reserved. +// Licensed under the MIT License. +// See LICENSE file in the project root for full license information. +// +// @jsxImportSource @emotion/react + +import React, { useContext } from "react"; +import { AuthenticationContext } from "../App"; + +export interface SignOutButtonProps { + text: string, +} + +export const SignOutButton : React.FC = (props: SignOutButtonProps) => { + const isAuthenticated = useContext(AuthenticationContext); + + function cleanSession() { + isAuthenticated.setSession(undefined); + } + + return +} diff --git a/src/components/Unauthenticated.tsx b/src/components/Unauthenticated.tsx new file mode 100644 index 0000000..0a20714 --- /dev/null +++ b/src/components/Unauthenticated.tsx @@ -0,0 +1,20 @@ +// Copyright (c) Hubert Bukowski. All rights reserved. +// Licensed under the MIT License. +// See LICENSE file in the project root for full license information. +// +// @jsxImportSource @emotion/react + +import React from "react"; +import { useContext } from "react"; +import { AuthenticationContext } from "../App"; + +export interface UnauthenticatedProps { +} + +export const Unauthenticated : React.FC> = (props: React.PropsWithChildren) => { + const isAuthenticated = useContext(AuthenticationContext); + + return isAuthenticated.session !== undefined + ? null + : <>{props.children}; +} diff --git a/src/index.tsx b/src/index.tsx index 937fc5a..5adb4c5 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -6,7 +6,7 @@ import ReactDOM from 'react-dom'; import { Optional, Nullable } from './Types'; -import App from './App'; +import { App } from './App'; const baseElement: HTMLCollectionOf = document.getElementsByTagName('base'); const baseUrl: Optional = (baseElement.length > 0) diff --git a/src/openid/Facebook.svg b/src/openid/Facebook.svg new file mode 100644 index 0000000..6827656 --- /dev/null +++ b/src/openid/Facebook.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/openid/Google.svg b/src/openid/Google.svg new file mode 100644 index 0000000..39be348 --- /dev/null +++ b/src/openid/Google.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/openid/openid.config.json b/src/openid/openid.config.json new file mode 100644 index 0000000..5c66408 --- /dev/null +++ b/src/openid/openid.config.json @@ -0,0 +1,8 @@ +{ + "baseUrl": "https://login.dgtiles.com/dgtiles.com/", + "clientId": "886eac23-9993-4293-8043-35c5e5610968", + "issuerUrl": "https://login.dgtiles.com/d2030318-9db4-4d47-97c2-1df2ec7db6ba/v2.0/", + "identityProviders": [ + "Google", "Facebook" + ] +} diff --git a/src/openid/openid.ts b/src/openid/openid.ts new file mode 100644 index 0000000..8817d20 --- /dev/null +++ b/src/openid/openid.ts @@ -0,0 +1,103 @@ +// Copyright (c) Hubert Bukowski. All rights reserved. +// Licensed under the MIT License. +// See LICENSE file in the project root for full license information. +// +// @jsxImportSource @emotion/react + +import { base64url, createRemoteJWKSet, jwtVerify } from "jose"; +import { Optional } from "../Types"; +import * as openidConfiguration from './openid.config.json'; + +export interface IdentityProviderOptions { + baseUrl: URL, + clientId: string, + identityProviderName: string, + issuerUrl: URL, +} + +export function getIdentityProviderOptions(): IdentityProviderOptions[] { + return openidConfiguration.identityProviders.map(provider => ({ + baseUrl: new URL(openidConfiguration.baseUrl), + clientId: openidConfiguration.clientId, + identityProviderName: provider, + issuerUrl: new URL(openidConfiguration.issuerUrl), + })); +} + +export function generateNonce(): string { + var random : Uint8Array = new Uint8Array(16); + window.crypto.getRandomValues(random); + return base64url.encode(random); +} + +export function getAuthorizationUrl(options: IdentityProviderOptions, nonce: string): URL { + const endpointUrl = joinUrl(options.baseUrl, ["B2C_1A_" + options.identityProviderName.toUpperCase(), "oauth2/v2.0/authorize"]); + const currentLocationWithoutHash = window.location.protocol + + '//' + window.location.hostname + + (window.location.port ? (':' + window.location.port) : '') + + window.location.pathname + + (window.location.search ? window.location.search : '') + + const queryParams = { + "client_id": options.clientId, + "nonce": nonce, + "redirect_uri": joinUrl(new URL(currentLocationWithoutHash), ["signed-in", options.identityProviderName]).toString(), + "scope": "openid", + "response_mode": "fragment", + "response_type": "id_token", + "prompt": "login", + }; + + const query = Object.entries(queryParams).map(([key, value]) => encodeURIComponent(key) + "=" + encodeURIComponent(value)).join("&"); + endpointUrl.search = query; + return endpointUrl; +} + +export async function verifyIdToken(options: IdentityProviderOptions, nonce: string): Promise> { + const idToken = extractParam("id_token"); + + if (idToken !== undefined) { + const openidConfigurationUrl = joinUrl(options.baseUrl, ["B2C_1A_" + options.identityProviderName.toUpperCase(), "v2.0/.well-known/openid-configuration"]).toString(); + const response = await fetch(openidConfigurationUrl); + const { jwks_uri } = await response.json(); + const jwksSet = createRemoteJWKSet(new URL(jwks_uri)); + const { payload } = await jwtVerify(idToken, jwksSet, { + issuer: options.issuerUrl.toString(), + audience: options.clientId, + }); + + const tokenNonce = payload["nonce"]; + if (tokenNonce !== nonce) { + throw new Error("Expected nonce value has not been found in the URL fragment."); + } + } + else { + let error = extractParam("error_description"); + if (!!!error) { + error = extractParam("error"); + } + + if (error != null) { + throw new Error(error); + } + } + + return idToken; +} + +function extractParam(paramName : string): Optional { + const pairs = window.location.hash.substring(1).split('&'); + for (let i = 0; i < pairs.length; i++) { + const pair = pairs[i].split('='); + const key = decodeURIComponent(pair[0].replace(/\+/g, '%20')); + if (key === paramName) { + return decodeURIComponent(pair[1].replace(/\+/g, '%20')); + } + } + + return undefined; +} + +function joinUrl(baseUrl: URL, nodes: string[]): URL { + return nodes.reduce((previous, current, index) => new URL(current + ((index < nodes.length - 1) ? "/" : ""), previous), baseUrl); +} diff --git a/src/store/configureStore.ts b/src/store/configureStore.ts deleted file mode 100644 index 2c47b88..0000000 --- a/src/store/configureStore.ts +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Hubert Bukowski. All rights reserved. -// Licensed under the MIT License. -// See LICENSE file in the project root for full license information. -// -// @jsxImportSource @emotion/react - -import { applyMiddleware, combineReducers, compose, createStore } from 'redux'; -import createSagaMiddleware from 'redux-saga'; -import { connectRouter, routerMiddleware } from 'connected-react-router'; -import { History } from 'history'; -import { ApplicationState, reducers } from './'; - -/** - * Configures application store. - * - * @param history - History object used to configure router. - * @param initialState - Initial application state. - * @returns - Redux store creator function. - */ -export default function configureStore(history: History, initialState?: ApplicationState) { - const middleware = [ - createSagaMiddleware(), - routerMiddleware(history) - ]; - - const rootReducer = combineReducers({ - ...reducers, - router: connectRouter(history) - }); - - const enhancers = []; - const windowIfDefined = typeof window === 'undefined' ? null : window as any; - if (windowIfDefined && windowIfDefined.__REDUX_DEVTOOLS_EXTENSION__) { - enhancers.push(windowIfDefined.__REDUX_DEVTOOLS_EXTENSION__()); - } - - return createStore( - rootReducer, - initialState, - compose(applyMiddleware(...middleware), ...enhancers) - ); -} diff --git a/src/store/index.ts b/src/store/index.ts deleted file mode 100644 index 156f889..0000000 --- a/src/store/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Hubert Bukowski. All rights reserved. -// Licensed under the MIT License. -// See LICENSE file in the project root for full license information. -// -// @jsxImportSource @emotion/react - -// The top-level state object -export interface ApplicationState { -} - -// Whenever an action is dispatched, Redux will update each top-level application state property using -// the reducer with the matching name. It's important that the names match exactly, and that the reducer -// acts on the corresponding ApplicationState property type. -export const reducers = { -}; - -// This type can be used as a hint on action creators so that its 'dispatch' and 'getState' params are -// correctly typed to match your store. -export interface AppThunkAction { - (dispatch: (action: TAction) => void, getState: () => ApplicationState): void; -} diff --git a/staticwebapp.config.json b/staticwebapp.config.json index 4a1bb1f..85eb5bd 100644 --- a/staticwebapp.config.json +++ b/staticwebapp.config.json @@ -1,5 +1,5 @@ { - "navigationFallback": { - "rewrite": "/index.html" - } -} \ No newline at end of file + "navigationFallback": { + "rewrite": "/index.html" + } +}