diff --git a/apps/i15-1/package.json b/apps/i15-1/package.json index 5c2df73..226b122 100644 --- a/apps/i15-1/package.json +++ b/apps/i15-1/package.json @@ -11,10 +11,12 @@ "dependencies": { "@atlas/blueapi": "workspace:*", "@atlas/blueapi-query": "workspace:*", - "@diamondlightsource/sci-react-ui": "^0.2.0", + "@diamondlightsource/sci-react-ui": "^0.4.0", "@mui/icons-material": "^6.5.0", "@mui/material": "<7.0.0", - "@tanstack/react-query": "^5.90.21" + "@tanstack/react-query": "^5.90.21", + "axios": "^1.13.4", + "react-error-boundary": "^6.0.0" }, "devDependencies": { "@atlas/vitest-conf": "workspace:*", diff --git a/apps/i15-1/src/components/RunPlanButton.tsx b/apps/i15-1/src/components/RunPlanButton.tsx index 673a39d..567841d 100644 --- a/apps/i15-1/src/components/RunPlanButton.tsx +++ b/apps/i15-1/src/components/RunPlanButton.tsx @@ -3,6 +3,7 @@ import { useState } from "react"; import { useSetActiveTask, useSubmitTask } from "@atlas/blueapi-query"; import type { TaskRequest } from "@atlas/blueapi"; +import { useUserAuth } from "../context/userAuth/useUserAuth"; type RunPlanButtonProps = { name: string; @@ -17,13 +18,14 @@ const RunPlanButton = ({ instrumentSession, buttonText = "Run", }: RunPlanButtonProps) => { - + const user = useUserAuth(); + const submitTask = useSubmitTask(); const startTask = useSetActiveTask(); const submitAndRunTask = async (task: TaskRequest) => { await submitTask .mutateAsync(task) - .then(response => startTask.mutateAsync(response.task_id)); + .then((response) => startTask.mutateAsync(response.task_id)); }; const [loading, setLoading] = useState(false); @@ -38,12 +40,19 @@ const RunPlanButton = ({ setLoading(false); }; + const isButtonDisabled = () => { + const disable = + user.person == null || user.person == undefined ? true : false; + return disable; + }; + return ( diff --git a/apps/i15-1/src/components/WaffleNavbar.tsx b/apps/i15-1/src/components/WaffleNavbar.tsx index 9d40a38..4a9ce61 100644 --- a/apps/i15-1/src/components/WaffleNavbar.tsx +++ b/apps/i15-1/src/components/WaffleNavbar.tsx @@ -5,9 +5,16 @@ import { Navbar, NavLink, NavLinks, + User, } from "@diamondlightsource/sci-react-ui"; +import { useUserAuth } from "../context/userAuth/useUserAuth"; function WaffleNavbar() { + const user = useUserAuth(); + + const handleLogIn = () => window.location.assign("/oauth2/sign_in"); + const handleLogOut = () => window.location.assign("/oauth2/sign_out"); + return ( + } diff --git a/apps/i15-1/src/context/userAuth/UserAuthContext.ts b/apps/i15-1/src/context/userAuth/UserAuthContext.ts new file mode 100644 index 0000000..c3d4e28 --- /dev/null +++ b/apps/i15-1/src/context/userAuth/UserAuthContext.ts @@ -0,0 +1,6 @@ +import { createContext } from "react"; +import { type UserAuthStatus } from "./authUtils"; + +const userDefault: UserAuthStatus = {person: null, person_status: "PENDING"}; + +export const UserAuthContext = createContext(userDefault); diff --git a/apps/i15-1/src/context/userAuth/UserAuthProvider.tsx b/apps/i15-1/src/context/userAuth/UserAuthProvider.tsx new file mode 100644 index 0000000..e5374dc --- /dev/null +++ b/apps/i15-1/src/context/userAuth/UserAuthProvider.tsx @@ -0,0 +1,58 @@ +import { type ReactNode } from "react"; +import { UserAuthContext } from "./UserAuthContext"; +import { getUser, type UserAuthStatus } from "./authUtils"; +import { useQuery } from "@tanstack/react-query"; +import type { AxiosError } from "axios"; + +export const UserAuthProvider = ({ children }: { children: ReactNode }) => { + const query = useQuery({ + queryKey: ["user"], + queryFn: getUser, + retry: (failureCount, error: AxiosError) => { + if ("status" in error && (error.status == 401 || error.status == 403)) { + //dont retry 401/403 + return false; + } + + return failureCount < 2; + }, + }); + + //undefined if pending + let response: UserAuthStatus = { + person: null, + person_status: "PENDING", + }; + + if (query.isError) { + if (query.error.status == 401) { + response = { + person: null, + person_status: "UNAUTHORIZED", + }; + } else if (query.error.status == 403) { + response = { + person: null, + person_status: "FORBIDDEN", + }; + } else { + response = { + person: null, + person_status: "ERROR", + }; + } + } else { + if (query.data) { + response = { + person: query.data.preferredUsername, + person_status: "OK", + }; + } + } + + return ( + + {children} + + ); +}; diff --git a/apps/i15-1/src/context/userAuth/authUtils.ts b/apps/i15-1/src/context/userAuth/authUtils.ts new file mode 100644 index 0000000..86289b4 --- /dev/null +++ b/apps/i15-1/src/context/userAuth/authUtils.ts @@ -0,0 +1,22 @@ +import axios, { type AxiosResponse } from "axios"; + +const userUrl = "oauth2/userinfo"; + +export type Person = { + identifier: string; + accepted_orca_eula: boolean; +}; + +export type UserName = { + preferredUsername: string; +}; + +export type UserAuthStatus = { + person: string | null | undefined; + person_status: "PENDING" | "UNAUTHORIZED" | "FORBIDDEN" | "OK" | "ERROR"; +}; + +export const getUser = async () => { + const { data } = await axios.get>(userUrl); + return data; +}; diff --git a/apps/i15-1/src/context/userAuth/useUserAuth.ts b/apps/i15-1/src/context/userAuth/useUserAuth.ts new file mode 100644 index 0000000..e61f45d --- /dev/null +++ b/apps/i15-1/src/context/userAuth/useUserAuth.ts @@ -0,0 +1,12 @@ +import { UserAuthContext } from "./UserAuthContext" +import { useContext } from "react" + +export const useUserAuth = () => { + const context = useContext(UserAuthContext); + if (!context) { + throw new Error( + "No user context found, is your component without UserAuthProvider?" + ); + } + return context; +} \ No newline at end of file diff --git a/apps/i15-1/src/main.tsx b/apps/i15-1/src/main.tsx index 6d90631..3f4b290 100644 --- a/apps/i15-1/src/main.tsx +++ b/apps/i15-1/src/main.tsx @@ -17,6 +17,7 @@ declare global { import { createApi } from "@atlas/blueapi"; import { BlueapiProvider } from "@atlas/blueapi-query"; +import { UserAuthProvider } from "./context/userAuth/UserAuthProvider.tsx"; async function enableMocking() { if (import.meta.env.DEV) { @@ -51,9 +52,11 @@ enableMocking().then(() => { - - - + + + + + diff --git a/apps/i15-1/src/mocks/handlers.ts b/apps/i15-1/src/mocks/handlers.ts index a2a3aa7..41d50eb 100644 --- a/apps/i15-1/src/mocks/handlers.ts +++ b/apps/i15-1/src/mocks/handlers.ts @@ -1,4 +1,4 @@ -import { http, HttpResponse, graphql } from "msw"; +import { http, HttpResponse } from "msw"; const fakeTaskId = "7304e8e0-81c6-4978-9a9d-9046ab79ce3c"; @@ -18,4 +18,8 @@ export const handlers = [ http.put("/api/worker/state", () => { return HttpResponse.json("IDLE"); }), + + http.get("/oauth2/userinfo", () => { + return HttpResponse.json({ preferredUsername: "abc123456" }); + }), ]; diff --git a/apps/i15-1/src/mocks/node.ts b/apps/i15-1/src/mocks/node.ts new file mode 100644 index 0000000..172ca7f --- /dev/null +++ b/apps/i15-1/src/mocks/node.ts @@ -0,0 +1,4 @@ +import { setupServer } from "msw/node"; +import { handlers } from "./handlers.js"; + +export const server = setupServer(...handlers); diff --git a/apps/i15-1/src/routes/Fallback.tsx b/apps/i15-1/src/routes/Fallback.tsx new file mode 100644 index 0000000..6e56557 --- /dev/null +++ b/apps/i15-1/src/routes/Fallback.tsx @@ -0,0 +1,14 @@ +import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline"; +import { Box, Typography } from "@mui/material"; + + +export function AuthFallbackScreen() { + return ( + + + + Something went wrong with authentication! + + + ); +} \ No newline at end of file diff --git a/helm/templates/configmap.yaml b/helm/templates/configmap.yaml index a47fe49..b63d430 100644 --- a/helm/templates/configmap.yaml +++ b/helm/templates/configmap.yaml @@ -9,4 +9,7 @@ data: cookie_samesite = "lax" cookie_expire = "168h" cookie_refresh = "45s" - cookie_name = "{{ include "oauth2-proxy.fullname" . }}-cookie" \ No newline at end of file + cookie_name = "{{ include "oauth2-proxy.fullname" . }}-cookie" + skip_auth_routes = ["GET=^/$","GET=^/assets",] + api_routes = ["^/oauth2", "^/api"] + skip_provider_button = true diff --git a/helm/values.yaml b/helm/values.yaml index 1d151eb..3bcb400 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -38,10 +38,10 @@ oauth2-proxy: config: existingSecret: ui-keycloak-secret existingConfig: oauth2-proxy-config - + alphaConfig: enabled: true existingSecret: ui-oauth2-alpha-secret - + service: - portNumber: 4180 \ No newline at end of file + portNumber: 4180 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index df18a84..868e8a2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -58,8 +58,8 @@ importers: specifier: workspace:* version: link:../../packages/blueapi-query '@diamondlightsource/sci-react-ui': - specifier: ^0.2.0 - version: 0.2.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@mui/icons-material@6.5.0(@mui/material@6.5.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@mui/material@6.5.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + specifier: ^0.4.0 + version: 0.4.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@jsonforms/core@3.6.0)(@jsonforms/material-renderers@3.6.0(a2958437c922d885fd4a4f8b5e701907))(@jsonforms/react@3.6.0(@jsonforms/core@3.6.0)(react@18.3.1))(@mui/icons-material@6.5.0(@mui/material@6.5.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@mui/material@6.5.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) '@mui/icons-material': specifier: ^6.5.0 version: 6.5.0(@mui/material@6.5.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.28)(react@18.3.1) @@ -69,6 +69,12 @@ importers: '@tanstack/react-query': specifier: ^5.90.21 version: 5.90.21(react@18.3.1) + axios: + specifier: ^1.13.4 + version: 1.13.6 + react-error-boundary: + specifier: ^6.0.0 + version: 6.1.1(react@18.3.1) devDependencies: '@atlas/vitest-conf': specifier: workspace:* @@ -401,6 +407,18 @@ packages: '@mui/material': ^6.1.7 react: ^18.3.1 + '@diamondlightsource/sci-react-ui@0.4.1': + resolution: {integrity: sha512-Ks3DFfsBvan173N6jigfFMQp4dW7yW/Yjh0OTUmQ3TGBY42CEDtClkuAu8AKe6XM0kLrATGXm/zpYHo5o+Yhpw==} + peerDependencies: + '@emotion/react': ^11.13.3 + '@emotion/styled': ^11.13.0 + '@jsonforms/core': ^3.6.0 + '@jsonforms/material-renderers': ^3.6.0 + '@jsonforms/react': ^3.6.0 + '@mui/icons-material': ^6.1.7 + '@mui/material': ^6.1.7 + react: ^18.3.1 + '@dimforge/rapier3d-compat@0.12.0': resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==} @@ -2526,6 +2544,9 @@ packages: engines: {node: '>=6'} hasBin: true + keycloak-js@26.2.3: + resolution: {integrity: sha512-widjzw/9T6bHRgEp6H/Se3NCCarU7u5CwFKBcwtu7xfA1IfdZb+7Q7/KGusAnBo34Vtls8Oz9vzSqkQvQ7+b4Q==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -2736,6 +2757,9 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -3381,6 +3405,9 @@ packages: resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==} engines: {node: '>=6.14.2'} + utif@3.1.0: + resolution: {integrity: sha512-WEo4D/xOvFW53K5f5QTaTbbiORcm2/pCL9P6qmJnup+17eYfKaEhDeX9PeQkuyEoIxlbGklDuGl8xwuXYMrrXQ==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -3850,6 +3877,20 @@ snapshots: react: 18.3.1 react-icons: 5.6.0(react@18.3.1) + '@diamondlightsource/sci-react-ui@0.4.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@jsonforms/core@3.6.0)(@jsonforms/material-renderers@3.6.0(a2958437c922d885fd4a4f8b5e701907))(@jsonforms/react@3.6.0(@jsonforms/core@3.6.0)(react@18.3.1))(@mui/icons-material@6.5.0(@mui/material@6.5.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@mui/material@6.5.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + dependencies: + '@emotion/react': 11.14.0(@types/react@18.3.28)(react@18.3.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1) + '@jsonforms/core': 3.6.0 + '@jsonforms/material-renderers': 3.6.0(a2958437c922d885fd4a4f8b5e701907) + '@jsonforms/react': 3.6.0(@jsonforms/core@3.6.0)(react@18.3.1) + '@mui/icons-material': 6.5.0(@mui/material@6.5.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.28)(react@18.3.1) + '@mui/material': 6.5.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + keycloak-js: 26.2.3 + react: 18.3.1 + react-icons: 5.6.0(react@18.3.1) + utif: 3.1.0 + '@dimforge/rapier3d-compat@0.12.0': {} '@emotion/babel-plugin@11.13.5': @@ -5980,6 +6021,8 @@ snapshots: json5@2.2.3: {} + keycloak-js@26.2.3: {} + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -6184,6 +6227,8 @@ snapshots: dependencies: p-limit: 3.1.0 + pako@1.0.11: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -6842,6 +6887,10 @@ snapshots: dependencies: node-gyp-build: 4.8.4 + utif@3.1.0: + dependencies: + pako: 1.0.11 + util-deprecate@1.0.2: {} util-extend@1.0.3: {}