diff --git a/.env.acceptance b/.env.acceptance
index f0bb06a96..52c48d655 100644
--- a/.env.acceptance
+++ b/.env.acceptance
@@ -13,3 +13,15 @@ REACT_APP_HOST_TON=https://ton.woon-a.azure.amsterdam.nl/
REACT_APP_AUTH_URL=https://acc.iam.amsterdam.nl/auth/
REACT_APP_KEYCLOAK_REALM=datapunt-ad-acc
REACT_APP_KEYCLOAK_CLIENT_ID=wonen-zaaksysteem-frontend
+
+############################# Vite #############################
+
+# General
+VITE_APP_TITLE="Amsterdamse Zaak Administratie"
+VITE_APP_TITLE_SHORT="AZA"
+VITE_APP_ENV_SHORT=ACC
+
+# ENTRA-ID
+VITE_OIDC_CLIENT_ID=14c4257b-bcd1-4850-889e-7156c9efe2ec
+VITE_OIDC_REDIRECT_URL=http://localhost:2999
+VITE_OIDC_OBO_SCOPE_BRP=api://c53de0e2-ae05-43aa-9852-31adb9ad7489/wonen
\ No newline at end of file
diff --git a/.env.development b/.env.development
index e801499d8..1ee8f2ce9 100644
--- a/.env.development
+++ b/.env.development
@@ -1,6 +1,3 @@
-# NOTE: Variables need to be prefixed using REACT_APP_:
-# https://create-react-app.dev/docs/adding-custom-environment-variables/
-
# Commented variables are only to be used as examples
# Add any local changes to `.env.development.local` so they won't be committed
@@ -23,5 +20,14 @@ REACT_APP_HOST_TON=https://acc.ton.amsterdam.nl/
# To bypass Keycloak locally
# REACT_APP_API_TOKEN={ generate token at http://localhost:8080/api/v1/swagger/#/oidc-authenticate/oidc_authenticate_create }
-# Translations
-REACT_APP_PAGE_TITLE="Amsterdamse Zaak Administratie | Gemeente Amsterdam"
\ No newline at end of file
+############################# Vite #############################
+
+# General
+VITE_APP_TITLE="Amsterdamse Zaak Administratie"
+VITE_APP_TITLE_SHORT="AZA"
+VITE_APP_ENV_SHORT=LOCAL
+
+# ENTRA-ID
+VITE_OIDC_CLIENT_ID=14c4257b-bcd1-4850-889e-7156c9efe2ec
+VITE_OIDC_REDIRECT_URL=http://localhost:2999
+VITE_OIDC_OBO_SCOPE_BRP=api://c53de0e2-ae05-43aa-9852-31adb9ad7489/wonen
\ No newline at end of file
diff --git a/index.html b/index.html
index f907a7237..62f6f6a74 100644
--- a/index.html
+++ b/index.html
@@ -7,10 +7,10 @@
-
+
-
%REACT_APP_PAGE_TITLE%
+ %VITE_APP_TITLE_SHORT%
diff --git a/package-lock.json b/package-lock.json
index 69a1b033f..62c0f440f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -34,6 +34,7 @@
"final-form": "^4.20.10",
"final-form-arrays": "^3.0.2",
"immer": "^10.1.1",
+ "jwt-decode": "^4.0.0",
"keycloak-js": "^25.0.6",
"lodash.debounce": "^4.0.8",
"lodash.isempty": "^4.4.0",
@@ -43,6 +44,7 @@
"react-dom": "^17.0.2",
"react-final-form": "^6.5.9",
"react-final-form-arrays": "^3.1.4",
+ "react-oidc-context": "^3.2.0",
"react-router-dom": "^6.28.0",
"resize-observer-polyfill": "^1.5.1",
"styled-components": "^5.3.11",
@@ -13128,6 +13130,19 @@
"license": "ISC",
"optional": true
},
+ "node_modules/oidc-client-ts": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.1.0.tgz",
+ "integrity": "sha512-IDopEXjiwjkmJLYZo6BTlvwOtnlSniWZkKZoXforC/oLZHC9wkIxd25Kwtmo5yKFMMVcsp3JY6bhcNJqdYk8+g==",
+ "license": "Apache-2.0",
+ "peer": true,
+ "dependencies": {
+ "jwt-decode": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -13811,6 +13826,19 @@
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"license": "MIT"
},
+ "node_modules/react-oidc-context": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/react-oidc-context/-/react-oidc-context-3.2.0.tgz",
+ "integrity": "sha512-ZLaCRLWV84Cn9pFdsatmblqxLMv0np69GWVXq9RWGqAjppdOGXNIbIxWMByIio0oSCVUwdeqwYRnJme0tjqd8A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "oidc-client-ts": "^3.1.0",
+ "react": ">=16.8.0"
+ }
+ },
"node_modules/react-refresh": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
diff --git a/package.json b/package.json
index 24339e192..4cd156d08 100644
--- a/package.json
+++ b/package.json
@@ -51,6 +51,7 @@
"final-form": "^4.20.10",
"final-form-arrays": "^3.0.2",
"immer": "^10.1.1",
+ "jwt-decode": "^4.0.0",
"keycloak-js": "^25.0.6",
"lodash.debounce": "^4.0.8",
"lodash.isempty": "^4.4.0",
@@ -60,6 +61,7 @@
"react-dom": "^17.0.2",
"react-final-form": "^6.5.9",
"react-final-form-arrays": "^3.1.4",
+ "react-oidc-context": "^3.2.0",
"react-router-dom": "^6.28.0",
"resize-observer-polyfill": "^1.5.1",
"styled-components": "^5.3.11",
diff --git a/src/App.tsx b/src/App.tsx
index b17402d08..0f311658b 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,25 +1,54 @@
-import React from "react"
+import React, { useEffect, useState } from "react"
import { ThemeProvider, GlobalStyle } from "@amsterdam/asc-ui"
import { BrowserRouter } from "react-router-dom"
-import KeycloakProvider from "app/state/auth/keycloak/KeycloakProvider"
-import initializedCallback from "app/state/auth/keycloak/initializedCallback"
+import { hasAuthParams, useAuth } from "react-oidc-context"
import Router from "app/routing/components/Router"
import FlashMessageProvider from "app/state/flashMessages/FlashMessageProvider"
import ApiProvider from "app/state/rest/provider/ApiProvider"
import ValueProvider from "app/state/context/ValueProvider"
-import isLocalDevelopment from "app/state/auth/keycloak/isLocalDevelopment"
import PageTitle from "app/routing/components/PageTitle"
+// import { env } from "app/config/env"
+import { LoadingScreenBasic, FullScreenWrapper } from "app/components/shared/loading"
+const App = () => {
+ const auth = useAuth()
+ const [hasTriedSignin, setHasTriedSignin] = useState(false)
-const App = () => (
-
-
-
-
-
+ useEffect(() => {
+ if (
+ !hasAuthParams() &&
+ !auth.isAuthenticated &&
+ !auth.activeNavigator &&
+ !auth.isLoading &&
+ !hasTriedSignin
+ ) {
+ // TODO: Redirect to the current URL after login
+ // Redirect uri must be change to enable this: http://localhost:2999/* ?
+ // auth.signinRedirect({
+ // redirect_uri: `${ env.VITE_OIDC_REDIRECT_URL }${ window.location.pathname }`
+ // })
+ auth.signinRedirect()
+ setHasTriedSignin(true)
+ }
+ }, [auth, hasTriedSignin])
+
+ if (auth.isLoading) {
+ return
+ }
+
+ if (auth.error) {
+ return Oops... {auth.error.message}
+ }
+
+ if (!auth.isAuthenticated) {
+ return Sorry, het is niet gelukt om in te loggen.
+ }
+
+ return (
+
+
+
+
@@ -29,9 +58,9 @@ const App = () => (
-
-
-
-)
+
+
+ )
+}
export default App
diff --git a/src/app/components/addresses/ResidentsOverview/ResidentsOverview.tsx b/src/app/components/addresses/ResidentsOverview/ResidentsOverview.tsx
index 4f02795bc..11d04222f 100644
--- a/src/app/components/addresses/ResidentsOverview/ResidentsOverview.tsx
+++ b/src/app/components/addresses/ResidentsOverview/ResidentsOverview.tsx
@@ -9,12 +9,13 @@ type Props = {
const ResidentsOverview: React.FC = ({ bagId }) => {
const [data, { isBusy }] = useResidents(bagId)
+ const dataSource = (data || []) as Components.Schemas.Residents
if (isBusy) {
return
}
return (
-
+
)
}
diff --git a/src/app/components/auth/KeycloakValues/KeycloadValues.tsx b/src/app/components/auth/KeycloakValues/KeycloadValues.tsx
deleted file mode 100644
index 1ad384be8..000000000
--- a/src/app/components/auth/KeycloakValues/KeycloadValues.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import type KeycloakTokenParsedExtended from "app/state/auth/keycloak/KeycloakTokenParsedExtended"
-import useKeycloak from "app/state/auth/keycloak/useKeycloak"
-import { DefinitionList } from "@amsterdam/wonen-ui"
-import useValues from "./hooks/useValues"
-
-const KeycloakValues: React.FC = () => {
-
- const keycloak = useKeycloak()
- const tokenParsed = keycloak.tokenParsed as KeycloakTokenParsedExtended
- const values = useValues(keycloak, tokenParsed)
-
- return
-}
-
-export default KeycloakValues
\ No newline at end of file
diff --git a/src/app/components/auth/KeycloakValues/hooks/useValues.ts b/src/app/components/auth/KeycloakValues/hooks/useValues.ts
deleted file mode 100644
index f3b075b5d..000000000
--- a/src/app/components/auth/KeycloakValues/hooks/useValues.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import type KeycloakTokenParsedExtended from "app/state/auth/keycloak/KeycloakTokenParsedExtended"
-import { KeycloakInstance } from "keycloak-js"
-
-export default (keycloak?: KeycloakInstance, token?: KeycloakTokenParsedExtended) => {
- if (keycloak === undefined || token === undefined) return
-
- const values = [
- ["Naam", token.name],
- ["E-mail", token.email],
- ["Gebruikersnaam", token.preferred_username],
- ["Keycloak groepen", keycloak.realmAccess?.roles.join(", ") ?? "-"],
- ["aud", token.aud]
- ]
-
- return Object.fromEntries(values)
-}
\ No newline at end of file
diff --git a/src/app/components/auth/OidcValues/OidcValues.tsx b/src/app/components/auth/OidcValues/OidcValues.tsx
new file mode 100644
index 000000000..1f625b67e
--- /dev/null
+++ b/src/app/components/auth/OidcValues/OidcValues.tsx
@@ -0,0 +1,17 @@
+import { DefinitionList } from "@amsterdam/wonen-ui"
+import { useDecodedToken } from "app/state/auth/oidc/useDecodedToken"
+
+
+const OidcValues: React.FC = () => {
+ const decodedToken = useDecodedToken()
+
+ const values = decodedToken ? {
+ "Voornaam": decodedToken?.given_name,
+ "Achternaam": decodedToken?.family_name,
+ "E-mail": decodedToken?.unique_name
+ } : {}
+
+ return
+}
+
+export default OidcValues
\ No newline at end of file
diff --git a/src/app/components/case/CaseDetails/ChangeHousingCorporation/ChangeHousingCorporation.tsx b/src/app/components/case/CaseDetails/ChangeHousingCorporation/ChangeHousingCorporation.tsx
index af1b4ae31..74f55830d 100644
--- a/src/app/components/case/CaseDetails/ChangeHousingCorporation/ChangeHousingCorporation.tsx
+++ b/src/app/components/case/CaseDetails/ChangeHousingCorporation/ChangeHousingCorporation.tsx
@@ -4,7 +4,7 @@ import { useCorporations, useAddresses, useCase } from "app/state/rest"
import ChangeableItem from "../ChangeableItem/ChangeableItem"
import Modal, { ModalBlock } from "app/components/shared/Modal/Modal"
import ChangeHousingCorporationForm from "./ChangeHousingCorporationForm"
-import SpinnerWrapper from "app/components/shared/SpinnerWrapper/SpinnerWrapper"
+import { SpinnerWrapper } from "app/components/shared/loading"
type Props = {
housingCorporationId?: Components.Schemas.HousingCorporation["id"] | null
diff --git a/src/app/components/case/Documents/DocumentsTable/TableActions/DownloadDocument.tsx b/src/app/components/case/Documents/DocumentsTable/TableActions/DownloadDocument.tsx
index ba025db5e..2359cc476 100644
--- a/src/app/components/case/Documents/DocumentsTable/TableActions/DownloadDocument.tsx
+++ b/src/app/components/case/Documents/DocumentsTable/TableActions/DownloadDocument.tsx
@@ -2,7 +2,7 @@ import { Button, Spinner } from "@amsterdam/asc-ui"
import { Download } from "@amsterdam/asc-assets"
import { makeApiUrl } from "app/state/rest/hooks/utils/apiUrl"
import { useState } from "react"
-import useKeycloak from "app/state/auth/keycloak/useKeycloak"
+import { useAuth } from "react-oidc-context"
type Props = {
record: any
@@ -12,14 +12,15 @@ type Props = {
const DownloadDocument: React.FC = ({ record, size = 20 }) => {
const [loading, setLoading] = useState(false)
const apiUrl = makeApiUrl("documents", record.id, "download")
- const keycloak = useKeycloak()
+ const auth = useAuth()
const downloadFile = async () => {
setLoading(true)
+ const token = auth.user?.id_token
fetch(apiUrl, {
method: "GET",
headers: {
- "Authorization": `Bearer ${ keycloak.token }`
+ "Authorization": `Bearer ${ token }`
}
})
.then((response) => {
diff --git a/src/app/components/layouts/DefaultLayout/DefaultLayout.tsx b/src/app/components/layouts/DefaultLayout/DefaultLayout.tsx
index aab38f26f..c6e028792 100644
--- a/src/app/components/layouts/DefaultLayout/DefaultLayout.tsx
+++ b/src/app/components/layouts/DefaultLayout/DefaultLayout.tsx
@@ -7,6 +7,7 @@ import FlashMessages from "app/components/layouts/FlashMessages/FlashMessages"
import UserInfo from "app/components/shared/UserInfo/UserInfo"
import SkipLinks from "app/components/shared/SkipLinks/SkipLinks"
import BreadCrumbsWrap from "app/components/shared/BreadCrumbs/BreadCrumbsWrap"
+import { env } from "app/config/env"
type Props = {
showSearchButton?: boolean
@@ -43,7 +44,7 @@ const DefaultLayout: React.FC = ({ showSearchButton = true, children }) =
diff --git a/src/app/components/shared/Modal/ConfirmModal.tsx b/src/app/components/shared/Modal/ConfirmModal.tsx
index 64b893b4e..48b007bf5 100644
--- a/src/app/components/shared/Modal/ConfirmModal.tsx
+++ b/src/app/components/shared/Modal/ConfirmModal.tsx
@@ -3,7 +3,7 @@ import styled from "styled-components"
import { Button, Paragraph } from "@amsterdam/asc-ui"
import Modal, { ModalBlock } from "./Modal"
-import SpinnerButton from "app/components/shared/SpinnerButton/SpinnerButton"
+import { SpinnerButton } from "app/components/shared/loading"
export type Props = {
title: string
diff --git a/src/app/components/shared/UserInfo/UserDisplay.tsx b/src/app/components/shared/UserInfo/UserDisplay.tsx
index e0104b29b..19f0b6a29 100644
--- a/src/app/components/shared/UserInfo/UserDisplay.tsx
+++ b/src/app/components/shared/UserInfo/UserDisplay.tsx
@@ -7,7 +7,7 @@ type Props = {
onClick: () => void
}
-const Div = styled.div`
+const UserWrapper = styled.div`
padding: 12px 0 0 16px;
vertical-align: middle;
height: 54px;
@@ -39,20 +39,25 @@ const StyledMenuButton = styled(MenuButton)`
padding: 12px 16px 9px;
`
-const UserDisplay: React.FC = ({ name, onClick }) =>
+const UserDisplay: React.FC = ({ name, onClick }) => (
<>
- { name &&
-
- }
+ {name && (
+
+
+
+
+ {name}
+
+ )}
}
+ tabIndex={0}
+ onClick={onClick}
+ iconLeft={ }
title="Uitloggen"
- iconSize={ 24 }
- >Uitloggen
+ iconSize={24}
+ >
+ Uitloggen
+
>
+)
export default UserDisplay
diff --git a/src/app/components/shared/UserInfo/UserInfo.tsx b/src/app/components/shared/UserInfo/UserInfo.tsx
index 24f6a4928..b965f7b30 100644
--- a/src/app/components/shared/UserInfo/UserInfo.tsx
+++ b/src/app/components/shared/UserInfo/UserInfo.tsx
@@ -1,20 +1,23 @@
-
-import type KeycloakTokenParsedExtended from "app/state/auth/keycloak/KeycloakTokenParsedExtended"
-
-import useKeycloak from "app/state/auth/keycloak/useKeycloak"
+import { useAuth } from "react-oidc-context"
import UserDisplay from "./UserDisplay"
+import { useDecodedToken } from "app/state/auth/oidc/useDecodedToken"
type Props = {
showAsListItem?: boolean
-}
+};
const UserInfo: React.FC = ({ showAsListItem = false }) => {
- const { tokenParsed, logout } = useKeycloak()
- const name = (tokenParsed as KeycloakTokenParsedExtended)?.name
- const userDisplay =
+ const auth = useAuth()
+ const decodedToken = useDecodedToken()
+
+ const userDisplay = (
+
+ )
- return showAsListItem ?
- { userDisplay } :
- { userDisplay }
+ return showAsListItem ? (
+ {userDisplay}
+ ) : (
+ {userDisplay}
+ )
}
export default UserInfo
diff --git a/src/app/components/shared/PageSpinner/PageSpinner.tsx b/src/app/components/shared/loading/LoadingScreen.tsx
similarity index 82%
rename from src/app/components/shared/PageSpinner/PageSpinner.tsx
rename to src/app/components/shared/loading/LoadingScreen.tsx
index 1781175dc..7840fe015 100644
--- a/src/app/components/shared/PageSpinner/PageSpinner.tsx
+++ b/src/app/components/shared/loading/LoadingScreen.tsx
@@ -9,7 +9,8 @@ const Wrap = styled.div`
justify-content: center;
height: 400px;
`
-const PageSpinner: React.FC = () => (
+
+export const LoadingScreen: React.FC = () => (
@@ -17,4 +18,4 @@ const PageSpinner: React.FC = () => (
)
-export default PageSpinner
\ No newline at end of file
+export default LoadingScreen
diff --git a/src/app/components/shared/loading/LoadingScreenAmsterdam.tsx b/src/app/components/shared/loading/LoadingScreenAmsterdam.tsx
new file mode 100644
index 000000000..156fd4cef
--- /dev/null
+++ b/src/app/components/shared/loading/LoadingScreenAmsterdam.tsx
@@ -0,0 +1,53 @@
+import React from "react"
+import styled, { keyframes } from "styled-components"
+
+const CenterWrapper = styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+ height: 100vh;
+ width: 100vw;
+`
+
+const spin = keyframes`
+ 100% {
+ transform: rotate(calc(var(--s, 1) * 1turn));
+ }
+`
+
+const Cross = styled.div<{ duration: number, delay: number }>`
+ width: 100px;
+ aspect-ratio: 1;
+ padding: 10px;
+ box-sizing: border-box;
+ display: grid;
+ background: #fff;
+ filter: blur(4px) contrast(10);
+ mix-blend-mode: darken;
+
+ &::before,
+ &::after {
+ content: "";
+ grid-area: 1 / 1;
+ margin: 30px 0;
+ border-radius: 100px;
+ background: #ec0000;
+ animation: ${ spin } ${ ({ duration }) => duration }s infinite linear;
+ animation-delay: ${ ({ delay }) => delay }s;
+ }
+
+ &::after {
+ --s: -1;
+ }
+`
+
+export const LoadingScreenAmsterdam: React.FC = () => (
+
+
+
+
+
+)
+
+export default LoadingScreenAmsterdam
diff --git a/src/app/components/shared/loading/LoadingScreenBasic.tsx b/src/app/components/shared/loading/LoadingScreenBasic.tsx
new file mode 100644
index 000000000..831b2a4a6
--- /dev/null
+++ b/src/app/components/shared/loading/LoadingScreenBasic.tsx
@@ -0,0 +1,40 @@
+import styled, { keyframes } from "styled-components"
+
+export const FullScreenWrapper = styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100vh;
+ width: 100vw;
+`
+
+const spin = keyframes`
+ 100% {
+ transform: rotate(1turn);
+ }
+`
+
+export const BasicSpinner = styled.div<{ color?: string, size?: number }>`
+ width: ${ ({ size }) => size ?? 50 }px;
+ aspect-ratio: 1;
+ border-radius: 50%;
+ background:
+ radial-gradient(
+ farthest-side,
+ ${ ({ color }) => color ?? "#ffa516" } 94%,
+ #0000
+ )
+ top/8px 8px no-repeat,
+ conic-gradient(#0000 30%, ${ ({ color }) => color ?? "#ffa516" });
+ -webkit-mask: radial-gradient(farthest-side, #0000 calc(100% - 8px), #000 0);
+ animation: ${ spin } 1s infinite linear;
+`
+
+export const LoadingScreenBasic: React.FC<{
+ color?: string
+ size?: number
+}> = ({ color = "#ec0000", size = 60 }) => (
+
+
+
+)
diff --git a/src/app/components/shared/SpinnerButton/SpinnerButton.tsx b/src/app/components/shared/loading/SpinnerButton.tsx
similarity index 90%
rename from src/app/components/shared/SpinnerButton/SpinnerButton.tsx
rename to src/app/components/shared/loading/SpinnerButton.tsx
index 6bcc339a7..5d1021cb0 100644
--- a/src/app/components/shared/SpinnerButton/SpinnerButton.tsx
+++ b/src/app/components/shared/loading/SpinnerButton.tsx
@@ -7,7 +7,7 @@ type Props = Omit, "onClick"> & {
onClick: () => Promise
}
-const SpinnerButton: React.FC = ({ onClick, ...restProps }) => {
+export const SpinnerButton: React.FC = ({ onClick, ...restProps }) => {
const isMounted = useIsMounted()
const [isSpinning, setIsSpinning] = useState(false)
diff --git a/src/app/components/shared/SpinnerWrapper/SpinnerWrapper.tsx b/src/app/components/shared/loading/SpinnerWrapper.tsx
similarity index 87%
rename from src/app/components/shared/SpinnerWrapper/SpinnerWrapper.tsx
rename to src/app/components/shared/loading/SpinnerWrapper.tsx
index 78be37a85..4f6832994 100644
--- a/src/app/components/shared/SpinnerWrapper/SpinnerWrapper.tsx
+++ b/src/app/components/shared/loading/SpinnerWrapper.tsx
@@ -20,7 +20,7 @@ const SpinnerContainer = styled.div`
`
-const SpinnerWrapper: React.FC = ({ spinning = true, children }) => (
+export const SpinnerWrapper: React.FC = ({ spinning = true, children }) => (
{ spinning && (
diff --git a/src/app/components/shared/loading/index.ts b/src/app/components/shared/loading/index.ts
new file mode 100644
index 000000000..1b50b9146
--- /dev/null
+++ b/src/app/components/shared/loading/index.ts
@@ -0,0 +1,5 @@
+export * from "./LoadingScreenBasic"
+export * from "./LoadingScreen"
+export * from "./LoadingScreenAmsterdam"
+export * from "./SpinnerButton"
+export * from "./SpinnerWrapper"
diff --git a/src/app/components/shared/navigation/DefaultNavigation.tsx b/src/app/components/shared/navigation/DefaultNavigation.tsx
index b9b0647c9..6d79a188a 100644
--- a/src/app/components/shared/navigation/DefaultNavigation.tsx
+++ b/src/app/components/shared/navigation/DefaultNavigation.tsx
@@ -1,8 +1,8 @@
+import { useAuth } from "react-oidc-context"
import { MenuInline, Button, MenuToggle, Hidden } from "@amsterdam/asc-ui"
import styled from "styled-components"
import ButtonLink from "app/components/shared/ButtonLink/ButtonLink"
import to from "app/routing/utils/to"
-import useKeycloak from "app/state/auth/keycloak/useKeycloak"
import { Search, Help } from "app/components/shared/Icons"
import MenuItems from "app/components/shared/navigation/MenuItems"
import UserInfo from "../UserInfo/UserInfo"
@@ -16,7 +16,8 @@ const IconButton = styled(Button)`
`
const DefaultNavigation: React.FC = ({ showSearchButton }) => {
- const { token } = useKeycloak()
+ const auth = useAuth()
+ const token = auth.user?.access_token
if (!token) return null
diff --git a/src/app/pages/auth/AuthPage.tsx b/src/app/pages/auth/AuthPage.tsx
index 6b0e402df..da8ed9ba9 100644
--- a/src/app/pages/auth/AuthPage.tsx
+++ b/src/app/pages/auth/AuthPage.tsx
@@ -1,15 +1,14 @@
import { Heading } from "@amsterdam/asc-ui"
-
import DefaultLayout from "app/components/layouts/DefaultLayout/DefaultLayout"
import NotAuthorizedAlert from "app/components/auth/NotAuthorizedAlert/NotAuthorizedAlert"
-import KeycloakValues from "app/components/auth/KeycloakValues/KeycloadValues"
+import OidcValues from "app/components/auth/OidcValues/OidcValues"
const AuthPage: React.FC = () => (
- Keycloak gebruiker
+ Microsoft Entra-ID gebruiker
-
+
)
diff --git a/src/app/pages/cases/details/DetailsPage.tsx b/src/app/pages/cases/details/DetailsPage.tsx
index 43a27c936..cc3ca6b8b 100644
--- a/src/app/pages/cases/details/DetailsPage.tsx
+++ b/src/app/pages/cases/details/DetailsPage.tsx
@@ -13,7 +13,7 @@ import DetailHeaderByCaseId from "app/components/shared/DetailHeader/DetailHeade
import { Column } from "app/components/layouts/Grid"
import CaseStatus from "app/components/case/CaseStatus/CaseStatus"
import useExistingCase from "./hooks/useExistingCase"
-import PageSpinner from "app/components/shared/PageSpinner/PageSpinner"
+import { LoadingScreen } from "app/components/shared/loading"
import CaseNuisanceAlert from "app/components/case/CaseNuisanceAlert/CaseNuisanceAlert"
import useHasPermission, { SENSITIVE_CASE_PERMISSION } from "app/state/rest/custom/usePermissions/useHasPermission"
import NotAuthorizedPage from "app/pages/auth/NotAuthorizedPage"
@@ -41,7 +41,7 @@ const DetailsPage: React.FC = () => {
const [isDocumentsTabActive, setIsDocumentsTabActive] = useState(false)
if (showSpinner) {
- return
+ return
}
if (exists && !isAuthorized) {
return
diff --git a/src/app/routing/components/PageTitle.tsx b/src/app/routing/components/PageTitle.tsx
index b5a1df728..b1750a904 100644
--- a/src/app/routing/components/PageTitle.tsx
+++ b/src/app/routing/components/PageTitle.tsx
@@ -3,7 +3,7 @@ import find from "../utils/find"
import routes from "app/routing/routes"
import { env } from "app/config/env"
-const PAGE_TITLE = env.REACT_APP_PAGE_TITLE ?? ""
+const PAGE_TITLE = env.VITE_APP_TITLE_SHORT ?? ""
const setPageTitle = () => {
const route = find(routes, window.location.pathname)
diff --git a/src/app/routing/components/ProtectedPage.tsx b/src/app/routing/components/ProtectedPage.tsx
index 19ee5fa8b..6d28bdf58 100644
--- a/src/app/routing/components/ProtectedPage.tsx
+++ b/src/app/routing/components/ProtectedPage.tsx
@@ -1,4 +1,4 @@
-import useKeycloak from "app/state/auth/keycloak/useKeycloak"
+import { useAuth } from "react-oidc-context"
import AuthorizedPage from "./AuthorizedPage"
type Props = {
@@ -10,7 +10,8 @@ type Props = {
* The user needs to be logged on to visit this route
*/
const ProtectedPage: React.FC = (props) => {
- const { token } = useKeycloak()
+ const auth = useAuth()
+ const token = auth.user?.access_token
if (token === undefined) return null
diff --git a/src/app/state/auth/keycloak/KeycloakProvider.tsx b/src/app/state/auth/keycloak/KeycloakProvider.tsx
deleted file mode 100644
index 8214546e0..000000000
--- a/src/app/state/auth/keycloak/KeycloakProvider.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import { createContext, useState, useEffect } from "react"
-import Keycloak from "keycloak-js"
-import { keycloak } from "./keycloak"
-import options from "./options"
-
-export type Context = {
- isInitialized: boolean
- isAuthenticated: boolean
- keycloak: Keycloak
-}
-export const KeycloakContext = createContext(undefined)
-
-type Props = {
- shouldInitialize?: boolean
- initializedCallback?: (keycloak: Keycloak, isAuthenticated: boolean) => Promise
-}
-
-const KeycloakProvider: React.FC = ({
- shouldInitialize = true, initializedCallback, children
-}) => {
- const [isInitialized, setIsInitialized] = useState(false)
- const [isAuthenticated, setIsAuthenticated] = useState(false)
-
- useEffect(() => {
- if (shouldInitialize === false) {
- return
- }
- if (isAuthenticated && isInitialized) {
- return
- }
- (async () => {
- try {
- const isAuthenticated = await keycloak.init(options)
- setIsInitialized(true)
- setIsAuthenticated(isAuthenticated)
- if (initializedCallback !== undefined) await initializedCallback(keycloak, isAuthenticated)
- } catch (err) {
- console.error("Keycloak failed to initialize")
- console.error(err)
- }
- })()
- // eslint-disable-next-line
- }, [initializedCallback, shouldInitialize])
-
- const value = {
- isInitialized,
- isAuthenticated,
- keycloak
- }
-
- return (
-
- { children }
-
- )
-}
-export default KeycloakProvider
diff --git a/src/app/state/auth/keycloak/KeycloakTokenParsedExtended.ts b/src/app/state/auth/keycloak/KeycloakTokenParsedExtended.ts
deleted file mode 100644
index 21f3097b8..000000000
--- a/src/app/state/auth/keycloak/KeycloakTokenParsedExtended.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { KeycloakTokenParsed } from "keycloak-js"
-type KeycloakTokenParsedExtended =
- KeycloakTokenParsed &
- {
- name: string
- email: string
- preferred_username: string
- }
-export default KeycloakTokenParsedExtended
diff --git a/src/app/state/auth/keycloak/initializedCallback.ts b/src/app/state/auth/keycloak/initializedCallback.ts
deleted file mode 100644
index 1f68bc7ed..000000000
--- a/src/app/state/auth/keycloak/initializedCallback.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import Keyloack from "keycloak-js"
-import { makeApiUrl } from "app/state/rest/hooks/utils/apiUrl"
-import createAuthHeaders from "app/state/rest/hooks/utils/createAuthHeaders"
-
-export default async (keycloak: Keyloack, isAuthenticated: boolean) => {
- if (isAuthenticated === false) return
- const response = await fetch(makeApiUrl("is-authorized"), {
- headers: {
- ...createAuthHeaders(keycloak.token ?? ""),
- "Content-Type": "application/json"
- }
- })
- const { is_authorized } = await response.json()
- if (is_authorized === false) {
- // This is not working outside the routing wrapper.
- // navigateTo("/auth")
- }
-}
diff --git a/src/app/state/auth/keycloak/isLocalDevelopment.ts b/src/app/state/auth/keycloak/isLocalDevelopment.ts
deleted file mode 100644
index f263c49a3..000000000
--- a/src/app/state/auth/keycloak/isLocalDevelopment.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { env } from "app/config/env"
-
-export default env.NODE_ENV === "development" && env.REACT_APP_API_TOKEN !== undefined
diff --git a/src/app/state/auth/keycloak/keycloak.mock.ts b/src/app/state/auth/keycloak/keycloak.mock.ts
deleted file mode 100644
index 30ce25e0e..000000000
--- a/src/app/state/auth/keycloak/keycloak.mock.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { env } from "app/config/env"
-
-export default {
- init: async () => {},
- updateToken: async () => {},
- logout: () => {},
- token: env.REACT_APP_API_TOKEN ?? ""
-}
diff --git a/src/app/state/auth/keycloak/keycloak.ts b/src/app/state/auth/keycloak/keycloak.ts
deleted file mode 100644
index 9d55f3833..000000000
--- a/src/app/state/auth/keycloak/keycloak.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import settings from "./settings"
-import Keycloak from "keycloak-js"
-import keycloakMock from "./keycloak.mock"
-import isLocalDevelopment from "./isLocalDevelopment"
-import { env } from "app/config/env"
-
-export const keycloak = env.NODE_ENV !== "test" && isLocalDevelopment === false ? new (Keycloak as any)(settings) : keycloakMock
-
-if (env.NODE_ENV === "development") {
- (window as any).keycloak = keycloak
-}
diff --git a/src/app/state/auth/keycloak/options.ts b/src/app/state/auth/keycloak/options.ts
deleted file mode 100644
index 5e606a92f..000000000
--- a/src/app/state/auth/keycloak/options.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export default {
- onLoad: "login-required",
- checkLoginIframe: false
-}
diff --git a/src/app/state/auth/keycloak/settings.ts b/src/app/state/auth/keycloak/settings.ts
deleted file mode 100644
index edaf3473a..000000000
--- a/src/app/state/auth/keycloak/settings.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { env } from "app/config/env"
-
-export default {
- "url": env.REACT_APP_AUTH_URL ?? "https://iam.amsterdam.nl/auth/",
- "realm": env.REACT_APP_KEYCLOAK_REALM ?? "",
- "ssl-required": "external",
- "resource": env.REACT_APP_KEYCLOAK_CLIENT_ID ?? "wonen-woon-o-azure",
- "public-client": true,
- "confidential-port": 0,
- "clientId": env.REACT_APP_KEYCLOAK_CLIENT_ID ?? "wonen-woon-o-azure"
-}
diff --git a/src/app/state/auth/keycloak/useKeycloak.ts b/src/app/state/auth/keycloak/useKeycloak.ts
deleted file mode 100644
index 486c41638..000000000
--- a/src/app/state/auth/keycloak/useKeycloak.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { useContext } from "react"
-import { KeycloakContext } from "./KeycloakProvider"
-
-export default () => {
- const context = useContext(KeycloakContext)
- if (context === undefined) throw new Error("KeycloakContext was not set")
- return context.keycloak
-}
diff --git a/src/app/state/auth/keycloak/README.md b/src/app/state/auth/oidc/README.md
similarity index 87%
rename from src/app/state/auth/keycloak/README.md
rename to src/app/state/auth/oidc/README.md
index 757188519..62240075c 100644
--- a/src/app/state/auth/keycloak/README.md
+++ b/src/app/state/auth/oidc/README.md
@@ -1,9 +1,10 @@
-# Keycloak, React, TypeScript
+# Entra-ID, React, TypeScript
+
+
## Implement
- Add `` to [App.tsx](https://github.com/Amsterdam/zaken-frontend/blob/main/src/App.tsx)
- Optionally add a `initializedCallback` function
-- Use `useKeycloak` hook in your components
## Local development
- Make sure to set `LOCAL_DEVELOPMENT_AUTHENTICATION=True` in `zaken-backend`
diff --git a/src/app/state/auth/oidc/oidcConfig.ts b/src/app/state/auth/oidc/oidcConfig.ts
new file mode 100644
index 000000000..26d863a22
--- /dev/null
+++ b/src/app/state/auth/oidc/oidcConfig.ts
@@ -0,0 +1,31 @@
+import { env } from "app/config/env"
+
+/*
+ ** You must provide an implementation of onSigninCallback to oidcConfig to remove the payload from the URL upon successful login.
+ ** Otherwise if you refresh the page and the payload is still there, signinSilent - which handles renewing your token - won't work.
+ */
+
+export const onSigninCallback = () => {
+ window.history.replaceState({}, document.title, window.location.pathname)
+}
+
+export const oidcConfig = {
+ authority:
+ "https://login.microsoftonline.com/72fca1b1-2c2e-4376-a445-294d80196804",
+ client_id: `${ env.VITE_OIDC_CLIENT_ID }`,
+ redirect_uri: `${ env.VITE_OIDC_REDIRECT_URL }`,
+ response_type: "code",
+ scope: `openid email api://${ env.VITE_OIDC_CLIENT_ID }/user_impersonation`,
+ post_logout_redirect_uri: `${ env.VITE_OIDC_REDIRECT_URL }`,
+ metadata: {
+ issuer:
+ "https://login.microsoftonline.com/72fca1b1-2c2e-4376-a445-294d80196804/v2.0",
+ authorization_endpoint:
+ "https://login.microsoftonline.com/72fca1b1-2c2e-4376-a445-294d80196804/oauth2/v2.0/authorize",
+ token_endpoint:
+ "https://login.microsoftonline.com/72fca1b1-2c2e-4376-a445-294d80196804/oauth2/v2.0/token",
+ end_session_endpoint:
+ "https://login.microsoftonline.com/common/oauth2/v2.0/logout"
+ },
+ onSigninCallback
+}
diff --git a/src/app/state/auth/oidc/useDecodedToken.ts b/src/app/state/auth/oidc/useDecodedToken.ts
new file mode 100644
index 000000000..3994c48c0
--- /dev/null
+++ b/src/app/state/auth/oidc/useDecodedToken.ts
@@ -0,0 +1,20 @@
+import { useAuth } from "react-oidc-context"
+import { jwtDecode } from "jwt-decode"
+
+export type DecodedToken = {
+ given_name: string // firstname
+ family_name: string // lastname
+ name: string // lastname, firstname
+ unique_name: string // email
+ [key: string]: number | string | string[]
+}
+
+export const useDecodedToken = (): DecodedToken | undefined => {
+ const auth = useAuth()
+ const token = auth.user?.access_token
+
+ if (!token) return
+
+ const decoded = jwtDecode(token)
+ return decoded
+}
diff --git a/src/app/state/rest/addresses.ts b/src/app/state/rest/addresses.ts
index ee7c35148..5e738d88b 100644
--- a/src/app/state/rest/addresses.ts
+++ b/src/app/state/rest/addresses.ts
@@ -52,17 +52,6 @@ export const useMeldingen = (bagId: string) => {
})
}
-export const useResidents = (bagId: Components.Schemas.Address["bag_id"], options?: Options) => {
- const handleError = useErrorHandler()
- return useApiRequest({
- ...options,
- url: makeApiUrl("addresses", bagId, "residents"),
- groupName: "addresses",
- handleError,
- isProtected: true
- })
-}
-
export const useCorporations = (options?: Options) => {
const handleError = useErrorHandler()
return useApiRequest({
diff --git a/src/app/state/rest/hooks/useApiCache.ts b/src/app/state/rest/hooks/useApiCache.ts
index c2034d8c6..aff1f753c 100644
--- a/src/app/state/rest/hooks/useApiCache.ts
+++ b/src/app/state/rest/hooks/useApiCache.ts
@@ -52,13 +52,13 @@ const reducer = (state: State, action: Action) => {
.reduce((acc, [key, val]) => ({
...acc,
[key]: { valid: false, value: val.value, errors: [] }
- }), {})
+ }), {} as State)
}
}
}
export const useApiCache = () => {
- const [ cache, dispatch ] = useReducer(reducer, {})
+ const [ cache, dispatch ] = useReducer(reducer, {} as State)
const getCacheItem = useCallback((key: string) => cache[key], [ cache ])
const setCacheItem = useCallback((key: string, value: any) => dispatch({ type: "SET_ITEM", key, value }), [ dispatch ])
diff --git a/src/app/state/rest/hooks/useApiRequest.test.tsx b/src/app/state/rest/hooks/useApiRequest.test.tsx
deleted file mode 100644
index ace786dd3..000000000
--- a/src/app/state/rest/hooks/useApiRequest.test.tsx
+++ /dev/null
@@ -1,166 +0,0 @@
-
-import nock from "nock"
-import { BrowserRouter } from "react-router-dom"
-import { renderHook, act } from "@testing-library/react-hooks"
-import useApiRequest from "./useApiRequest"
-import ApiProvider from "../provider/ApiProvider"
-import KeycloakProvider from "app/state/auth/keycloak/KeycloakProvider"
-
-type Pet = {
- name: string
- type: string
-}
-
-const Wrapper: React.FC = ({ children }) => (
-
-
-
- { children }
-
-
-
-)
-
-describe("useApiRequest", () => {
- it("should perform a GET request on mount", async () => {
- const usePet = () => useApiRequest({ url: "http://localhost/pet", groupName: "cases" })
-
- // Define nock scope:
- const scope = nock("http://localhost")
- .get("/pet")
- .reply(200, { name: "Fifi", type: "dog" })
-
- const { result, waitForNextUpdate } = renderHook(usePet, { wrapper: Wrapper })
-
- // Busy... no results yet.
- expect(result.current[1].isBusy).toEqual(true)
- expect(result.current[0]).toEqual(undefined)
-
- // // Make API respond:
- // await act(() => waitForNextUpdate())
- // await act(() => waitForNextUpdate())
- // Make API respond:
- await act(async () => {
- await waitForNextUpdate()
- })
- await act(async () => {
- await waitForNextUpdate()
- })
-
- // not busy anymore... results are in!
- expect(result.current[1].isBusy).toEqual(false)
- expect(result.current[0]).toEqual({ name: "Fifi", type: "dog" })
-
- expect(scope.isDone()).toEqual(true) // <- all scoped endpoints are called
- })
-
- it("should NOT perform duplicate requests", async () => {
- const usePet = () => useApiRequest({ url: "http://localhost/pet", groupName: "cases" })
-
- const scope = nock("http://localhost")
- .get("/pet")
- .once() // <- Make nock return ony once! (Will throw error when called twice)
- .reply(200, { name: "Fifi", type: "dog" })
-
- const useTwoHooks = () => ({
- first: usePet(),
- second: usePet()
- })
-
- const { result, waitForNextUpdate } = renderHook(useTwoHooks, { wrapper: Wrapper })
-
- // Busy...
- expect(result.current.first[1].isBusy).toEqual(true)
- expect(result.current.second[1].isBusy).toEqual(true)
- // no results yet....
- expect(result.current.first[0]).toEqual(undefined)
- expect(result.current.second[0]).toEqual(undefined)
-
- await act(() => waitForNextUpdate())
- await act(() => waitForNextUpdate())
-
- // not busy anymore
- expect(result.current.first[1].isBusy).toEqual(false)
- expect(result.current.second[1].isBusy).toEqual(false)
- // Results are in!
- expect(result.current.first[0]).toEqual({ name: "Fifi", type: "dog" })
- expect(result.current.second[0]).toEqual({ name: "Fifi", type: "dog" })
-
- expect(scope.isDone()).toEqual(true) // <- all scoped endpoints are called
- })
-
- test.each([
- [ "POST",
- (scope: nock.Scope) => scope.post("/pet").reply(200),
- (hook: any) => hook[1].execPost({ name: "popo" })
- ],
- [ "PUT",
- (scope: nock.Scope) => scope.put("/pet").reply(200),
- (hook: any) => hook[1].execPut({ name: "popo" })
- ],
- [ "PATCH",
- (scope: nock.Scope) => scope.patch("/pet").reply(200),
- (hook: any) => hook[1].execPatch({ name: "popo" })
- ],
- [ "DELETE",
- (scope: nock.Scope) => scope.delete("/pet").reply(200),
- (hook: any) => hook[1].execDelete()
- ]
- ])("should re-execute a GET after a %s", async (method, prepareScope, exec) => {
- const usePet = () => useApiRequest({ url: "http://localhost/pet", groupName: "cases" })
-
- const scope = nock("http://localhost")
- .get("/pet")
- .reply(200, { name: "Fifi", type: "dog" })
- .get("/pet")
- .reply(200, { name: "Popo", type: "dog" })
-
- prepareScope(scope)
-
- const onSuccess = jest.fn()
- const { result, waitForNextUpdate } = renderHook(usePet, { wrapper: Wrapper })
- await act(() => waitForNextUpdate())
- await act(() => waitForNextUpdate())
-
- // On mount, "Fifi" should be fetched
- expect(result.current[0]).toEqual({ name: "Fifi", type: "dog" })
-
- await act(async () => {
- await exec(result.current).then(onSuccess)
- return waitForNextUpdate()
- })
-
- // Cache was cleared. Data should be undefined now:
- expect(result.current[0]).toEqual(undefined)
-
- // New fetch should happen
- await act(() => waitForNextUpdate())
- expect(result.current[0]).toEqual({ name: "Popo", type: "dog" })
-
- expect(onSuccess).toHaveBeenCalled()
- expect(scope.isDone()).toEqual(true) // <- all scoped endpoints are called
- })
-
- it("should call the error handler when a error occurs", async () => {
- const handleError = jest.fn()
- const usePet = () => useApiRequest({ url: "http://localhost/pet", groupName: "cases", handleError })
-
- const scope = nock("http://localhost")
- .get("/pet")
- .reply(500, { detail: "S.O.S." })
-
- const { waitForNextUpdate } = renderHook(usePet, { wrapper: Wrapper })
- await act(() => waitForNextUpdate())
- await act(() => waitForNextUpdate())
-
- expect(handleError).toHaveBeenCalledWith(expect.objectContaining({
- message: "Request failed with status code 500",
- response: expect.objectContaining({
- status: 500,
- data: { detail: "S.O.S." }
- })
- }))
-
- expect(scope.isDone()).toEqual(true) // <- all scoped endpoints are called
- })
-})
diff --git a/src/app/state/rest/hooks/useProtectedRequest.ts b/src/app/state/rest/hooks/useProtectedRequest.ts
index c0b2b639f..3bbcd4e83 100644
--- a/src/app/state/rest/hooks/useProtectedRequest.ts
+++ b/src/app/state/rest/hooks/useProtectedRequest.ts
@@ -1,13 +1,12 @@
import { useCallback } from "react"
-
-import useKeycloak from "app/state/auth/keycloak/useKeycloak"
+import { useAuth } from "react-oidc-context"
import useRequest, { Method } from "./useRequest"
import useNavigation from "app/routing/useNavigation"
import { RequestError } from "./useRequestWrapper"
export default () => {
- const keycloak = useKeycloak()
+ const auth = useAuth()
const request = useRequest()
const { navigateTo } = useNavigation()
@@ -15,9 +14,9 @@ export default () => {
async (method: Method, url: string, data?: unknown, additionalHeaders = {}) => {
try {
// Update the access token when it expires in less than 30 seconds
- await keycloak.updateToken(30)
+ const token = auth.user?.access_token
const headers = {
- Authorization: `Bearer ${ keycloak.token }`,
+ Authorization: `Bearer ${ token }`,
...additionalHeaders
}
const response = await request(
@@ -29,12 +28,12 @@ export default () => {
return response
} catch (error) {
switch ((error as RequestError)?.response?.status) {
- case 401: keycloak.logout(); break
+ // case 401: auth.signoutRedirect(); break
case 403: navigateTo("/auth"); break
}
if (error !== undefined) throw error
}
},
- [keycloak, request, navigateTo]
+ [auth.user?.access_token, request, navigateTo]
)
}
diff --git a/src/app/state/rest/index.ts b/src/app/state/rest/index.ts
index c89c3e4c3..1a84905a8 100644
--- a/src/app/state/rest/index.ts
+++ b/src/app/state/rest/index.ts
@@ -1,19 +1,19 @@
export type ApiGroup =
| "addresses"
| "auth"
- | "users"
| "case"
| "cases"
| "dataPunt"
| "fines"
+ | "housingCorporations"
+ | "listings"
+ | "permissions"
| "permits"
- | "themes"
- | "supportContacts"
| "roles"
- | "permissions"
- | "listings"
+ | "supportContacts"
| "task"
- | "housingCorporations"
+ | "themes"
+ | "users"
export type Options = {
keepUsingInvalidCache?: boolean
@@ -23,19 +23,21 @@ export type Options = {
export * from "./addresses"
export * from "./auth"
-export * from "./users"
-export * from "./cases"
-export * from "./tasks"
+export * from "./bagPdok"
+export * from "./benkAgg"
export * from "./case"
+export * from "./cases"
export * from "./dataPunt"
export * from "./fines"
export * from "./help"
-export * from "./schedules"
-export * from "./themes"
-export * from "./processes"
-export * from "./roles"
-export * from "./permissions"
export * from "./listing"
+export * from "./permissions"
+export * from "./processes"
export * from "./reasons"
-export * from "./bagPdok"
-export * from "./benkAgg"
+export * from "./residents"
+export * from "./roles"
+export * from "./schedules"
+export * from "./tasks"
+export * from "./themes"
+export * from "./tokenOboService"
+export * from "./users"
diff --git a/src/app/state/rest/residents.ts b/src/app/state/rest/residents.ts
new file mode 100644
index 000000000..607220e4c
--- /dev/null
+++ b/src/app/state/rest/residents.ts
@@ -0,0 +1,50 @@
+import { useCallback, useEffect, useState } from "react"
+import axios from "axios"
+import { useAuth } from "react-oidc-context"
+import { makeApiUrl } from "./hooks/utils/apiUrl"
+import { useOboToken } from "./tokenOboService"
+import { env } from "app/config/env"
+
+export const useResidents = (bagId: Components.Schemas.Address["bag_id"]) => {
+ const auth = useAuth()
+ const [isBusy, setIsBusy] = useState(false)
+ const { fetchOboToken } = useOboToken(env.VITE_OIDC_OBO_SCOPE_BRP)
+ const [data, setData] = useState(
+ undefined
+ )
+
+ const url = makeApiUrl("addresses", bagId, "residents")
+
+ const fetchResidents = useCallback(
+ (oboToken: string) => {
+ setIsBusy(true)
+ axios
+ .post(
+ url,
+ {
+ obo_access_token: oboToken
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${ auth.user?.access_token }`
+ }
+ }
+ )
+ .then((response) => {
+ setData(response.data)
+ })
+ .finally(() => {
+ setIsBusy(false)
+ })
+ },
+ [url, auth.user?.access_token]
+ )
+
+ useEffect(() => {
+ fetchOboToken().then((oboToken) => {
+ fetchResidents(oboToken)
+ })
+ }, [fetchOboToken, fetchResidents])
+
+ return [ data, { isBusy }] as const
+}
diff --git a/src/app/state/rest/tokenOboService.ts b/src/app/state/rest/tokenOboService.ts
new file mode 100644
index 000000000..654831aa8
--- /dev/null
+++ b/src/app/state/rest/tokenOboService.ts
@@ -0,0 +1,44 @@
+import { useCallback, useMemo, useState } from "react"
+import axios from "axios"
+import { useAuth } from "react-oidc-context"
+import { oidcConfig } from "app/state/auth/oidc/oidcConfig"
+
+const url = oidcConfig.metadata.token_endpoint
+const headers = {
+ "Content-Type": "application/x-www-form-urlencoded"
+}
+
+export const useOboToken = (scope: string) => {
+ const auth = useAuth()
+ const [loading, setLoading] = useState(false)
+ const [accessToken, setAccessToken] = useState(null)
+
+ const data = useMemo(
+ () => ({
+ grant_type: "refresh_token",
+ client_id: oidcConfig.client_id,
+ refresh_token: auth.user?.refresh_token,
+ scope
+ }),
+ [auth.user?.refresh_token, scope]
+ )
+
+ const fetchOboToken = useCallback(() => {
+ setLoading(true)
+ return axios
+ .post(url, data, { headers })
+ .then((response) => {
+ const accessToken = response.data.access_token
+ setAccessToken(accessToken)
+ return accessToken
+ })
+ .catch((error) => {
+ throw error
+ })
+ .finally(() => {
+ setLoading(false)
+ })
+ }, [data])
+
+ return { fetchOboToken, loading, accessToken }
+}
diff --git a/src/index.tsx b/src/index.tsx
index 66cb78881..76b167ffe 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,14 +1,18 @@
import { StrictMode } from "react"
import ReactDOM from "react-dom"
+import { AuthProvider } from "react-oidc-context"
import App from "./App"
import * as serviceWorker from "./serviceWorker"
import packageInfo from "../package.json"
import { env } from "app/config/env"
+import { oidcConfig } from "app/state/auth/oidc/oidcConfig"
ReactDOM.render(
-
+
+
+
,
document.getElementById("root")
)
diff --git a/vite.config.mts b/vite.config.mts
index 05941d6f9..f4bd0cccd 100644
--- a/vite.config.mts
+++ b/vite.config.mts
@@ -28,7 +28,7 @@ export default defineConfig(({ mode }) => {
function setEnv(mode: string) {
Object.assign(
process.env,
- loadEnv(mode, ".", ["REACT_APP_", "NODE_ENV", "PUBLIC_URL"])
+ loadEnv(mode, ".", ["REACT_APP_", "NODE_ENV", "PUBLIC_URL", "VITE_"])
)
process.env.NODE_ENV ||= mode
const { homepage } = JSON.parse(readFileSync("package.json", "utf-8"))
@@ -48,7 +48,7 @@ function envPlugin(): Plugin {
return {
name: "env-plugin",
config(_, { mode }) {
- const env = loadEnv(mode, ".", ["REACT_APP_", "NODE_ENV", "PUBLIC_URL"])
+ const env = loadEnv(mode, ".", ["REACT_APP_", "NODE_ENV", "PUBLIC_URL", "VITE_"])
return {
define: Object.fromEntries(
Object.entries(env).map(([key, value]) => [
@@ -168,7 +168,7 @@ function importPrefixPlugin(): Plugin {
// Migration guide: Follow the guide below, you may need to rename your environment variable to a name that begins with VITE_ instead of REACT_APP_
// https://vitejs.dev/guide/env-and-mode.html#html-env-replacement
function htmlPlugin(mode: string): Plugin {
- const env = loadEnv(mode, ".", ["REACT_APP_", "NODE_ENV", "PUBLIC_URL"])
+ const env = loadEnv(mode, ".", ["REACT_APP_", "NODE_ENV", "PUBLIC_URL", "VITE_"])
return {
name: "html-plugin",
transformIndexHtml: {