From f3d07c0d5e9d3180e620e4271e464d75783bdfc4 Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Sun, 9 Jun 2024 00:05:12 +0300 Subject: [PATCH 1/7] Add OIDC packages --- package.json | 2 ++ yarn.lock | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/package.json b/package.json index 381d05a..5a0895d 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,13 @@ "i18next-http-backend": "^2.5.2", "js-pkce": "^1.4.0", "js-yaml": "^4.1.0", + "oidc-client-ts": "^3.0.1", "prop-types": "^15.8.1", "react": "^18.3.1", "react-bootstrap": "^2.10.2", "react-dom": "^18.3.1", "react-i18next": "^14.1.2", + "react-oidc-context": "^3.1.0", "react-redux": "^9.1.2", "react-router-dom": "^6.23.1", "sass": "~1.77.4", diff --git a/yarn.lock b/yarn.lock index d488807..891198c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3322,6 +3322,11 @@ jsonfile@^6.0.1: object.assign "^4.1.4" object.values "^1.1.6" +jwt-decode@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-4.0.0.tgz#2270352425fd413785b2faf11f6e755c5151bd4b" + integrity sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA== + keyv@^4.5.3: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" @@ -3528,6 +3533,13 @@ object.values@^1.1.6, object.values@^1.1.7, object.values@^1.2.0: define-properties "^1.2.1" es-object-atoms "^1.0.0" +oidc-client-ts@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/oidc-client-ts/-/oidc-client-ts-3.0.1.tgz#be264fb87c89f74f73863646431c32cd06f5ceb7" + integrity sha512-xX8unZNtmtw3sOz4FPSqDhkLFnxCDsdo2qhFEH2opgWnF/iXMFoYdBQzkwCxAZVgt3FT3DnuBY3k80EZHT0RYg== + dependencies: + jwt-decode "^4.0.0" + once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -3703,6 +3715,11 @@ react-lifecycles-compat@^3.0.4: resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== +react-oidc-context@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/react-oidc-context/-/react-oidc-context-3.1.0.tgz#1047ee859b12793132854d583eaf0160b8a05d4f" + integrity sha512-ceQztvDfdl28mbr0So31XF/tCJamyF1+nm4AQNIE/nub+Xs9PLtDqLy/+75Yx1ahI0/n3nsq0R2qcP0R2Laa3Q== + react-redux@^9.1.2: version "9.1.2" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-9.1.2.tgz#deba38c64c3403e9abd0c3fbeab69ffd9d8a7e4b" From 74b3e452a8ecd2b024dc99cbe7bc77f6525f9546 Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Sun, 9 Jun 2024 00:31:34 +0300 Subject: [PATCH 2/7] Rename environment variables --- .env | 8 ++++---- .env.development | 8 ++++---- .env.production | 8 ++++---- .github/workflows/deploy.yml | 2 +- src/features/apiSlice.js | 6 +++--- src/layout/NavBar.jsx | 2 +- src/oauth.js | 4 ++-- vite.config.js | 8 ++++---- 8 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.env b/.env index ae2999f..1b4611c 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ -BACKEND_URL= -DEVICE_BACKEND_URL= -MQTT_BACKEND_URL= -OAUTH_CLIENT_ID= +OIDC_AUTHORITY_URL= +OIDC_CLIENT_ID= +PORTIER_URL= +MQTT_PROXY_URL= diff --git a/.env.development b/.env.development index b95b071..807dda3 100644 --- a/.env.development +++ b/.env.development @@ -1,4 +1,4 @@ -BACKEND_URL=http://localhost:3000/ -DEVICE_BACKEND_URL=http://localhost:4000/ -MQTT_BACKEND_URL=https://mqtt.initlab.org/ -OAUTH_CLIENT_ID=hCOEcK3ntyBym-uLQkogX6w8457kicVlZbY0PQZJusw +OIDC_AUTHORITY_URL=http://localhost:3000/ +OIDC_CLIENT_ID=hCOEcK3ntyBym-uLQkogX6w8457kicVlZbY0PQZJusw +PORTIER_URL=http://localhost:4000/ +MQTT_PROXY_URL=https://mqtt.initlab.org/ diff --git a/.env.production b/.env.production index b4f0a05..cd52384 100644 --- a/.env.production +++ b/.env.production @@ -1,4 +1,4 @@ -BACKEND_URL=https://fauna.initlab.org/ -DEVICE_BACKEND_URL=https://portier.initlab.org/ -MQTT_BACKEND_URL=https://mqtt.initlab.org/ -OAUTH_CLIENT_ID=YueB6ct6SKPN8Ar72G0LC1QFxW9meUDQIOHdAu5mfCE +OIDC_AUTHORITY_URL=https://fauna.initlab.org/ +OIDC_CLIENT_ID=YueB6ct6SKPN8Ar72G0LC1QFxW9meUDQIOHdAu5mfCE +PORTIER_URL=https://portier.initlab.org/ +MQTT_PROXY_URL=https://mqtt.initlab.org/ diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index acae0a3..cabab4c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -36,7 +36,7 @@ jobs: with: cmd: build env: - OAUTH_CLIENT_ID: ERBrlPWjf98L_nI8Moxr7Aqjy4no1eRU-zSOROc2RbU + OIDC_CLIENT_ID: ERBrlPWjf98L_nI8Moxr7Aqjy4no1eRU-zSOROc2RbU - name: Upload production-ready build files uses: actions/upload-artifact@v3 diff --git a/src/features/apiSlice.js b/src/features/apiSlice.js index b2db772..34f70d4 100644 --- a/src/features/apiSlice.js +++ b/src/features/apiSlice.js @@ -2,9 +2,9 @@ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; import { getAccessToken, isAccessTokenExpired } from '../hooks/useAuthStorage.js'; import { refreshTokenIfNeeded } from '../oauth.js'; -const apiBaseUrl = import.meta.env.BACKEND_URL + 'api/'; -const deviceApiBaseUrl = import.meta.env.DEVICE_BACKEND_URL + 'api/'; -const mqttApiBaseUrl = import.meta.env.MQTT_BACKEND_URL; +const apiBaseUrl = import.meta.env.OIDC_AUTHORITY_URL + 'api/'; +const deviceApiBaseUrl = import.meta.env.PORTIER_URL + 'api/'; +const mqttApiBaseUrl = import.meta.env.MQTT_PROXY_URL; const anonymousBaseQuery = fetchBaseQuery({ baseUrl: apiBaseUrl, diff --git a/src/layout/NavBar.jsx b/src/layout/NavBar.jsx index f7b4c9b..17e9564 100644 --- a/src/layout/NavBar.jsx +++ b/src/layout/NavBar.jsx @@ -13,7 +13,7 @@ import RequireRole from '../widgets/RequireRole.jsx'; const NavBar = () => { const {t} = useTranslation(); - const backendUrl = import.meta.env.BACKEND_URL; + const backendUrl = import.meta.env.OIDC_AUTHORITY_URL; const { hasAccessToken, user, diff --git a/src/oauth.js b/src/oauth.js index 340e1e4..ca7ecad 100644 --- a/src/oauth.js +++ b/src/oauth.js @@ -1,8 +1,8 @@ import PKCE from 'js-pkce'; import { clearTokens, getAccessToken, getRefreshToken, updateTokens } from './hooks/useAuthStorage.js'; -const clientId = import.meta.env.OAUTH_CLIENT_ID; -const baseUrl = import.meta.env.BACKEND_URL + 'oauth/'; +const clientId = import.meta.env.OIDC_CLIENT_ID; +const baseUrl = import.meta.env.OIDC_AUTHORITY_URL + 'oauth/'; const urls = { authorize: baseUrl + 'authorize', diff --git a/vite.config.js b/vite.config.js index 78c979f..58fd721 100644 --- a/vite.config.js +++ b/vite.config.js @@ -8,10 +8,10 @@ export default defineConfig({ sourcemap: true, }, envPrefix: [ - 'BACKEND_URL', - 'DEVICE_BACKEND_URL', - 'MQTT_BACKEND_URL', - 'OAUTH_CLIENT_ID', + 'OIDC_AUTHORITY_URL', + 'OIDC_CLIENT_ID', + 'PORTIER_URL', + 'MQTT_PROXY_URL', ], plugins: [ react(), From afd4b3529c972c8c9a2b27211f69153f80baf1ea Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Sun, 9 Jun 2024 00:36:31 +0300 Subject: [PATCH 3/7] Add config and provider for OIDC --- src/config.js | 6 ++++++ src/main.jsx | 6 +++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/config.js b/src/config.js index e95b6d0..3cf434c 100644 --- a/src/config.js +++ b/src/config.js @@ -1,3 +1,9 @@ +export const oidc = { + authority: import.meta.env.OIDC_AUTHORITY_URL, + client_id: import.meta.env.OIDC_CLIENT_ID, + redirect_uri: window.location.protocol + '//' + window.location.host + import.meta.env.BASE_URL + 'oauth-callback', +}; + export const sensors = { 'sensors/big-room/temperature': { type: 'Temperature', diff --git a/src/main.jsx b/src/main.jsx index ffae317..24f6520 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -3,6 +3,8 @@ import ReactDOM from 'react-dom/client'; import { Provider } from 'react-redux'; import { store } from './app/store'; import { BrowserRouter } from 'react-router-dom'; +import { oidc as oidcConfig } from './config'; +import { AuthProvider } from 'react-oidc-context'; import './i18n'; import App from './App'; @@ -14,7 +16,9 @@ ReactDOM.createRoot(document.getElementById('root')).render( - + + + , From c83b1ec2ef8c46a9824d9629970fcaa73f9dc7a9 Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Sun, 9 Jun 2024 00:44:07 +0300 Subject: [PATCH 4/7] Change dev authority URL --- .env.development | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.development b/.env.development index 807dda3..f356766 100644 --- a/.env.development +++ b/.env.development @@ -1,4 +1,4 @@ -OIDC_AUTHORITY_URL=http://localhost:3000/ +OIDC_AUTHORITY_URL=https://auth-staging.initlab.org/ OIDC_CLIENT_ID=hCOEcK3ntyBym-uLQkogX6w8457kicVlZbY0PQZJusw PORTIER_URL=http://localhost:4000/ MQTT_PROXY_URL=https://mqtt.initlab.org/ From 421f2423b6940badfa81682a69a463defc782e54 Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Sun, 9 Jun 2024 00:43:56 +0300 Subject: [PATCH 5/7] Migrate to OIDC --- .env.development | 2 +- package.json | 1 - src/App.jsx | 27 ++-- src/app/store.js | 16 +-- src/config.js | 11 +- src/features/apiSlice.js | 136 ++++++------------ src/hooks/useAuthStorage.js | 95 ------------ src/hooks/useCurrentUser.js | 18 --- src/hooks/useRememberPage.js | 54 ------- src/layout/NavBar.jsx | 54 +++---- src/oauth.js | 59 -------- src/pages/ActionLog.jsx | 5 +- src/pages/Devices.jsx | 10 +- src/pages/Doors.jsx | 3 +- src/pages/Lights.jsx | 3 +- src/pages/Login.jsx | 29 ---- src/pages/Logout.jsx | 39 ----- src/pages/OauthCallback.jsx | 41 ------ .../DeviceActionButton/DeviceActionButton.jsx | 7 +- .../PresentUsersWrapper.jsx | 38 +++-- src/widgets/RedirectToLogin.jsx | 11 -- src/widgets/RequireRole.jsx | 14 +- src/widgets/Route/RequireLoggedIn.jsx | 33 ----- yarn.lock | 12 -- 24 files changed, 140 insertions(+), 578 deletions(-) delete mode 100644 src/hooks/useAuthStorage.js delete mode 100644 src/hooks/useCurrentUser.js delete mode 100644 src/hooks/useRememberPage.js delete mode 100644 src/oauth.js delete mode 100644 src/pages/Login.jsx delete mode 100644 src/pages/Logout.jsx delete mode 100644 src/pages/OauthCallback.jsx delete mode 100644 src/widgets/RedirectToLogin.jsx delete mode 100644 src/widgets/Route/RequireLoggedIn.jsx diff --git a/.env.development b/.env.development index f356766..a99a94a 100644 --- a/.env.development +++ b/.env.development @@ -1,4 +1,4 @@ OIDC_AUTHORITY_URL=https://auth-staging.initlab.org/ -OIDC_CLIENT_ID=hCOEcK3ntyBym-uLQkogX6w8457kicVlZbY0PQZJusw +OIDC_CLIENT_ID=270767587204382721@access_control PORTIER_URL=http://localhost:4000/ MQTT_PROXY_URL=https://mqtt.initlab.org/ diff --git a/package.json b/package.json index 5a0895d..6e054b3 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "date-fns": "^3.6.0", "i18next": "^23.11.5", "i18next-http-backend": "^2.5.2", - "js-pkce": "^1.4.0", "js-yaml": "^4.1.0", "oidc-client-ts": "^3.0.1", "prop-types": "^15.8.1", diff --git a/src/App.jsx b/src/App.jsx index 44590c4..2fbd008 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -4,15 +4,13 @@ import Footer from './layout/Footer'; import { Route, Routes } from 'react-router-dom'; import Sensors from './pages/Sensors'; import Doors from './pages/Doors.jsx'; -import OauthCallback from './pages/OauthCallback.jsx'; -import Logout from './pages/Logout.jsx'; import { Container } from 'react-bootstrap'; -import Login from './pages/Login.jsx'; -import { RequireLoggedIn } from './widgets/Route/RequireLoggedIn.jsx'; import ActionLog from './pages/ActionLog.jsx'; import Lights from './pages/Lights.jsx'; import { useVariant } from './hooks/useVariant.js'; import { useEffect } from 'react'; +import { useAuth } from 'react-oidc-context'; +import i18n from './i18n.js'; function App() { const variant = useVariant(); @@ -23,23 +21,26 @@ function App() { } }, [variant]); + const auth = useAuth(); + // TODO + console.log(auth); + useEffect(function() { + if (auth.isAuthenticated) { + // TODO + i18n.changeLanguage(auth.user.profile?.preferredLanguage || 'bg').then(() => {}); + } + }, [auth]); + return (<>
} /> - - - } /> - - - } /> + } /> + } /> } /> } /> - } /> - } /> - } />
diff --git a/src/app/store.js b/src/app/store.js index 216b652..f322395 100644 --- a/src/app/store.js +++ b/src/app/store.js @@ -1,22 +1,16 @@ import { configureStore } from '@reduxjs/toolkit'; import { setupListeners } from '@reduxjs/toolkit/query'; -import { - anonymousApiSlice, - anonymousMqttApiSlice, - authenticatedApiSlice, - authenticatedDeviceApiSlice -} from '../features/apiSlice'; +import { anonymousMqttApiSlice, anonymousPresenceApiSlice, authenticatedPortierApiSlice } from '../features/apiSlice'; export const store = configureStore({ reducer: { - [anonymousApiSlice.reducerPath]: anonymousApiSlice.reducer, + [authenticatedPortierApiSlice.reducerPath]: authenticatedPortierApiSlice.reducer, + [anonymousPresenceApiSlice.reducerPath]: anonymousPresenceApiSlice.reducer, [anonymousMqttApiSlice.reducerPath]: anonymousMqttApiSlice.reducer, - [authenticatedApiSlice.reducerPath]: authenticatedApiSlice.reducer, - [authenticatedDeviceApiSlice.reducerPath]: authenticatedDeviceApiSlice.reducer, }, middleware: getDefaultMiddleware => - getDefaultMiddleware().concat(anonymousApiSlice.middleware).concat(anonymousMqttApiSlice.middleware) - .concat(authenticatedApiSlice.middleware).concat(authenticatedDeviceApiSlice.middleware), + getDefaultMiddleware().concat(authenticatedPortierApiSlice.middleware) + .concat(anonymousPresenceApiSlice.middleware).concat(anonymousMqttApiSlice.middleware), }); setupListeners(store.dispatch); diff --git a/src/config.js b/src/config.js index 3cf434c..2286edf 100644 --- a/src/config.js +++ b/src/config.js @@ -1,7 +1,16 @@ +import { WebStorageStateStore } from 'oidc-client-ts'; + export const oidc = { authority: import.meta.env.OIDC_AUTHORITY_URL, client_id: import.meta.env.OIDC_CLIENT_ID, - redirect_uri: window.location.protocol + '//' + window.location.host + import.meta.env.BASE_URL + 'oauth-callback', + redirect_uri: window.location.protocol + '//' + window.location.host + import.meta.env.BASE_URL, + scope: 'openid profile offline_access', + onSigninCallback: () => { + window.history.replaceState({}, document.title, window.location.pathname); + }, + userStore: new WebStorageStateStore({ + store: window.localStorage, + }), }; export const sensors = { diff --git a/src/features/apiSlice.js b/src/features/apiSlice.js index 34f70d4..4cba6a1 100644 --- a/src/features/apiSlice.js +++ b/src/features/apiSlice.js @@ -1,26 +1,25 @@ +import { oidc as oidcConfig } from '../config.js'; +import { User } from 'oidc-client-ts'; import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; -import { getAccessToken, isAccessTokenExpired } from '../hooks/useAuthStorage.js'; -import { refreshTokenIfNeeded } from '../oauth.js'; -const apiBaseUrl = import.meta.env.OIDC_AUTHORITY_URL + 'api/'; -const deviceApiBaseUrl = import.meta.env.PORTIER_URL + 'api/'; -const mqttApiBaseUrl = import.meta.env.MQTT_PROXY_URL; +function getAccessToken() { + const oidcStorage = localStorage.getItem(`oidc.user:${oidcConfig.authority}:${oidcConfig.client_id}`) -const anonymousBaseQuery = fetchBaseQuery({ - baseUrl: apiBaseUrl, - prepareHeaders: headers => { - headers.set('accept', 'application/json'); + if (!oidcStorage) { + return null; + } - return headers; - }, -}); + const user = User.fromStorageString(oidcStorage); -const anonymousMqttBaseQuery = fetchBaseQuery({ - baseUrl: mqttApiBaseUrl, -}); + return user?.access_token; +} -const authenticatedBaseQuery = fetchBaseQuery({ - baseUrl: apiBaseUrl, +const portierBaseUrl = import.meta.env.PORTIER_URL + 'api/'; +const presenceBaseUrl = import.meta.env.OIDC_AUTHORITY_URL + 'api/'; +const mqttProxyBaseUrl = import.meta.env.MQTT_PROXY_URL; + +const authenticatedPortierBaseQuery = fetchBaseQuery({ + baseUrl: portierBaseUrl, prepareHeaders: headers => { headers.set('accept', 'application/json'); @@ -34,80 +33,27 @@ const authenticatedBaseQuery = fetchBaseQuery({ }, }); -const authenticatedDeviceBaseQuery = fetchBaseQuery({ - baseUrl: deviceApiBaseUrl, +const anonymousPresenceBaseQuery = fetchBaseQuery({ + baseUrl: presenceBaseUrl, prepareHeaders: headers => { headers.set('accept', 'application/json'); - const token = getAccessToken(); - - if (token) { - headers.set('authorization', 'Bearer ' + token); - } - return headers; }, }); -const authenticatedBaseQueryWithReauth = async (args, api, extraOptions) => { - if (isAccessTokenExpired()) { - await refreshTokenIfNeeded(); - } - - let result = await authenticatedBaseQuery(args, api, extraOptions); - - if (result?.error?.status === 401 && await refreshTokenIfNeeded()) { - return authenticatedBaseQuery(args, api, extraOptions); - } - - return result; -}; - -const authenticatedDeviceBaseQueryWithReauth = async (args, api, extraOptions) => { - if (isAccessTokenExpired()) { - await refreshTokenIfNeeded(); - } - - let result = await authenticatedDeviceBaseQuery(args, api, extraOptions); - - if (result?.error?.status === 401 && await refreshTokenIfNeeded()) { - return authenticatedDeviceBaseQuery(args, api, extraOptions); - } +const anonymousMqttProxyBaseQuery = fetchBaseQuery({ + baseUrl: mqttProxyBaseUrl, +}); - return result; -}; const query = builder => url => builder.query({ query: () => url, }); -export const anonymousApiSlice = createApi({ - reducerPath: 'anonymousApi', - baseQuery: anonymousBaseQuery, - endpoints: builder => ({ - getPresentUsers: query(builder)('users/present'), - }), -}); - -export const anonymousMqttApiSlice = createApi({ - reducerPath: 'anonymousMqttApi', - baseQuery: anonymousMqttBaseQuery, - endpoints: builder => ({ - getStatus: query(builder)('status'), - }), -}); - -export const authenticatedApiSlice = createApi({ - reducerPath: 'authenticatedApi', - baseQuery: authenticatedBaseQueryWithReauth, - endpoints: builder => ({ - getCurrentUser: query(builder)('current_user'), - }), -}); - -export const authenticatedDeviceApiSlice = createApi({ - reducerPath: 'authenticatedDeviceApi', - baseQuery: authenticatedDeviceBaseQueryWithReauth, +export const authenticatedPortierApiSlice = createApi({ + reducerPath: 'authenticatedPortierApi', + baseQuery: authenticatedPortierBaseQuery, endpoints: builder => ({ getDevices: query(builder)('devices'), deviceAction: builder.mutation({ @@ -122,20 +68,32 @@ export const authenticatedDeviceApiSlice = createApi({ }), }); -export const { - useGetPresentUsersQuery, -} = anonymousApiSlice; - -export const { - useGetStatusQuery, -} = anonymousMqttApiSlice; +export const anonymousPresenceApiSlice = createApi({ + reducerPath: 'anonymousApi', + baseQuery: anonymousPresenceBaseQuery, + endpoints: builder => ({ + getPresentUsers: query(builder)('users/present'), + }), +}); -export const { - useGetCurrentUserQuery, -} = authenticatedApiSlice; +export const anonymousMqttApiSlice = createApi({ + reducerPath: 'anonymousMqttApi', + baseQuery: anonymousMqttProxyBaseQuery, + endpoints: builder => ({ + getStatus: query(builder)('status'), + }), +}); export const { useGetDevicesQuery, useDeviceActionMutation, useGetActionLogQuery, -} = authenticatedDeviceApiSlice; +} = authenticatedPortierApiSlice; + +export const { + useGetPresentUsersQuery, +} = anonymousPresenceApiSlice; + +export const { + useGetStatusQuery, +} = anonymousMqttApiSlice; diff --git a/src/hooks/useAuthStorage.js b/src/hooks/useAuthStorage.js deleted file mode 100644 index ecf991f..0000000 --- a/src/hooks/useAuthStorage.js +++ /dev/null @@ -1,95 +0,0 @@ -import { scopes } from '../oauth.js'; -import { useLocalStorage } from '@uidotdev/usehooks'; - -const STORAGE_KEY = 'tokens'; - -const ACCESS_TOKEN_KEY = 'accessToken'; -const ACCESS_TOKEN_EXPIRE_KEY = 'accessTokenExpire'; -const REFRESH_TOKEN_KEY = 'refreshToken'; - -const defaultTokensValue = {}; - -function parseTokenResponse(tokenResponse) { - if (tokenResponse.hasOwnProperty('error') && tokenResponse.hasOwnProperty('error_description')) { - throw new Error(tokenResponse.error_description); - } - - if ( - !tokenResponse.hasOwnProperty('access_token') || - !tokenResponse.hasOwnProperty('created_at') || - !tokenResponse.hasOwnProperty('expires_in') || - !tokenResponse.hasOwnProperty('refresh_token') || - !tokenResponse.hasOwnProperty('scope') || - !tokenResponse.hasOwnProperty('token_type') || - tokenResponse.token_type !== 'Bearer' || - tokenResponse.scope !== scopes - ) { - throw new Error('Incomplete response, missing fields'); - } - - return { - [ACCESS_TOKEN_KEY]: tokenResponse.access_token, - [ACCESS_TOKEN_EXPIRE_KEY]: (tokenResponse.created_at + tokenResponse.expires_in) * 1_000, - [REFRESH_TOKEN_KEY]: tokenResponse.refresh_token, - }; -} - -export function useAuthStorage() { - const [ tokens, setTokens ] = useLocalStorage(STORAGE_KEY, defaultTokensValue); - - function updateTokens(tokenResponse) { - setTokens(parseTokenResponse(tokenResponse)); - } - - function clearTokens() { - setTokens(); - } - - return { - accessToken: tokens?.[ACCESS_TOKEN_KEY], - accessTokenExpire: tokens?.[ACCESS_TOKEN_EXPIRE_KEY], - refreshToken: tokens?.[REFRESH_TOKEN_KEY], - updateTokens, - clearTokens, - }; -} - -function getStorageItem(key) { - return JSON.parse(localStorage.getItem(STORAGE_KEY))?.[key]; -} - -function setStorageValue(value) { - const key = STORAGE_KEY; - const newValue = JSON.stringify(value || defaultTokensValue); - localStorage.setItem(key, newValue); - window.dispatchEvent(new StorageEvent('storage', { - key, - newValue, - })); -} - -export function getAccessToken() { - return getStorageItem(ACCESS_TOKEN_KEY); -} - -function getAccessTokenExpire() { - return getStorageItem(ACCESS_TOKEN_EXPIRE_KEY); -} - -export function isAccessTokenExpired() { - const accessTokenExpire = getAccessTokenExpire(); - - return typeof accessTokenExpire !== 'number' || accessTokenExpire < Date.now(); -} - -export function getRefreshToken() { - return getStorageItem(REFRESH_TOKEN_KEY); -} - -export function updateTokens(tokenResponse) { - setStorageValue(parseTokenResponse(tokenResponse)); -} - -export function clearTokens() { - setStorageValue(); -} diff --git a/src/hooks/useCurrentUser.js b/src/hooks/useCurrentUser.js deleted file mode 100644 index 77b9075..0000000 --- a/src/hooks/useCurrentUser.js +++ /dev/null @@ -1,18 +0,0 @@ -import { useAuthStorage } from './useAuthStorage.js'; -import { useGetCurrentUserQuery } from '../features/apiSlice.js'; - -export function useCurrentUser() { - const { accessToken } = useAuthStorage(); - const hasAccessToken = !!accessToken; - const queryResult = useGetCurrentUserQuery(undefined, { - skip: !hasAccessToken, - }); - const user = queryResult.isSuccess ? queryResult.data : {}; - - return { - hasAccessToken, - user, - isLoggedIn: user.hasOwnProperty('id'), - ...queryResult, - }; -} diff --git a/src/hooks/useRememberPage.js b/src/hooks/useRememberPage.js deleted file mode 100644 index a5ae833..0000000 --- a/src/hooks/useRememberPage.js +++ /dev/null @@ -1,54 +0,0 @@ -import { useLocation, useNavigate } from 'react-router-dom'; - -const STATE_KEY = 'from'; -const STORAGE_KEY = 'redirectAfterLogin'; - -export function useRememberPage() { - const location = useLocation(); - const navigate = useNavigate(); - - function getPreviousPath() { - return location.state?.[STATE_KEY]?.pathname; - } - - function storePreviousPath() { - const previousPath = getPreviousPath(); - - if (previousPath) { - localStorage.setItem(STORAGE_KEY, JSON.stringify({ - path: previousPath, - expiresAt: Date.now() + 30 * 60 * 1_000, - })); - } - else { - localStorage.removeItem(STORAGE_KEY); - } - } - - function navigateToPreviousPath(returnPath = '/') { - const redirectInfo = localStorage.getItem(STORAGE_KEY); - - if (redirectInfo) { - const { - path, - expiresAt, - } = JSON.parse(redirectInfo); - - if (expiresAt > Date.now()) { - returnPath = path; - } - - localStorage.removeItem(STORAGE_KEY); - } - - navigate(returnPath, { - replace: true, - }); - } - - return { - getPreviousPath, - storePreviousPath, - navigateToPreviousPath, - }; -} diff --git a/src/layout/NavBar.jsx b/src/layout/NavBar.jsx index 17e9564..47c0069 100644 --- a/src/layout/NavBar.jsx +++ b/src/layout/NavBar.jsx @@ -3,34 +3,21 @@ import './NavBar.css'; import initLabLogo from '../assets/initlab/logo.svg'; import colibriLogo from '../assets/colibri/logo.png'; import { useTranslation } from 'react-i18next'; -import { NavLink, useLocation } from 'react-router-dom'; +import { NavLink } from 'react-router-dom'; import DoorClosedIcon from '../widgets/icons/DoorClosedIcon.jsx'; -import { useEffect } from 'react'; -import i18n from '../i18n.js'; import { useVariant } from '../hooks/useVariant.js'; -import { useCurrentUser } from '../hooks/useCurrentUser.js'; import RequireRole from '../widgets/RequireRole.jsx'; +import LoadingIcon from '../widgets/icons/LoadingIcon.jsx'; +import { useAuth } from 'react-oidc-context'; const NavBar = () => { const {t} = useTranslation(); - const backendUrl = import.meta.env.OIDC_AUTHORITY_URL; - const { - hasAccessToken, - user, - isSuccess, - } = useCurrentUser(); + const auth = useAuth(); + const oidcAuthorityUrl = import.meta.env.OIDC_AUTHORITY_URL; const variant = useVariant(); const isInitLab = variant === 'initlab'; const isColibri = variant === 'colibri'; - useEffect(function() { - if (isSuccess) { - i18n.changeLanguage(user.locale).then(() => {}); - } - }, [user.locale, isSuccess]); - - const location = useLocation(); - return ( { - + {' '} {t('views.navigation.labbers')} - {hasAccessToken ? - {' '} - {t('views.navigation.account')} - } className="ms-0 ms-lg-auto"> - + {auth.isLoading ? + + : (auth.isAuthenticated ? + {' '} + {t('views.navigation.account')} + } className="ms-0 ms-lg-auto"> + {t('views.navigation.view_edit')} {isInitLab && <> - + {t('views.navigation.network_devices')} - - {t('views.navigation.oauth_application_management')} - - + {t('views.navigation.oauth_token_management')} } - + void auth.removeUser()}> {t('views.navigation.sign_out')} - : + : void auth.signinRedirect()} className="ms-0 ms-lg-auto"> {' '} {t('views.navigation.sign_in')} - } + )} diff --git a/src/oauth.js b/src/oauth.js deleted file mode 100644 index ca7ecad..0000000 --- a/src/oauth.js +++ /dev/null @@ -1,59 +0,0 @@ -import PKCE from 'js-pkce'; -import { clearTokens, getAccessToken, getRefreshToken, updateTokens } from './hooks/useAuthStorage.js'; - -const clientId = import.meta.env.OIDC_CLIENT_ID; -const baseUrl = import.meta.env.OIDC_AUTHORITY_URL + 'oauth/'; - -const urls = { - authorize: baseUrl + 'authorize', - token: baseUrl + 'token', - revoke: baseUrl + 'revoke', -}; - -export const scopes = ['account_data_read'].join(' '); - -const pkce = new PKCE({ - client_id: clientId, - redirect_uri: window.location.protocol + '//' + window.location.host + import.meta.env.BASE_URL + 'oauth-callback', - authorization_endpoint: urls.authorize, - token_endpoint: urls.token, - requested_scopes: scopes, -}); - -export const authorizeUrl = () => pkce.authorizeUrl(); -export const exchangeForAccessToken = async () => pkce.exchangeForAccessToken(window.location.href); - -export async function revokeToken(token) { - return fetch(urls.revoke, { - method: 'POST', - headers: { - 'Authorization': 'Bearer ' + getAccessToken(), - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - client_id: clientId, - token, - }), - }); -} - -export async function refreshTokenIfNeeded() { - const refreshToken = getRefreshToken(); - - if (!refreshToken) { - clearTokens(); - console.error('No refresh token found'); - return false; - } - - try { - updateTokens(await pkce.refreshAccessToken(refreshToken)); - } - catch (e) { - clearTokens(); - console.error(e); - return false; - } - - return true; -} diff --git a/src/pages/ActionLog.jsx b/src/pages/ActionLog.jsx index 383b1b6..908dfdb 100644 --- a/src/pages/ActionLog.jsx +++ b/src/pages/ActionLog.jsx @@ -8,6 +8,7 @@ import LoadingIcon from '../widgets/icons/LoadingIcon.jsx'; import ErrorMessage from '../widgets/ErrorMessage.jsx'; import ActionLogEntry from '../widgets/ActionLog/ActionLogEntry.jsx'; import { useNetworkState } from '@uidotdev/usehooks'; +import { withAuthenticationRequired } from 'react-oidc-context'; const ActionLog = () => { const { t } = useTranslation(); @@ -32,7 +33,7 @@ const ActionLog = () => { }); if (!hasAccess) { - return (); + return (); } return (<> @@ -64,4 +65,4 @@ const ActionLog = () => { ); }; -export default ActionLog; +export default withAuthenticationRequired(ActionLog); diff --git a/src/pages/Devices.jsx b/src/pages/Devices.jsx index 75ca312..60ffd00 100644 --- a/src/pages/Devices.jsx +++ b/src/pages/Devices.jsx @@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next'; import ErrorMessage from '../widgets/ErrorMessage.jsx'; import { useVariant } from '../hooks/useVariant.js'; import PropTypes from 'prop-types'; -import { useNetworkState } from '@uidotdev/usehooks'; +// import { useNetworkState } from '@uidotdev/usehooks'; const Devices = ({ deviceType, @@ -15,9 +15,9 @@ const Devices = ({ }) => { const { t } = useTranslation(); - const { - online, - } = useNetworkState(); + // const { + // online, + // } = useNetworkState(); const { data: devices, @@ -26,7 +26,7 @@ const Devices = ({ isSuccess, isError, } = useGetDevicesQuery(undefined, { - pollingInterval: online === false ? 0 : 2_000, + // pollingInterval: online === false ? 0 : 2_000, }); const filteredDevices = useMemo(() => diff --git a/src/pages/Doors.jsx b/src/pages/Doors.jsx index 82195a7..639fd87 100644 --- a/src/pages/Doors.jsx +++ b/src/pages/Doors.jsx @@ -1,9 +1,10 @@ import React from 'react'; import { getDoorActions } from '../utils/device.js'; import Devices from './Devices.jsx'; +import { withAuthenticationRequired } from 'react-oidc-context'; const Doors = () => { return (); }; -export default Doors; +export default withAuthenticationRequired(Doors); diff --git a/src/pages/Lights.jsx b/src/pages/Lights.jsx index 072b7e3..4f604d2 100644 --- a/src/pages/Lights.jsx +++ b/src/pages/Lights.jsx @@ -1,9 +1,10 @@ import React from 'react'; import { getLightActions } from '../utils/device.js'; import Devices from './Devices.jsx'; +import { withAuthenticationRequired } from 'react-oidc-context'; const Lights = () => { return (); }; -export default Lights; +export default withAuthenticationRequired(Lights); diff --git a/src/pages/Login.jsx b/src/pages/Login.jsx deleted file mode 100644 index 7e6adb2..0000000 --- a/src/pages/Login.jsx +++ /dev/null @@ -1,29 +0,0 @@ -import { useEffect, useRef } from 'react'; -import { Col, Row } from 'react-bootstrap'; -import LoadingIcon from '../widgets/icons/LoadingIcon.jsx'; -import { authorizeUrl } from '../oauth.js'; -import { useRememberPage } from '../hooks/useRememberPage.js'; - -const Login = () => { - const flag = useRef(false); - const { storePreviousPath } = useRememberPage(); - - useEffect(() => { - if (flag.current) { - return; - } - - flag.current = true; - - storePreviousPath(); - window.location.replace(authorizeUrl()); - }, [storePreviousPath]); - - return ( - - - - ); -}; - -export default Login; diff --git a/src/pages/Logout.jsx b/src/pages/Logout.jsx deleted file mode 100644 index e26bf20..0000000 --- a/src/pages/Logout.jsx +++ /dev/null @@ -1,39 +0,0 @@ -import { useEffect, useRef } from 'react'; -import { Col, Row } from 'react-bootstrap'; -import LoadingIcon from '../widgets/icons/LoadingIcon.jsx'; -import { getAccessToken, getRefreshToken, useAuthStorage } from '../hooks/useAuthStorage.js'; -import { useRememberPage } from '../hooks/useRememberPage.js'; -import { useNavigate } from 'react-router-dom'; -import { revokeToken } from '../oauth.js'; - -const Logout = () => { - const flag = useRef(false); - const navigate = useNavigate(); - const { clearTokens } = useAuthStorage(); - const { getPreviousPath } = useRememberPage(); - - useEffect(() => { - if (flag.current) { - return; - } - - flag.current = true; - - (async () => { - await revokeToken(getRefreshToken()); - await revokeToken(getAccessToken()); - clearTokens(); - navigate(getPreviousPath() || '/', { - replace: true, - }); - })(); - }, [clearTokens, getPreviousPath, navigate]); - - return ( - - - - ); -}; - -export default Logout; diff --git a/src/pages/OauthCallback.jsx b/src/pages/OauthCallback.jsx deleted file mode 100644 index 4260458..0000000 --- a/src/pages/OauthCallback.jsx +++ /dev/null @@ -1,41 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; -import { exchangeForAccessToken } from '../oauth.js'; -import { useAuthStorage } from '../hooks/useAuthStorage.js'; -import { Col, Row } from 'react-bootstrap'; -import LoadingIcon from '../widgets/icons/LoadingIcon.jsx'; -import { useRememberPage } from '../hooks/useRememberPage.js'; - -const OauthCallback = () => { - const flag = useRef(false); - const [errorMessage, setErrorMessage] = useState(null); - const { updateTokens } = useAuthStorage(); - const { navigateToPreviousPath } = useRememberPage(); - - useEffect(() => { - if (flag.current) { - return; - } - - flag.current = true; - - (async () => { - const tokenResponse = await exchangeForAccessToken(); - - try { - updateTokens(tokenResponse); - navigateToPreviousPath(); - } - catch (e) { - setErrorMessage(e.message); - } - })(); - }, [navigateToPreviousPath, updateTokens]); - - return (errorMessage || - - - - ); -}; - -export default OauthCallback; diff --git a/src/widgets/DeviceActionButton/DeviceActionButton.jsx b/src/widgets/DeviceActionButton/DeviceActionButton.jsx index c17c14a..0d1caff 100644 --- a/src/widgets/DeviceActionButton/DeviceActionButton.jsx +++ b/src/widgets/DeviceActionButton/DeviceActionButton.jsx @@ -3,7 +3,6 @@ import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import './DeviceActionButton.scss'; import { useDeviceActionMutation } from '../../features/apiSlice.js'; -import RedirectToLogin from '../RedirectToLogin.jsx'; import { sleep } from '../../utils/time.js'; const types = { @@ -39,10 +38,7 @@ const DeviceActionButton = ({ }) => { const [ disabled, setDisabled ] = useState(false); - const [ execute, { - isError, - error, - } ] = useDeviceActionMutation(); + const [ execute ] = useDeviceActionMutation(); const {t} = useTranslation(); const type = types?.[action] || { @@ -71,7 +67,6 @@ const DeviceActionButton = ({
{label}
- {isError && [401, 403].includes(error.status) && } ); }; diff --git a/src/widgets/PresentUsersWrapper/PresentUsersWrapper.jsx b/src/widgets/PresentUsersWrapper/PresentUsersWrapper.jsx index d0f4ec0..75f1e96 100644 --- a/src/widgets/PresentUsersWrapper/PresentUsersWrapper.jsx +++ b/src/widgets/PresentUsersWrapper/PresentUsersWrapper.jsx @@ -1,30 +1,38 @@ import { Col, Row } from 'react-bootstrap'; import PresentUsers from '../PresentUsers/PresentUsers'; import { useTranslation } from 'react-i18next'; -import { useGetPresentUsersQuery } from '../../features/apiSlice.js'; +// import { useGetPresentUsersQuery } from '../../features/apiSlice.js'; import LoadingIcon from '../icons/LoadingIcon.jsx'; import { useMemo } from 'react'; import { format, formatISO } from 'date-fns'; import ErrorMessage from '../ErrorMessage.jsx'; -import { useNetworkState } from '@uidotdev/usehooks'; +// import { useNetworkState } from '@uidotdev/usehooks'; const PresentUsersWrapper = () => { const { t } = useTranslation(); - const { - online, - } = useNetworkState(); + // const { + // online, + // } = useNetworkState(); - const { - data: users, - error, - isLoading, - isSuccess, - isError, - fulfilledTimeStamp, - } = useGetPresentUsersQuery(undefined, { - pollingInterval: online === false ? 0 : 60_000, - }); + // TODO + // const { + // data: users, + // error, + // isLoading, + // isSuccess, + // isError, + // fulfilledTimeStamp, + // } = useGetPresentUsersQuery(undefined, { + // pollingInterval: online === false ? 0 : 60_000, + // }); + // noinspection JSMismatchedCollectionQueryUpdate + const users = []; + const error = null; + const isLoading = false; + const isSuccess = true; + const isError = false; + const fulfilledTimeStamp = useMemo(() => new Date(), []); const fulfilledTime = useMemo(() => new Date(fulfilledTimeStamp), [fulfilledTimeStamp]); const usersCount = isSuccess ? users.length : 0; diff --git a/src/widgets/RedirectToLogin.jsx b/src/widgets/RedirectToLogin.jsx deleted file mode 100644 index 5533ef0..0000000 --- a/src/widgets/RedirectToLogin.jsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Navigate, useLocation } from 'react-router-dom'; - -const RedirectToLogin = () => { - const location = useLocation(); - - return (); -}; - -export default RedirectToLogin; diff --git a/src/widgets/RequireRole.jsx b/src/widgets/RequireRole.jsx index 8c5a7dd..d90da57 100644 --- a/src/widgets/RequireRole.jsx +++ b/src/widgets/RequireRole.jsx @@ -1,15 +1,19 @@ -import { useCurrentUser } from '../hooks/useCurrentUser.js'; +import { useAuth } from 'react-oidc-context'; const RequireRole = ({ children, roles, }) => { - const { - user, - } = useCurrentUser(); + const auth = useAuth(); + + if (!auth.isAuthenticated) { + return null; + } + + const userRoles = Object.keys(auth.user.profile?.['urn:zitadel:iam:org:project:roles'] ?? {}); const searchRoles = typeof roles === 'string' ? [roles] : Array.from(roles); - const isAllowed = (user.roles || []).some(userRole => searchRoles.includes(userRole)); + const isAllowed = userRoles.some(userRole => searchRoles.includes(userRole)); return isAllowed ? children : null; }; diff --git a/src/widgets/Route/RequireLoggedIn.jsx b/src/widgets/Route/RequireLoggedIn.jsx deleted file mode 100644 index c1e808c..0000000 --- a/src/widgets/Route/RequireLoggedIn.jsx +++ /dev/null @@ -1,33 +0,0 @@ -import { useCurrentUser } from '../../hooks/useCurrentUser.js'; -import LoadingIcon from '../icons/LoadingIcon.jsx'; -import RedirectToLogin from '../RedirectToLogin.jsx'; -import React from 'react'; -import { Col, Row } from 'react-bootstrap'; - -export function RequireLoggedIn({ - children, -}) { - const { - isLoading, - isError, - isLoggedIn, - } = useCurrentUser(); - - if (isLoading) { - return ( - - - - ); - } - - if (isError) { - return (); - } - - if (!isLoggedIn) { - return (); - } - - return children; -} diff --git a/yarn.lock b/yarn.lock index 891198c..20b7b98 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2165,11 +2165,6 @@ cross-spawn@^7.0.2: shebang-command "^2.0.0" which "^2.0.1" -crypto-js@^4.0.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" - integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q== - csstype@^3.0.2: version "3.1.3" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" @@ -3242,13 +3237,6 @@ iterator.prototype@^1.1.2: reflect.getprototypeof "^1.0.4" set-function-name "^2.0.1" -js-pkce@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/js-pkce/-/js-pkce-1.4.0.tgz#4b2275c3f8bbd7d4e0f859ff51eea027c8dbbdb3" - integrity sha512-ztPzIgjQ+hY2uvwsTi625Yt/123NODAInGg2Y7aQLlKpNpD16A3WePjKyY2+B8WCoZy1cGG4xC9C68Dt2ZB8iQ== - dependencies: - crypto-js "^4.0.0" - "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" From 4e0e386893c31be7c1529ecb91c63e68633bf2e7 Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Wed, 19 Jun 2024 23:57:31 +0300 Subject: [PATCH 6/7] Implement user info retrieving, send correct scopes, fetch roles from userinfo --- src/app/store.js | 10 +++++--- src/config.js | 2 +- src/features/apiSlice.js | 48 ++++++++++++++++++------------------- src/hooks/useCurrentUser.js | 19 +++++++++++++++ src/widgets/RequireRole.jsx | 11 +++------ 5 files changed, 53 insertions(+), 37 deletions(-) create mode 100644 src/hooks/useCurrentUser.js diff --git a/src/app/store.js b/src/app/store.js index f322395..c4a2470 100644 --- a/src/app/store.js +++ b/src/app/store.js @@ -1,16 +1,20 @@ import { configureStore } from '@reduxjs/toolkit'; import { setupListeners } from '@reduxjs/toolkit/query'; -import { anonymousMqttApiSlice, anonymousPresenceApiSlice, authenticatedPortierApiSlice } from '../features/apiSlice'; +import { + anonymousMqttApiSlice, + authenticatedOidcAuthorityApiSlice, + authenticatedPortierApiSlice, +} from '../features/apiSlice'; export const store = configureStore({ reducer: { [authenticatedPortierApiSlice.reducerPath]: authenticatedPortierApiSlice.reducer, - [anonymousPresenceApiSlice.reducerPath]: anonymousPresenceApiSlice.reducer, + [authenticatedOidcAuthorityApiSlice.reducerPath]: authenticatedOidcAuthorityApiSlice.reducer, [anonymousMqttApiSlice.reducerPath]: anonymousMqttApiSlice.reducer, }, middleware: getDefaultMiddleware => getDefaultMiddleware().concat(authenticatedPortierApiSlice.middleware) - .concat(anonymousPresenceApiSlice.middleware).concat(anonymousMqttApiSlice.middleware), + .concat(authenticatedOidcAuthorityApiSlice.middleware).concat(anonymousMqttApiSlice.middleware), }); setupListeners(store.dispatch); diff --git a/src/config.js b/src/config.js index 2286edf..28fdf96 100644 --- a/src/config.js +++ b/src/config.js @@ -4,7 +4,7 @@ export const oidc = { authority: import.meta.env.OIDC_AUTHORITY_URL, client_id: import.meta.env.OIDC_CLIENT_ID, redirect_uri: window.location.protocol + '//' + window.location.host + import.meta.env.BASE_URL, - scope: 'openid profile offline_access', + scope: 'openid profile offline_access urn:zitadel:iam:org:project:id:zitadel:aud urn:zitadel:iam:org:projects:roles', onSigninCallback: () => { window.history.replaceState({}, document.title, window.location.pathname); }, diff --git a/src/features/apiSlice.js b/src/features/apiSlice.js index 4cba6a1..b25a602 100644 --- a/src/features/apiSlice.js +++ b/src/features/apiSlice.js @@ -14,32 +14,30 @@ function getAccessToken() { return user?.access_token; } +function prepareHeaders(headers) { + headers.set('accept', 'application/json'); + + const token = getAccessToken(); + + if (token) { + headers.set('authorization', 'Bearer ' + token); + } + + return headers; +} + const portierBaseUrl = import.meta.env.PORTIER_URL + 'api/'; -const presenceBaseUrl = import.meta.env.OIDC_AUTHORITY_URL + 'api/'; +const oidcAuthorityBaseUrl = import.meta.env.OIDC_AUTHORITY_URL; const mqttProxyBaseUrl = import.meta.env.MQTT_PROXY_URL; const authenticatedPortierBaseQuery = fetchBaseQuery({ baseUrl: portierBaseUrl, - prepareHeaders: headers => { - headers.set('accept', 'application/json'); - - const token = getAccessToken(); - - if (token) { - headers.set('authorization', 'Bearer ' + token); - } - - return headers; - }, + prepareHeaders, }); -const anonymousPresenceBaseQuery = fetchBaseQuery({ - baseUrl: presenceBaseUrl, - prepareHeaders: headers => { - headers.set('accept', 'application/json'); - - return headers; - }, +const authenticatedOidcAuthorityBaseQuery = fetchBaseQuery({ + baseUrl: oidcAuthorityBaseUrl, + prepareHeaders, }); const anonymousMqttProxyBaseQuery = fetchBaseQuery({ @@ -68,11 +66,11 @@ export const authenticatedPortierApiSlice = createApi({ }), }); -export const anonymousPresenceApiSlice = createApi({ - reducerPath: 'anonymousApi', - baseQuery: anonymousPresenceBaseQuery, +export const authenticatedOidcAuthorityApiSlice = createApi({ + reducerPath: 'authenticatedOidcAuthorityApi', + baseQuery: authenticatedOidcAuthorityBaseQuery, endpoints: builder => ({ - getPresentUsers: query(builder)('users/present'), + getUserInfo: query(builder)('oidc/v1/userinfo'), }), }); @@ -91,8 +89,8 @@ export const { } = authenticatedPortierApiSlice; export const { - useGetPresentUsersQuery, -} = anonymousPresenceApiSlice; + useGetUserInfoQuery, +} = authenticatedOidcAuthorityApiSlice; export const { useGetStatusQuery, diff --git a/src/hooks/useCurrentUser.js b/src/hooks/useCurrentUser.js new file mode 100644 index 0000000..09571fa --- /dev/null +++ b/src/hooks/useCurrentUser.js @@ -0,0 +1,19 @@ +import { useAuth } from 'react-oidc-context'; +import { useGetUserInfoQuery } from '../features/apiSlice.js'; + +export function useCurrentUser() { + const auth = useAuth(); + const { + data: user, + isError, + isLoading, + } = useGetUserInfoQuery(undefined, { + skip: !auth.isAuthenticated, + }); + + if (!auth.isAuthenticated || isLoading || isError) { + return null; + } + + return user; +} diff --git a/src/widgets/RequireRole.jsx b/src/widgets/RequireRole.jsx index d90da57..ebd29c9 100644 --- a/src/widgets/RequireRole.jsx +++ b/src/widgets/RequireRole.jsx @@ -1,16 +1,11 @@ -import { useAuth } from 'react-oidc-context'; +import { useCurrentUser } from '../hooks/useCurrentUser.js'; const RequireRole = ({ children, roles, }) => { - const auth = useAuth(); - - if (!auth.isAuthenticated) { - return null; - } - - const userRoles = Object.keys(auth.user.profile?.['urn:zitadel:iam:org:project:roles'] ?? {}); + const user = useCurrentUser(); + const userRoles = Object.keys(user?.['urn:zitadel:iam:org:project:roles'] ?? {}); const searchRoles = typeof roles === 'string' ? [roles] : Array.from(roles); const isAllowed = userRoles.some(userRole => searchRoles.includes(userRole)); From c54ba35c1c83a3407c81e2fe3c58a6686420774b Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Wed, 19 Jun 2024 23:57:31 +0300 Subject: [PATCH 7/7] Implement language switching on sign in --- src/App.jsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 2fbd008..acdbad2 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -9,7 +9,7 @@ import ActionLog from './pages/ActionLog.jsx'; import Lights from './pages/Lights.jsx'; import { useVariant } from './hooks/useVariant.js'; import { useEffect } from 'react'; -import { useAuth } from 'react-oidc-context'; +import { useGetUserInfoQuery } from './features/apiSlice.js'; import i18n from './i18n.js'; function App() { @@ -21,15 +21,17 @@ function App() { } }, [variant]); - const auth = useAuth(); - // TODO - console.log(auth); + const { + data: user, + } = useGetUserInfoQuery(); + useEffect(function() { - if (auth.isAuthenticated) { - // TODO - i18n.changeLanguage(auth.user.profile?.preferredLanguage || 'bg').then(() => {}); + if (user?.locale) { + (async () => { + await i18n.changeLanguage(user.locale); + })(); } - }, [auth]); + }, [user?.locale]); return (<>