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..a99a94a 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=https://auth-staging.initlab.org/
+OIDC_CLIENT_ID=270767587204382721@access_control
+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/package.json b/package.json
index 381d05a..6e054b3 100644
--- a/package.json
+++ b/package.json
@@ -19,13 +19,14 @@
"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",
"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/src/App.jsx b/src/App.jsx
index 44590c4..acdbad2 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 { useGetUserInfoQuery } from './features/apiSlice.js';
+import i18n from './i18n.js';
function App() {
const variant = useVariant();
@@ -23,23 +21,28 @@ function App() {
}
}, [variant]);
+ const {
+ data: user,
+ } = useGetUserInfoQuery();
+
+ useEffect(function() {
+ if (user?.locale) {
+ (async () => {
+ await i18n.changeLanguage(user.locale);
+ })();
+ }
+ }, [user?.locale]);
+
return (<>
} />
-
-
- } />
-
-
- } />
+ } />
+ } />
} />
} />
- } />
- } />
- } />
diff --git a/src/app/store.js b/src/app/store.js
index 216b652..c4a2470 100644
--- a/src/app/store.js
+++ b/src/app/store.js
@@ -1,22 +1,20 @@
import { configureStore } from '@reduxjs/toolkit';
import { setupListeners } from '@reduxjs/toolkit/query';
import {
- anonymousApiSlice,
anonymousMqttApiSlice,
- authenticatedApiSlice,
- authenticatedDeviceApiSlice
+ authenticatedOidcAuthorityApiSlice,
+ authenticatedPortierApiSlice,
} from '../features/apiSlice';
export const store = configureStore({
reducer: {
- [anonymousApiSlice.reducerPath]: anonymousApiSlice.reducer,
+ [authenticatedPortierApiSlice.reducerPath]: authenticatedPortierApiSlice.reducer,
+ [authenticatedOidcAuthorityApiSlice.reducerPath]: authenticatedOidcAuthorityApiSlice.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(authenticatedOidcAuthorityApiSlice.middleware).concat(anonymousMqttApiSlice.middleware),
});
setupListeners(store.dispatch);
diff --git a/src/config.js b/src/config.js
index e95b6d0..28fdf96 100644
--- a/src/config.js
+++ b/src/config.js
@@ -1,3 +1,18 @@
+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,
+ 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);
+ },
+ userStore: new WebStorageStateStore({
+ store: window.localStorage,
+ }),
+};
+
export const sensors = {
'sensors/big-room/temperature': {
type: 'Temperature',
diff --git a/src/features/apiSlice.js b/src/features/apiSlice.js
index b2db772..b25a602 100644
--- a/src/features/apiSlice.js
+++ b/src/features/apiSlice.js
@@ -1,113 +1,57 @@
+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.BACKEND_URL + 'api/';
-const deviceApiBaseUrl = import.meta.env.DEVICE_BACKEND_URL + 'api/';
-const mqttApiBaseUrl = import.meta.env.MQTT_BACKEND_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');
-
- return headers;
- },
-});
-
-const anonymousMqttBaseQuery = fetchBaseQuery({
- baseUrl: mqttApiBaseUrl,
-});
-
-const authenticatedBaseQuery = fetchBaseQuery({
- baseUrl: apiBaseUrl,
- prepareHeaders: headers => {
- headers.set('accept', 'application/json');
-
- const token = getAccessToken();
-
- if (token) {
- headers.set('authorization', 'Bearer ' + token);
- }
-
- return headers;
- },
-});
-
-const authenticatedDeviceBaseQuery = fetchBaseQuery({
- baseUrl: deviceApiBaseUrl,
- 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();
+ if (!oidcStorage) {
+ return null;
}
- let result = await authenticatedBaseQuery(args, api, extraOptions);
-
- if (result?.error?.status === 401 && await refreshTokenIfNeeded()) {
- return authenticatedBaseQuery(args, api, extraOptions);
- }
+ const user = User.fromStorageString(oidcStorage);
- return result;
-};
+ return user?.access_token;
+}
-const authenticatedDeviceBaseQueryWithReauth = async (args, api, extraOptions) => {
- if (isAccessTokenExpired()) {
- await refreshTokenIfNeeded();
- }
+function prepareHeaders(headers) {
+ headers.set('accept', 'application/json');
- let result = await authenticatedDeviceBaseQuery(args, api, extraOptions);
+ const token = getAccessToken();
- if (result?.error?.status === 401 && await refreshTokenIfNeeded()) {
- return authenticatedDeviceBaseQuery(args, api, extraOptions);
+ if (token) {
+ headers.set('authorization', 'Bearer ' + token);
}
- return result;
-};
+ return headers;
+}
-const query = builder => url => builder.query({
- query: () => url,
+const portierBaseUrl = import.meta.env.PORTIER_URL + 'api/';
+const oidcAuthorityBaseUrl = import.meta.env.OIDC_AUTHORITY_URL;
+const mqttProxyBaseUrl = import.meta.env.MQTT_PROXY_URL;
+
+const authenticatedPortierBaseQuery = fetchBaseQuery({
+ baseUrl: portierBaseUrl,
+ prepareHeaders,
});
-export const anonymousApiSlice = createApi({
- reducerPath: 'anonymousApi',
- baseQuery: anonymousBaseQuery,
- endpoints: builder => ({
- getPresentUsers: query(builder)('users/present'),
- }),
+const authenticatedOidcAuthorityBaseQuery = fetchBaseQuery({
+ baseUrl: oidcAuthorityBaseUrl,
+ prepareHeaders,
});
-export const anonymousMqttApiSlice = createApi({
- reducerPath: 'anonymousMqttApi',
- baseQuery: anonymousMqttBaseQuery,
- endpoints: builder => ({
- getStatus: query(builder)('status'),
- }),
+const anonymousMqttProxyBaseQuery = fetchBaseQuery({
+ baseUrl: mqttProxyBaseUrl,
});
-export const authenticatedApiSlice = createApi({
- reducerPath: 'authenticatedApi',
- baseQuery: authenticatedBaseQueryWithReauth,
- endpoints: builder => ({
- getCurrentUser: query(builder)('current_user'),
- }),
+
+const query = builder => url => builder.query({
+ query: () => url,
});
-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 +66,32 @@ export const authenticatedDeviceApiSlice = createApi({
}),
});
-export const {
- useGetPresentUsersQuery,
-} = anonymousApiSlice;
-
-export const {
- useGetStatusQuery,
-} = anonymousMqttApiSlice;
+export const authenticatedOidcAuthorityApiSlice = createApi({
+ reducerPath: 'authenticatedOidcAuthorityApi',
+ baseQuery: authenticatedOidcAuthorityBaseQuery,
+ endpoints: builder => ({
+ getUserInfo: query(builder)('oidc/v1/userinfo'),
+ }),
+});
-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 {
+ useGetUserInfoQuery,
+} = authenticatedOidcAuthorityApiSlice;
+
+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
index 77b9075..09571fa 100644
--- a/src/hooks/useCurrentUser.js
+++ b/src/hooks/useCurrentUser.js
@@ -1,18 +1,19 @@
-import { useAuthStorage } from './useAuthStorage.js';
-import { useGetCurrentUserQuery } from '../features/apiSlice.js';
+import { useAuth } from 'react-oidc-context';
+import { useGetUserInfoQuery } from '../features/apiSlice.js';
export function useCurrentUser() {
- const { accessToken } = useAuthStorage();
- const hasAccessToken = !!accessToken;
- const queryResult = useGetCurrentUserQuery(undefined, {
- skip: !hasAccessToken,
+ const auth = useAuth();
+ const {
+ data: user,
+ isError,
+ isLoading,
+ } = useGetUserInfoQuery(undefined, {
+ skip: !auth.isAuthenticated,
});
- const user = queryResult.isSuccess ? queryResult.data : {};
- return {
- hasAccessToken,
- user,
- isLoggedIn: user.hasOwnProperty('id'),
- ...queryResult,
- };
+ if (!auth.isAuthenticated || isLoading || isError) {
+ return null;
+ }
+
+ return user;
}
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 f7b4c9b..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.BACKEND_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/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(
-
+
+
+
,
diff --git a/src/oauth.js b/src/oauth.js
deleted file mode 100644
index 340e1e4..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.OAUTH_CLIENT_ID;
-const baseUrl = import.meta.env.BACKEND_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..ebd29c9 100644
--- a/src/widgets/RequireRole.jsx
+++ b/src/widgets/RequireRole.jsx
@@ -4,12 +4,11 @@ const RequireRole = ({
children,
roles,
}) => {
- const {
- user,
- } = useCurrentUser();
+ 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 = (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/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(),
diff --git a/yarn.lock b/yarn.lock
index d488807..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"
@@ -3322,6 +3310,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 +3521,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 +3703,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"