From 4d7d7b077cbc09ecc768475a71860770cdd010e3 Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Sun, 9 Jun 2024 00:44:07 +0300 Subject: [PATCH 1/9] Change dev authority URL --- .env.development | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.development b/.env.development index 2d03c83..a30ffc5 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 bacfc25bab269fb9667a8339eefa8f9113d4e002 Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Mon, 3 Mar 2025 17:35:33 +0200 Subject: [PATCH 2/9] Migrate to OIDC --- .env.development | 2 +- package.json | 1 - src/App.jsx | 31 +++--- src/config.js | 11 ++- src/hooks/useAuthStorage.js | 95 ------------------- src/hooks/useAuthenticatedSWR.js | 8 +- src/hooks/useCurrentUser.js | 5 - src/hooks/useDeviceAction.js | 23 +---- src/hooks/useRememberPage.js | 54 ----------- src/layout/NavBar.jsx | 46 +++------ src/oauth.js | 59 ------------ src/pages/ActionLog.jsx | 5 +- src/pages/Doors.jsx | 3 +- src/pages/Lights.jsx | 3 +- src/pages/Login.jsx | 30 ------ src/pages/Logout.jsx | 40 -------- src/pages/OauthCallback.jsx | 42 -------- src/utils/swr.js | 30 +----- .../DeviceActionButton/DeviceActionButton.jsx | 3 - src/widgets/RedirectToLogin.jsx | 11 --- src/widgets/RequireRole.jsx | 14 ++- src/widgets/Route/RequireLoggedIn.jsx | 35 ------- yarn.lock | 12 --- 23 files changed, 69 insertions(+), 494 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 a30ffc5..2821c16 100644 --- a/.env.development +++ b/.env.development @@ -1,5 +1,5 @@ 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/ PRESENCE_URL=http://localhost:3000/ diff --git a/package.json b/package.json index b65a053..8d6726b 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,6 @@ "date-fns": "^4.1.0", "i18next": "^23.15.1", "i18next-http-backend": "^2.6.1", - "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 8b68d58..7d28da8 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -7,14 +7,12 @@ import NavBar from './layout/NavBar.jsx'; import Footer from './layout/Footer.jsx'; import Sensors from './pages/Sensors.jsx'; import Doors from './pages/Doors.jsx'; -import OauthCallback from './pages/OauthCallback.jsx'; -import Logout from './pages/Logout.jsx'; -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 Hvac from './pages/Hvac.jsx'; import { useVariant } from './hooks/useVariant.js'; +import { useAuth } from 'react-oidc-context'; +import i18n from './i18n.js'; function App() { const variant = useVariant(); @@ -25,26 +23,27 @@ 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/config.js b/src/config.js index 59af4f3..d7f7cdb 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 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 = { diff --git a/src/hooks/useAuthStorage.js b/src/hooks/useAuthStorage.js deleted file mode 100644 index afb7ace..0000000 --- a/src/hooks/useAuthStorage.js +++ /dev/null @@ -1,95 +0,0 @@ -import { useLocalStorage } from '@uidotdev/usehooks'; -import { scopes } from '../oauth.js'; - -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 (Object.prototype.hasOwnProperty.call(tokenResponse, 'error') && Object.prototype.hasOwnProperty.call(tokenResponse, 'error_description')) { - throw new Error(tokenResponse.error_description); - } - - if ( - !Object.prototype.hasOwnProperty.call(tokenResponse, 'access_token') || - !Object.prototype.hasOwnProperty.call(tokenResponse, 'created_at') || - !Object.prototype.hasOwnProperty.call(tokenResponse, 'expires_in') || - !Object.prototype.hasOwnProperty.call(tokenResponse, 'refresh_token') || - !Object.prototype.hasOwnProperty.call(tokenResponse, 'scope') || - !Object.prototype.hasOwnProperty.call(tokenResponse, '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/useAuthenticatedSWR.js b/src/hooks/useAuthenticatedSWR.js index 0f9bbc9..ed17320 100644 --- a/src/hooks/useAuthenticatedSWR.js +++ b/src/hooks/useAuthenticatedSWR.js @@ -1,10 +1,10 @@ import useSWR from 'swr'; import { authenticatedFetcher } from '../utils/swr.js'; -import { useAuthStorage } from './useAuthStorage.js'; +import { useAuth } from 'react-oidc-context'; export function useAuthenticatedSWR(key, config) { - const { accessToken } = useAuthStorage(); - const hasAccessToken = !!accessToken; + const auth = useAuth(); + const token = auth.isAuthenticated ? auth.user?.access_token : null; - return useSWR(hasAccessToken ? key : null, authenticatedFetcher, config); + return useSWR(token ? key : null, authenticatedFetcher(token), config); } diff --git a/src/hooks/useCurrentUser.js b/src/hooks/useCurrentUser.js deleted file mode 100644 index 04c3390..0000000 --- a/src/hooks/useCurrentUser.js +++ /dev/null @@ -1,5 +0,0 @@ -import { useAuthenticatedSWR } from './useAuthenticatedSWR.js'; - -export function useCurrentUser(config) { - return useAuthenticatedSWR(import.meta.env.OIDC_AUTHORITY_URL.concat('api/current_user'), config); -} diff --git a/src/hooks/useDeviceAction.js b/src/hooks/useDeviceAction.js index 1d78aab..3c25c07 100644 --- a/src/hooks/useDeviceAction.js +++ b/src/hooks/useDeviceAction.js @@ -1,24 +1,9 @@ -import { authenticatedFetcher } from '../utils/swr.js'; +import { useAuthenticatedSWR } from './useAuthenticatedSWR.js'; export default function useDeviceAction(deviceId, action) { const url = import.meta.env.PORTIER_URL.concat('api/device/').concat(deviceId).concat('/').concat(action); - let error = null; - async function execute() { - try { - return await authenticatedFetcher(url, { - method: 'POST', - }); - } - catch (e) { - error = { - status: e.status, - }; - } - } - - return { - execute, - error, - }; + return useAuthenticatedSWR(url, { + method: 'POST', + }); } 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 ae59a17..3aca560 100644 --- a/src/layout/NavBar.jsx +++ b/src/layout/NavBar.jsx @@ -1,5 +1,4 @@ -import { useEffect } from 'react'; -import { NavLink, useLocation } from 'react-router-dom'; +import { NavLink } from 'react-router-dom'; import { Container, Image, Nav, Navbar, NavDropdown } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; @@ -7,29 +6,19 @@ import './NavBar.css'; import initLabLogo from '../assets/initlab/logo.svg'; import colibriLogo from '../assets/colibri/logo.png'; import DoorClosedIcon from '../widgets/icons/DoorClosedIcon.jsx'; -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 { - data: user, - } = 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 (user?.locale) { - i18n.changeLanguage(user.locale).then(() => {}); - } - }, [user]); - - const location = useLocation(); - return ( { - + {' '} {t('views.navigation.labbers')} - {user ? + {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 88a43cf..9ff7cd6 100644 --- a/src/pages/ActionLog.jsx +++ b/src/pages/ActionLog.jsx @@ -7,6 +7,7 @@ import { useActionLog } from '../hooks/useActionLog.js'; import LoadingIcon from '../widgets/icons/LoadingIcon.jsx'; import ErrorMessage from '../widgets/ErrorMessage.jsx'; import ActionLogEntry from '../widgets/ActionLog/ActionLogEntry.jsx'; +import { withAuthenticationRequired } from 'react-oidc-context'; const ActionLog = () => { const { t } = useTranslation(); @@ -23,7 +24,7 @@ const ActionLog = () => { }); if (!hasAccess) { - return (); + return (); } return (<> @@ -51,4 +52,4 @@ const ActionLog = () => { ); }; -export default ActionLog; +export default withAuthenticationRequired(ActionLog); diff --git a/src/pages/Doors.jsx b/src/pages/Doors.jsx index 10a4fb1..342b8f8 100644 --- a/src/pages/Doors.jsx +++ b/src/pages/Doors.jsx @@ -1,8 +1,9 @@ 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 e627a50..0feb522 100644 --- a/src/pages/Lights.jsx +++ b/src/pages/Lights.jsx @@ -1,8 +1,9 @@ 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 6912dd7..0000000 --- a/src/pages/Login.jsx +++ /dev/null @@ -1,30 +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 2052e5e..0000000 --- a/src/pages/Logout.jsx +++ /dev/null @@ -1,40 +0,0 @@ -import { useEffect, useRef } from 'react'; -import { useNavigate } from 'react-router-dom'; -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 { 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 b1644f5..0000000 --- a/src/pages/OauthCallback.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; -import { Col, Row } from 'react-bootstrap'; - -import { exchangeForAccessToken } from '../oauth.js'; -import { useAuthStorage } from '../hooks/useAuthStorage.js'; -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/utils/swr.js b/src/utils/swr.js index be3e892..74d6dde 100644 --- a/src/utils/swr.js +++ b/src/utils/swr.js @@ -1,8 +1,4 @@ -import { getAccessToken, isAccessTokenExpired } from '../hooks/useAuthStorage.js'; -import { refreshTokenIfNeeded } from '../oauth.js'; - -function addTokenHeader(args) { - const token = getAccessToken(); +function addTokenHeader(args, token) { const authHeader = { authorization: 'Bearer '.concat(token), }; @@ -36,26 +32,8 @@ export const fetcher = async (...args) => { return await response.json(); }; -export const authenticatedFetcher = async (...args) => { - if (isAccessTokenExpired()) { - const refreshed = await refreshTokenIfNeeded(); - - if (!refreshed) { - return Promise.reject(); - } - } - - addTokenHeader(args); +export const authenticatedFetcher = token => async (...args) => { + addTokenHeader(args, token); - try { - return await fetcher(...args); - } - catch (e) { - if (e?.status === 401 && await refreshTokenIfNeeded()) { - addTokenHeader(args); - return await fetcher(...args); - } - - throw e; - } + return await fetcher(...args); }; diff --git a/src/widgets/DeviceActionButton/DeviceActionButton.jsx b/src/widgets/DeviceActionButton/DeviceActionButton.jsx index 868b52a..7d6c63a 100644 --- a/src/widgets/DeviceActionButton/DeviceActionButton.jsx +++ b/src/widgets/DeviceActionButton/DeviceActionButton.jsx @@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next'; import PropTypes from 'prop-types'; import useDeviceAction from '../../hooks/useDeviceAction.js'; -import RedirectToLogin from '../RedirectToLogin.jsx'; import { sleep } from '../../utils/time.js'; import './DeviceActionButton.scss'; @@ -62,7 +61,6 @@ const DeviceActionButton = ({ const { execute, - error, } = useDeviceAction(device.id, action); const {t} = useTranslation(); @@ -87,7 +85,6 @@ const DeviceActionButton = ({
{label}
- {error?.status && [401, 403].includes(error.status) && } ); }; 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 e5142f3..d52f54a 100644 --- a/src/widgets/RequireRole.jsx +++ b/src/widgets/RequireRole.jsx @@ -1,16 +1,20 @@ -import { useCurrentUser } from '../hooks/useCurrentUser.js'; +import { useAuth } from 'react-oidc-context'; import PropTypes from 'prop-types'; const RequireRole = ({ children, roles, }) => { - const { - data: 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 5e0a4da..0000000 --- a/src/widgets/Route/RequireLoggedIn.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Col, Row } from 'react-bootstrap'; -import PropTypes from 'prop-types'; - -import { useCurrentUser } from '../../hooks/useCurrentUser.js'; -import LoadingIcon from '../icons/LoadingIcon.jsx'; -import RedirectToLogin from '../RedirectToLogin.jsx'; - -const RequireLoggedIn = ({ - children, -}) => { - const { - data: user, - isLoading, - } = useCurrentUser(); - - if (isLoading) { - return ( - - - - ); - } - - if (!user) { - return (); - } - - return children; -}; - -RequireLoggedIn.propTypes = { - children: PropTypes.node.isRequired, -}; - -export default RequireLoggedIn; diff --git a/yarn.lock b/yarn.lock index ad0449d..23ec74d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -902,11 +902,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" @@ -1734,13 +1729,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 98fd7f12560914c034ce06d02d4a2555887383a4 Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Mon, 3 Mar 2025 19:03:30 +0200 Subject: [PATCH 3/9] Fix lint errors --- src/App.jsx | 12 +++++++----- src/pages/ActionLog.jsx | 4 +++- src/pages/Doors.jsx | 4 +++- src/pages/Lights.jsx | 4 +++- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 7d28da8..ce3e04e 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -24,13 +24,15 @@ function App() { }, [variant]); const auth = useAuth(); - // TODO - console.log(auth); + useEffect(function() { - if (auth.isAuthenticated) { - // TODO - i18n.changeLanguage(auth.user.profile?.preferredLanguage || 'bg').then(() => {}); + if (!auth.isAuthenticated) { + return ; } + + (async () => { + await i18n.changeLanguage(auth.user.profile?.preferredLanguage ?? 'bg'); + })(); }, [auth]); return (<> diff --git a/src/pages/ActionLog.jsx b/src/pages/ActionLog.jsx index 9ff7cd6..af618cc 100644 --- a/src/pages/ActionLog.jsx +++ b/src/pages/ActionLog.jsx @@ -52,4 +52,6 @@ const ActionLog = () => { ); }; -export default withAuthenticationRequired(ActionLog); +const AuthenticatedActionLog = withAuthenticationRequired(ActionLog); + +export default AuthenticatedActionLog; diff --git a/src/pages/Doors.jsx b/src/pages/Doors.jsx index 342b8f8..44fe313 100644 --- a/src/pages/Doors.jsx +++ b/src/pages/Doors.jsx @@ -6,4 +6,6 @@ const Doors = () => { return (); }; -export default withAuthenticationRequired(Doors); +const AuthenticatedDoors = withAuthenticationRequired(Doors); + +export default AuthenticatedDoors; diff --git a/src/pages/Lights.jsx b/src/pages/Lights.jsx index 0feb522..ea854c4 100644 --- a/src/pages/Lights.jsx +++ b/src/pages/Lights.jsx @@ -6,4 +6,6 @@ const Lights = () => { return (); }; -export default withAuthenticationRequired(Lights); +const AuthenticatedLights = withAuthenticationRequired(Lights); + +export default AuthenticatedLights; From 22b49a5ab563357bdb02b88278ccddc40dddcba7 Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Mon, 3 Mar 2025 19:12:40 +0200 Subject: [PATCH 4/9] Fix language switching --- src/App.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App.jsx b/src/App.jsx index ce3e04e..1cd2140 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -31,7 +31,7 @@ function App() { } (async () => { - await i18n.changeLanguage(auth.user.profile?.preferredLanguage ?? 'bg'); + await i18n.changeLanguage(auth.user.profile?.locale ?? 'bg'); })(); }, [auth]); From 40c86ad8bcfd73d8fc575839a1cfd6c16d1d71e0 Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Mon, 3 Mar 2025 19:12:48 +0200 Subject: [PATCH 5/9] Load userinfo automatically --- src/config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/config.js b/src/config.js index d7f7cdb..40bb61c 100644 --- a/src/config.js +++ b/src/config.js @@ -11,6 +11,7 @@ export const oidc = { userStore: new WebStorageStateStore({ store: window.localStorage, }), + loadUserInfo: true, }; export const sensors = { From 0464d3dc74b89c432bd80d60cd23c51216f036bf Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Mon, 3 Mar 2025 20:42:20 +0200 Subject: [PATCH 6/9] Fix error handling --- src/utils/swr.js | 1 + src/widgets/ErrorMessage.jsx | 10 ++++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/utils/swr.js b/src/utils/swr.js index 74d6dde..6b6edd7 100644 --- a/src/utils/swr.js +++ b/src/utils/swr.js @@ -26,6 +26,7 @@ export const fetcher = async (...args) => { if (!response.ok) { const error = new Error('HTTP error '.concat(response.status)); error.status = response.status; + error.statusText = response.statusText; throw error; } diff --git a/src/widgets/ErrorMessage.jsx b/src/widgets/ErrorMessage.jsx index 06fd53d..3f50fe1 100644 --- a/src/widgets/ErrorMessage.jsx +++ b/src/widgets/ErrorMessage.jsx @@ -3,16 +3,14 @@ import PropTypes from 'prop-types'; const ErrorMessage = ({ error, }) => { - return (<> - {error.status}{' '} - {error.error} - ); + return ( + {error.__proto__.name}: {error.message} + ); }; ErrorMessage.propTypes = { error: PropTypes.shape({ - status: PropTypes.string.isRequired, - error: PropTypes.string.isRequired, + message: PropTypes.string.isRequired, }).isRequired, }; From 08e97acc63f9ce42961621141753b3bc770105ad Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Mon, 3 Mar 2025 23:25:22 +0200 Subject: [PATCH 7/9] Fix SWR usage --- src/hooks/useActionLog.js | 2 +- src/hooks/useAuthenticatedSWR.js | 6 +++--- src/hooks/useAuthenticatedSWRMutation.js | 12 ++++++++++++ src/hooks/useDeviceAction.js | 6 ++---- src/hooks/useDevices.js | 2 +- src/utils/swr.js | 18 ++++++++++++++---- .../DeviceActionButton/DeviceActionButton.jsx | 4 ++-- 7 files changed, 35 insertions(+), 15 deletions(-) create mode 100644 src/hooks/useAuthenticatedSWRMutation.js diff --git a/src/hooks/useActionLog.js b/src/hooks/useActionLog.js index 4c0fa5d..a29ec1a 100644 --- a/src/hooks/useActionLog.js +++ b/src/hooks/useActionLog.js @@ -1,5 +1,5 @@ import { useAuthenticatedSWR } from './useAuthenticatedSWR.js'; -export function useActionLog(config) { +export function useActionLog(config = {}) { return useAuthenticatedSWR(import.meta.env.PORTIER_URL.concat('api/actionLog/0/0'), config); } diff --git a/src/hooks/useAuthenticatedSWR.js b/src/hooks/useAuthenticatedSWR.js index ed17320..c4d2d3e 100644 --- a/src/hooks/useAuthenticatedSWR.js +++ b/src/hooks/useAuthenticatedSWR.js @@ -2,9 +2,9 @@ import useSWR from 'swr'; import { authenticatedFetcher } from '../utils/swr.js'; import { useAuth } from 'react-oidc-context'; -export function useAuthenticatedSWR(key, config) { +export function useAuthenticatedSWR(key, config = {}) { const auth = useAuth(); - const token = auth.isAuthenticated ? auth.user?.access_token : null; + const token = auth.user?.access_token; - return useSWR(token ? key : null, authenticatedFetcher(token), config); + return useSWR([key, token], authenticatedFetcher, config); } diff --git a/src/hooks/useAuthenticatedSWRMutation.js b/src/hooks/useAuthenticatedSWRMutation.js new file mode 100644 index 0000000..1b2b961 --- /dev/null +++ b/src/hooks/useAuthenticatedSWRMutation.js @@ -0,0 +1,12 @@ +import useSWRMutation from 'swr/mutation'; +import { authenticatedFetcher } from '../utils/swr.js'; +import { useAuth } from 'react-oidc-context'; + +export function useAuthenticatedSWRMutation(key, config = {}) { + const auth = useAuth(); + const token = auth.user?.access_token; + + return useSWRMutation([key, token, { + method: 'POST', + }], authenticatedFetcher, config); +} diff --git a/src/hooks/useDeviceAction.js b/src/hooks/useDeviceAction.js index 3c25c07..4237b7b 100644 --- a/src/hooks/useDeviceAction.js +++ b/src/hooks/useDeviceAction.js @@ -1,9 +1,7 @@ -import { useAuthenticatedSWR } from './useAuthenticatedSWR.js'; +import { useAuthenticatedSWRMutation } from './useAuthenticatedSWRMutation.js'; export default function useDeviceAction(deviceId, action) { const url = import.meta.env.PORTIER_URL.concat('api/device/').concat(deviceId).concat('/').concat(action); - return useAuthenticatedSWR(url, { - method: 'POST', - }); + return useAuthenticatedSWRMutation(url); } diff --git a/src/hooks/useDevices.js b/src/hooks/useDevices.js index 607791d..8f3f65f 100644 --- a/src/hooks/useDevices.js +++ b/src/hooks/useDevices.js @@ -1,5 +1,5 @@ import { useAuthenticatedSWR } from './useAuthenticatedSWR.js'; -export function useDevices(config) { +export function useDevices(config = {}) { return useAuthenticatedSWR(import.meta.env.PORTIER_URL.concat('api/devices'), config); } diff --git a/src/utils/swr.js b/src/utils/swr.js index 6b6edd7..8dc70b4 100644 --- a/src/utils/swr.js +++ b/src/utils/swr.js @@ -1,4 +1,8 @@ -function addTokenHeader(args, token) { +function addTokenHeader(args = {}, token) { + if (!token) { + return args; + } + const authHeader = { authorization: 'Bearer '.concat(token), }; @@ -18,23 +22,29 @@ function addTokenHeader(args, token) { }, }; } + + return args; } export const fetcher = async (...args) => { const response = await fetch(...args); if (!response.ok) { - const error = new Error('HTTP error '.concat(response.status)); + const error = new Error(`HTTP error ${response.status.toString(10)} (${response.statusText})`); error.status = response.status; error.statusText = response.statusText; throw error; } + if (response.status === 204) { + return true; + } + return await response.json(); }; -export const authenticatedFetcher = token => async (...args) => { - addTokenHeader(args, token); +export const authenticatedFetcher = async ([key, token, ...options]) => { + const args = addTokenHeader([key, ...options], token); return await fetcher(...args); }; diff --git a/src/widgets/DeviceActionButton/DeviceActionButton.jsx b/src/widgets/DeviceActionButton/DeviceActionButton.jsx index 7d6c63a..72d437b 100644 --- a/src/widgets/DeviceActionButton/DeviceActionButton.jsx +++ b/src/widgets/DeviceActionButton/DeviceActionButton.jsx @@ -60,7 +60,7 @@ const DeviceActionButton = ({ const [ disabled, setDisabled ] = useState(false); const { - execute, + trigger, } = useDeviceAction(device.id, action); const {t} = useTranslation(); @@ -71,7 +71,7 @@ const DeviceActionButton = ({ async function handleClick() { setDisabled(true); - await execute(); + await trigger(); await sleep(3000); setDisabled(false); } From 812e502e80c8b20ccfbfce25b82ca65d1818593f Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Mon, 3 Mar 2025 23:35:04 +0200 Subject: [PATCH 8/9] Change string concatenations to template strings --- src/config.js | 2 +- src/hooks/useActionLog.js | 2 +- src/hooks/useDeviceAction.js | 2 +- src/hooks/useDevices.js | 2 +- src/hooks/useMqttStatus.js | 2 +- src/hooks/usePresentUsers.js | 2 +- src/i18n.js | 2 +- src/layout/NavBar.jsx | 6 +++--- src/pages/Devices.jsx | 2 +- src/pages/Sensors.jsx | 4 ++-- src/utils/swr.js | 2 +- src/widgets/DeviceActionButton/DeviceActionButton.jsx | 2 +- src/widgets/PresentUsers/PresentUsers.jsx | 4 ++-- 13 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/config.js b/src/config.js index 40bb61c..003c7aa 100644 --- a/src/config.js +++ b/src/config.js @@ -3,7 +3,7 @@ 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, + 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); diff --git a/src/hooks/useActionLog.js b/src/hooks/useActionLog.js index a29ec1a..d666d6c 100644 --- a/src/hooks/useActionLog.js +++ b/src/hooks/useActionLog.js @@ -1,5 +1,5 @@ import { useAuthenticatedSWR } from './useAuthenticatedSWR.js'; export function useActionLog(config = {}) { - return useAuthenticatedSWR(import.meta.env.PORTIER_URL.concat('api/actionLog/0/0'), config); + return useAuthenticatedSWR(`${import.meta.env.PORTIER_URL}api/actionLog/0/0`, config); } diff --git a/src/hooks/useDeviceAction.js b/src/hooks/useDeviceAction.js index 4237b7b..d33cc24 100644 --- a/src/hooks/useDeviceAction.js +++ b/src/hooks/useDeviceAction.js @@ -1,7 +1,7 @@ import { useAuthenticatedSWRMutation } from './useAuthenticatedSWRMutation.js'; export default function useDeviceAction(deviceId, action) { - const url = import.meta.env.PORTIER_URL.concat('api/device/').concat(deviceId).concat('/').concat(action); + const url = `${import.meta.env.PORTIER_URL}api/device/${deviceId}/${action}`; return useAuthenticatedSWRMutation(url); } diff --git a/src/hooks/useDevices.js b/src/hooks/useDevices.js index 8f3f65f..ad3c067 100644 --- a/src/hooks/useDevices.js +++ b/src/hooks/useDevices.js @@ -1,5 +1,5 @@ import { useAuthenticatedSWR } from './useAuthenticatedSWR.js'; export function useDevices(config = {}) { - return useAuthenticatedSWR(import.meta.env.PORTIER_URL.concat('api/devices'), config); + return useAuthenticatedSWR(`${import.meta.env.PORTIER_URL}api/devices`, config); } diff --git a/src/hooks/useMqttStatus.js b/src/hooks/useMqttStatus.js index 952f482..21e3d94 100644 --- a/src/hooks/useMqttStatus.js +++ b/src/hooks/useMqttStatus.js @@ -2,5 +2,5 @@ import useSWR from 'swr'; import { fetcher } from '../utils/swr.js'; export function useMqttStatus(config) { - return useSWR(import.meta.env.MQTT_PROXY_URL.concat('status'), fetcher, config); + return useSWR(`${import.meta.env.MQTT_PROXY_URL}status`, fetcher, config); } diff --git a/src/hooks/usePresentUsers.js b/src/hooks/usePresentUsers.js index 93f8003..09069f4 100644 --- a/src/hooks/usePresentUsers.js +++ b/src/hooks/usePresentUsers.js @@ -2,5 +2,5 @@ import useSWR from 'swr'; import { fetcher } from '../utils/swr.js'; export function usePresentUsers(config) { - return useSWR(import.meta.env.PRESENCE_URL.concat('api/users/present'), fetcher, config); + return useSWR(`${import.meta.env.PRESENCE_URL}api/users/present`, fetcher, config); } diff --git a/src/i18n.js b/src/i18n.js index 4bf10ec..82f03d6 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -9,7 +9,7 @@ i18n .init({ fallbackLng: 'bg', backend: { - loadPath: import.meta.env.BASE_URL + 'locales/{{lng}}.yaml', + loadPath: `${import.meta.env.BASE_URL}locales/{{lng}}.yaml`, parse: data => load(data), }, interpolation: { diff --git a/src/layout/NavBar.jsx b/src/layout/NavBar.jsx index 3aca560..bd46f33 100644 --- a/src/layout/NavBar.jsx +++ b/src/layout/NavBar.jsx @@ -90,7 +90,7 @@ const NavBar = () => { - + {' '} {t('views.navigation.labbers')} @@ -101,14 +101,14 @@ const NavBar = () => { {' '} {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_token_management')} diff --git a/src/pages/Devices.jsx b/src/pages/Devices.jsx index ef97702..3746bee 100644 --- a/src/pages/Devices.jsx +++ b/src/pages/Devices.jsx @@ -58,7 +58,7 @@ const Devices = ({ }) : - {t('views.' + deviceType + '.no_access')} + {t(`views.${deviceType}.no_access`)} } diff --git a/src/pages/Sensors.jsx b/src/pages/Sensors.jsx index 208c570..430a97e 100644 --- a/src/pages/Sensors.jsx +++ b/src/pages/Sensors.jsx @@ -22,8 +22,8 @@ const Sensors = () => { {grafana.panels.map(panelId => -