diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index 9eaa9fa6..c34ac3ba 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -14,80 +14,6 @@ on: workflow_dispatch: jobs: - build-and-deploy-frontend: - name: 'Build and deploy Frontend' - runs-on: ubuntu-latest - environment: Development - - defaults: - run: - working-directory: frontend - - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Setup Node.js version - uses: actions/setup-node@v3 - with: - node-version: "18.x" - - - name: Restore cache - uses: actions/cache@v3 - with: - path: | - frontend/.next/cache - # Generate a new cache whenever packages or source files change. - key: ${{ runner.os }}-nextjs-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} - # If source files changed but packages didn't, rebuild from a prior cache. - restore-keys: | - ${{ runner.os }}-nextjs-${{ hashFiles('**/yarn.lock') }}- - - - name: Install ci dependencies - run: yarn --frozen-lockfile --ignore-scripts - - - name: Build with Next.js (NO) - run: | - yarn build - mv ./build/static ./build/standalone/build - cp -r ./public ./build/standalone/ - mv ./build ./build-no - env: - NEXT_PUBLIC_VIBES_BACKEND_URL: ${{ vars.NEXT_PUBLIC_VIBES_BACKEND_URL_NO }} - NEXT_PUBLIC_CLIENT_ID: ${{ vars.NEXT_PUBLIC_CLIENT_ID }} - NEXT_PUBLIC_TENANT_ID: ${{ vars.NEXT_PUBLIC_TENANT_ID }} - NEXT_PUBLIC_APP_SCOPE: ${{ vars.NEXT_PUBLIC_APP_SCOPE }} - - - name: Build with Next.js (SE) - run: | - yarn build - mv ./build/static ./build/standalone/build - cp -r ./public ./build/standalone - mv ./build ./build-se - env: - NEXT_PUBLIC_VIBES_BACKEND_URL: ${{ vars.NEXT_PUBLIC_VIBES_BACKEND_URL_SE }} - NEXT_PUBLIC_CLIENT_ID: ${{ vars.NEXT_PUBLIC_CLIENT_ID }} - NEXT_PUBLIC_TENANT_ID: ${{ vars.NEXT_PUBLIC_TENANT_ID }} - NEXT_PUBLIC_APP_SCOPE: ${{ vars.NEXT_PUBLIC_APP_SCOPE }} - - - - name: Deploy to App Service (NO) - id: deploy-to-webapp-no - uses: azure/webapps-deploy@v2 - with: - app-name: 'vibes-frontend-norway-dev' - slot-name: 'Production' - publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE_FRONTEND_NO }} - package: ./frontend/build-no/standalone - - - name: Deploy to App Service (SE) - id: deploy-to-webapp-se - uses: azure/webapps-deploy@v2 - with: - app-name: 'vibes-frontend-sweden-dev' - slot-name: 'Production' - publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE_FRONTEND_SE }} - package: ./frontend/build-se/standalone build-and-deploy-backend: name: 'Build and deploy Backend' @@ -127,3 +53,50 @@ jobs: app-name: vibes-backend-sweden-dev publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE_BACKEND_SE }} # Define secret variable in repository settings as per action documentation package: 'backend/Api/build' + + build-frontend: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 + + - name: Log in to GitHub container registry + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + + - name: Lowercase the repo name and username + run: echo "REPO=${GITHUB_REPOSITORY,,}" >>${GITHUB_ENV} + + - name: Build and push container image to registry + uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0 + with: + push: true + tags: ghcr.io/${{ env.REPO }}-frontend:${{ github.sha }} + file: Frontend.Dockerfile + + deploy-frontend: + permissions: + contents: none + runs-on: ubuntu-latest + needs: build-frontend + environment: + name: 'Development' + url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + + steps: + - name: Lowercase the repo name and username + run: echo "REPO=${GITHUB_REPOSITORY,,}" >>${GITHUB_ENV} + + - name: Deploy to Azure Web App + id: deploy-to-webapp + uses: azure/webapps-deploy@v2 + with: + app-name: vibes-frontend-dev + publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE_FRONTEND }} + images: 'ghcr.io/${{ env.REPO }}-frontend:${{ github.sha }}' \ No newline at end of file diff --git a/frontend/Dockerfile b/Frontend.Dockerfile similarity index 52% rename from frontend/Dockerfile rename to Frontend.Dockerfile index a7bb271b..a8e791fb 100644 --- a/frontend/Dockerfile +++ b/Frontend.Dockerfile @@ -1,29 +1,16 @@ FROM node:18-alpine AS base # Install dependencies only when needed -FROM base AS deps +FROM base AS builder # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. RUN apk add --no-cache libc6-compat WORKDIR /app # Install dependencies based on the preferred package manager -COPY yarn.lock* package.json ./ +COPY ./frontend . RUN yarn --frozen-lockfile --ignore-scripts; - - -# Rebuild the source code only when needed -FROM base AS builder -WORKDIR /bundle -COPY --from=deps /app/node_modules ./node_modules -COPY . . - RUN yarn build -# If using npm comment out above and use below instead -# RUN npm run build - -# Production image, copy all the files and run next -FROM base AS runner WORKDIR /app ENV NODE_ENV production @@ -32,18 +19,9 @@ ENV NODE_ENV production RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs - -COPY --from=builder /bundle/public ./public - -# Set the correct permission for prerender cache RUN mkdir .next RUN chown nextjs:nodejs .next -# Automatically leverage output traces to reduce image size -# https://nextjs.org/docs/advanced-features/output-file-tracing -COPY --from=builder --chown=nextjs:nodejs /bundle/build/standalone ./ -COPY --from=builder --chown=nextjs:nodejs /bundle/build/static ./build/static - USER nextjs EXPOSE 3000 @@ -52,4 +30,4 @@ ENV PORT 3000 # set hostname to localhost ENV HOSTNAME "0.0.0.0" -CMD ["node", "server.js"] \ No newline at end of file +CMD ["yarn", "start"] \ No newline at end of file diff --git a/backend/Api/Consultants/ConsultantController.cs b/backend/Api/Consultants/ConsultantController.cs index 4468eba0..76a2830b 100644 --- a/backend/Api/Consultants/ConsultantController.cs +++ b/backend/Api/Consultants/ConsultantController.cs @@ -27,7 +27,7 @@ public ConsultantController(ApplicationContext context, IMemoryCache cache, Cons [HttpGet] public ActionResult> Get( [FromQuery(Name = "weeks")] int numberOfWeeks = 8, - [FromQuery(Name = "includeOccupied")] bool includeOccupied = false) + [FromQuery(Name = "includeOccupied")] bool includeOccupied = true) { var consultants = GetConsultantsWithAvailability(numberOfWeeks) .Where(c => @@ -88,7 +88,8 @@ private List LoadConsultantAvailability(int numberOfWeeks) { var applicableWeeks = DateService.GetNextWeeks(numberOfWeeks); var firstDayOfCurrentWeek = DateService.GetFirstDayOfWeekContainingDate(DateTime.Now); - var firstWorkDayOutOfScope = DateService.GetFirstDayOfWeekContainingDate(DateTime.Now.AddDays(numberOfWeeks*7)); + var firstWorkDayOutOfScope = + DateService.GetFirstDayOfWeekContainingDate(DateTime.Now.AddDays(numberOfWeeks * 7)); // Needed to filter planned absence and staffing. // From november, we will span two years. diff --git a/backend/Api/Departments/DeparmentController.cs b/backend/Api/Departments/DeparmentController.cs index 91c99176..76405de6 100644 --- a/backend/Api/Departments/DeparmentController.cs +++ b/backend/Api/Departments/DeparmentController.cs @@ -1,7 +1,7 @@ using Database.DatabaseContext; using Microsoft.AspNetCore.Mvc; -[Route("/departments")] +[Route("/v0/departments")] [ApiController] public class DepartmentController : ControllerBase { diff --git a/frontend/.env.template b/frontend/.env.template index dcd7f2d8..a4680394 100644 --- a/frontend/.env.template +++ b/frontend/.env.template @@ -1,5 +1,6 @@ -NEXT_PUBLIC_CLIENT_ID= # App Registrations (Azure portal) -> select app reg -> Copy client ID -NEXT_PUBLIC_TENANT_ID= # App Registrations (Azure portal) -> select app reg -> Copy tenant ID -NEXT_PUBLIC_APP_SCOPE= # App Registrations (Azure portal) -> select app reg -> Expose an API -> Copy scope ID (i.e. api://oidajwoig/...) - -# NEXT_PUBLIC_VIBES_BACKEND_URL= #Not necessary for local development, defaults to localhost api \ No newline at end of file +AZURE_AD_CLIENT_ID= # App Registrations (Azure portal) -> select app reg -> Copy client ID +AZURE_AD_TENANT_ID= # App Registrations (Azure portal) -> select app reg -> Copy tenant ID +AZURE_AD_APP_SCOPE= # App Registrations (Azure portal) -> select app reg -> Expose an API -> Copy scope ID (i.e. api://oidajwoig/...) +AZURE_AD_CLIENT_SECRET= #Generate from app registration +NEXTAUTH_SECRET= # High-entroy random secret +NEXTAUTH_URL= # i.e. http://localhost:3000 \ No newline at end of file diff --git a/frontend/next.config.js b/frontend/next.config.js index 5c827a34..3a48579c 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -1,18 +1,7 @@ /** @type {import('next').NextConfig} */ - -const apiBackendUrl = - process.env.NEXT_PUBLIC_VIBES_BACKEND_URL ?? "http://localhost:7172"; // See backend/launchSettings.json for details on dev-env +// See backend/launchSettings.json for details on dev-env const nextConfig = { - async rewrites() { - return [ - { - source: "/api/:path*", - destination: `${apiBackendUrl}/:path*`, - }, - ]; - }, - output: "standalone", distDir: "build", swcMinify: true, modularizeImports: { diff --git a/frontend/package.json b/frontend/package.json index 01f0e1de..130753be 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,15 +4,13 @@ "private": true, "scripts": { "dev": "next dev", - "start-test": "NEXT_PUBLIC_NO_AUTH=true next dev", + "start-test": "NEXT_PUBLIC_NO_AUTH=true NEXTAUTH_SECRET='insecure-secret' next dev", "build": "next build", "start": "next start", "lint": "next lint", "prettier:ci": "prettier --check . " }, "dependencies": { - "@azure/msal-browser": "^3.1.0", - "@azure/msal-react": "^2.0.3", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.14.9", @@ -24,6 +22,7 @@ "eslint": "8.48.0", "eslint-config-next": "^13.5.2", "next": "^13.5.2", + "next-auth": "^4.23.2", "postcss": "^8.4.31", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/frontend/src/app/api/auth/[...nextauth]/route.ts b/frontend/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 00000000..a96a6c4a --- /dev/null +++ b/frontend/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,54 @@ +import { AuthOptions, getServerSession, Session } from "next-auth"; +import AzureADProvider from "next-auth/providers/azure-ad"; + +export type CustomSession = { + id_token?: string; + access_token?: string; +} & Session; + +export const authOptions: AuthOptions = { + // Configure one or more authentication providers + providers: [ + AzureADProvider({ + clientId: process.env.AZURE_AD_CLIENT_ID!, + clientSecret: process.env.AZURE_AD_CLIENT_SECRET!, + tenantId: process.env.AZURE_AD_TENANT_ID!, + authorization: { + params: { + scope: `openid profile email ${process.env.AZURE_AD_APP_SCOPE}`, + }, + }, + idToken: true, + }), + ], + session: { + strategy: "jwt", + maxAge: 30 * 24 * 60 * 60, // 30 days + }, + + callbacks: { + async redirect({ baseUrl }) { + return baseUrl; + }, + async jwt({ token, account }) { + if (account) { + token.id_token = account.id_token; + token.access_token = account.access_token; + } + return token; + }, + async session({ session, token }) { + if (session) { + session = Object.assign({}, session, { + id_token: token.id_token, + access_token: token.access_token, + }); + } + return session; + }, + }, +}; + +export async function getCustomServerSession(authOptions: AuthOptions) { + return (await getServerSession(authOptions)) as CustomSession; +} diff --git a/frontend/src/app/bemanning/page.tsx b/frontend/src/app/bemanning/page.tsx index 1ce68cda..1dde0df2 100644 --- a/frontend/src/app/bemanning/page.tsx +++ b/frontend/src/app/bemanning/page.tsx @@ -1,22 +1,14 @@ -"use client"; - import FilteredConsultantsList from "@/components/FilteredConsultantsList"; -import useVibesApi from "@/hooks/useVibesApi"; -import { CircularProgress } from "@mui/material"; - -export default function Bemanning() { - const { data, isLoading } = useVibesApi(true); - - if (isLoading) { - return ; - } - - if (data) { - return ( -
-

Konsulenter

- -
- ); - } +import { fetchWithToken } from "@/data/fetchWithToken"; +import { Variant } from "@/types"; + +export default async function Bemanning() { + const consultants = (await fetchWithToken("variants")) ?? []; + + return ( +
+

Konsulenter

+ +
+ ); } diff --git a/frontend/src/auth/fetchWithToken.ts b/frontend/src/auth/fetchWithToken.ts deleted file mode 100644 index 6b05e7d2..00000000 --- a/frontend/src/auth/fetchWithToken.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { msalInstance } from "@/auth/msalInstance"; -import { loginRequest } from "@/authConfig"; -import { MockConsultants } from "../../mockdata/mockConsultants"; -import { MockDepartments } from "../../mockdata/mockDepartments"; - -export async function fetchWithToken(path: string) { - if (process.env.NEXT_PUBLIC_NO_AUTH) { - return mockedCall(path); - } - - const account = msalInstance.getActiveAccount(); - if (!account) { - throw Error( - "No active account! Verify a user has been signed in and setActiveAccount has been called.", - ); - } - - if (!process.env.NEXT_PUBLIC_APP_SCOPE) { - throw new Error( - "Environment variable: NEXT_PUBLIC_APP_SCOPE is missing or empty", - ); - } - - const response = await msalInstance.acquireTokenSilent({ - ...loginRequest, - account: account, - scopes: [process.env.NEXT_PUBLIC_APP_SCOPE ?? ""], - }); - - // @ts-ignore - const headers = new Headers(); - const bearer = `Bearer ${response.accessToken}`; - - headers.append("Authorization", bearer); - - const options = { - method: "GET", - headers: headers, - }; - - try { - const response = await fetch(path, options); - return await response.json(); - } catch (error) { - console.error(error); - } -} - -function mockedCall(path: string) { - if (path.includes("/variants")) { - return MockConsultants; - } - if (path.includes("/departments")) { - return MockDepartments; - } -} diff --git a/frontend/src/auth/msalInstance.ts b/frontend/src/auth/msalInstance.ts deleted file mode 100644 index 6d7066a9..00000000 --- a/frontend/src/auth/msalInstance.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { msalConfig } from "@/authConfig"; -import { PublicClientApplication } from "@azure/msal-browser"; -//@ts-ignore -export const msalInstance = new PublicClientApplication(msalConfig); diff --git a/frontend/src/authConfig.ts b/frontend/src/authConfig.ts deleted file mode 100644 index df049751..00000000 --- a/frontend/src/authConfig.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Config object to be passed to Msal on creation -const msalClientId = process.env.NEXT_PUBLIC_CLIENT_ID; -const msalTenantId = process.env.NEXT_PUBLIC_TENANT_ID; - -export const msalConfig = { - auth: { - clientId: msalClientId, - authority: `https://login.microsoftonline.com/${msalTenantId}`, - redirectUri: "/", - postLogoutRedirectUri: "/", - }, - system: { - allowNativeBroker: false, // Disables WAM Broker - }, -}; - -// Add here scopes for id token to be used at MS Identity Platform endpoints. -export const loginRequest = { - scopes: ["User.Read"], -}; diff --git a/frontend/src/components/AppProviders.tsx b/frontend/src/components/AppProviders.tsx index 39325977..058e887d 100644 --- a/frontend/src/components/AppProviders.tsx +++ b/frontend/src/components/AppProviders.tsx @@ -1,32 +1,8 @@ "use client"; -import { EventType } from "@azure/msal-browser"; -import { MsalProvider } from "@azure/msal-react"; import { CssBaseline } from "@mui/material"; -import { QueryClient, QueryClientProvider } from "react-query"; -import { msalInstance } from "@/auth/msalInstance"; import PageLayout from "./PageLayout"; import ThemeRegistry from "./ThemeRegistry/ThemeRegistry"; -msalInstance.initialize().then(() => { - // Account selection logic is app dependent. Adjust as needed for different use cases. - const accounts = msalInstance.getAllAccounts(); - if (accounts.length > 0) { - msalInstance.setActiveAccount(accounts[0]); - } - - msalInstance.addEventCallback((event) => { - // Types are outdated: Event payload is an object with account, token, etcw - //@ts-ignore - if (event.eventType === EventType.LOGIN_SUCCESS && event.payload.account) { - //@ts-ignore - const account = event.payload.account; - msalInstance.setActiveAccount(account); - } - }); -}); - -const queryClient = new QueryClient(); - export default function AppProviders({ children, }: { @@ -35,11 +11,7 @@ export default function AppProviders({ return ( - - - {children} - - + {children} ); } diff --git a/frontend/src/components/DepartmentFilter.tsx b/frontend/src/components/DepartmentFilter.tsx index cbf95771..5485bd09 100644 --- a/frontend/src/components/DepartmentFilter.tsx +++ b/frontend/src/components/DepartmentFilter.tsx @@ -1,22 +1,17 @@ -"use client"; import FilterButton from "./FilterButton"; -import useDepartmentsApi from "@/hooks/useDepartmentsApi"; -import { CircularProgress } from "@mui/material"; +import { fetchWithToken } from "@/data/fetchWithToken"; +import { Department } from "@/types"; -export default function DepartmentFilter() { - const { data, isLoading } = useDepartmentsApi(); +export default async function DepartmentFilter() { + const departments = (await fetchWithToken("departments")) ?? []; - if (isLoading) { - return ; - } - - if (data) { + if (departments.length > 0) { return (

Avdelinger

- {data?.map((department, index) => ( + {departments?.map((department, index) => ( ))}
diff --git a/frontend/src/components/PageLayout.tsx b/frontend/src/components/PageLayout.tsx index 396260f6..452c0b58 100644 --- a/frontend/src/components/PageLayout.tsx +++ b/frontend/src/components/PageLayout.tsx @@ -1,11 +1,4 @@ -"use client"; -import { - AuthenticatedTemplate, - UnauthenticatedTemplate, -} from "@azure/msal-react"; -import { Box } from "@mui/material"; import VibesAppBar from "./VibesNavBar"; -import SignInSignOutButton from "./vibes-buttons/SignInSignOutButton"; import React from "react"; export default function PageLayout({ @@ -16,35 +9,7 @@ export default function PageLayout({ return (
- {children} - - - - Please log in first - - - + {children}
); } - -function Authenticated({ children }: { children: React.ReactNode }) { - return process.env.NEXT_PUBLIC_NO_AUTH ? ( - <>{children} - ) : ( - {children} - ); -} - -function Unauthenticated({ children }: { children: React.ReactNode }) { - return process.env.NEXT_PUBLIC_NO_AUTH ? ( - <> - ) : ( - {children} - ); -} diff --git a/frontend/src/components/VibesNavBar.tsx b/frontend/src/components/VibesNavBar.tsx index e4670d14..5dd6bf60 100644 --- a/frontend/src/components/VibesNavBar.tsx +++ b/frontend/src/components/VibesNavBar.tsx @@ -38,7 +38,6 @@ export default function VibesAppBar() { }} ml={2} > - ); diff --git a/frontend/src/components/vibes-buttons/SignInButton.tsx b/frontend/src/components/vibes-buttons/SignInButton.tsx index 72b53be8..fff1b607 100644 --- a/frontend/src/components/vibes-buttons/SignInButton.tsx +++ b/frontend/src/components/vibes-buttons/SignInButton.tsx @@ -1,21 +1,14 @@ -import { loginRequest } from "@/authConfig"; -import { useMsal } from "@azure/msal-react"; +"use client"; + import LoginIcon from "@mui/icons-material/Login"; import { IconButton } from "@mui/material"; +import { signIn } from "next-auth/react"; -export default function SignInButton() { - const { instance } = useMsal(); - - function handleLogin() { - instance.loginRedirect(loginRequest).catch((e) => { - console.error(`loginRedirect failed: ${e}`); - }); - } - +function SignInButton() { return (
signIn("azure-ad")} size="small" color="inherit" sx={{ ml: 2 }} @@ -25,3 +18,5 @@ export default function SignInButton() {
); } + +export default SignInButton; diff --git a/frontend/src/components/vibes-buttons/SignInSignOutButton.tsx b/frontend/src/components/vibes-buttons/SignInSignOutButton.tsx index 3bbf1b49..2c7ae883 100644 --- a/frontend/src/components/vibes-buttons/SignInSignOutButton.tsx +++ b/frontend/src/components/vibes-buttons/SignInSignOutButton.tsx @@ -1,21 +1,19 @@ -import { InteractionStatus } from "@azure/msal-browser"; -import { useIsAuthenticated, useMsal } from "@azure/msal-react"; +"use client"; +import { CircularProgress } from "@mui/material"; import SignInButton from "./SignInButton"; import SignOutButton from "./SignOutButton"; +import { useSession } from "next-auth/react"; -export default function SignInSignOutButton() { - const { inProgress } = useMsal(); - const isAuthenticated = useIsAuthenticated(); +function SignInSignOutButton() { + const { status } = useSession(); - if (isAuthenticated) { + if (status === "authenticated") { return ; - } else if ( - inProgress !== InteractionStatus.Startup && - inProgress !== InteractionStatus.HandleRedirect - ) { - // inProgress check prevents sign-in button from being displayed briefly after returning from a redirect sign-in. Processing the server response takes a render cycle or two + } else if (status === "unauthenticated") { return ; } else { - return null; + return ; } } + +export default SignInSignOutButton; diff --git a/frontend/src/components/vibes-buttons/SignOutButton.tsx b/frontend/src/components/vibes-buttons/SignOutButton.tsx index ddbe216b..f3953a2c 100644 --- a/frontend/src/components/vibes-buttons/SignOutButton.tsx +++ b/frontend/src/components/vibes-buttons/SignOutButton.tsx @@ -1,26 +1,18 @@ +"use client"; import { AnchorProp } from "@/types"; -import { useAccount, useMsal } from "@azure/msal-react"; import { Logout } from "@mui/icons-material"; import { Avatar, ListItemIcon } from "@mui/material"; import IconButton from "@mui/material/IconButton"; import Menu from "@mui/material/Menu"; import MenuItem from "@mui/material/MenuItem"; +import { signOut } from "next-auth/react"; + import { useState } from "react"; -import { AccountInfo } from "@azure/msal-common"; export default function SignOutButton() { - const { instance } = useMsal(); - const account = useAccount(); - const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); - function handleLogout() { - instance.logoutRedirect().catch((e) => { - console.error(`logoutPopup failed: ${e}`); - }); - } - return (
- {GetInitials(account)} + ABC setAnchorEl(null)} > - + signOut()}> @@ -70,14 +62,3 @@ export default function SignOutButton() {
); } - -function GetInitials(account: AccountInfo | null): string { - if (!account) return ""; - - const initials = account.name?.split(" ").map((n) => n.substring(0, 1)) ?? [ - "", - "", - ]; - - return `${initials[0]}${initials[initials.length - 1]}`; -} diff --git a/frontend/src/data/fetchWithToken.ts b/frontend/src/data/fetchWithToken.ts new file mode 100644 index 00000000..41591fbd --- /dev/null +++ b/frontend/src/data/fetchWithToken.ts @@ -0,0 +1,42 @@ +import { MockConsultants } from "../../mockdata/mockConsultants"; +import { MockDepartments } from "../../mockdata/mockDepartments"; +import { + authOptions, + getCustomServerSession, +} from "@/app/api/auth/[...nextauth]/route"; + +export async function fetchWithToken(path: string): Promise { + if (process.env.NEXT_PUBLIC_NO_AUTH) { + return mockedCall(path); + } + + const session = await getCustomServerSession(authOptions); + + if (!session || !session.access_token) return; + + const apiBackendUrl = process.env.BACKEND_URL ?? "http://localhost:7172/v0"; + + // @ts-ignore + const headers = new Headers(); + const bearer = `Bearer ${session.access_token}`; + + headers.append("Authorization", bearer); + + const options = { + method: "GET", + headers: headers, + }; + const response = await fetch(`${apiBackendUrl}/${path}`, options); + return (await response.json()) as T; +} + +function mockedCall(path: string): Promise { + return new Promise((resolve) => { + if (path.includes("variants")) { + resolve(MockConsultants as T); + } + if (path.includes("departments")) { + resolve(MockDepartments as T); + } + }); +} diff --git a/frontend/src/hooks/useDepartmentsApi.ts b/frontend/src/hooks/useDepartmentsApi.ts deleted file mode 100644 index 5ad441c7..00000000 --- a/frontend/src/hooks/useDepartmentsApi.ts +++ /dev/null @@ -1,32 +0,0 @@ -"use client"; - -import { useIsAuthenticated } from "@azure/msal-react"; -import { useQuery } from "react-query"; -import { Department } from "@/types"; -import { fetchWithToken } from "@/auth/fetchWithToken"; - -function useDepartmentsApi() { - const isAuthenticated = - useIsAuthenticated() || process.env.NEXT_PUBLIC_NO_AUTH; - - return useQuery({ - queryKey: "departments", - queryFn: async () => { - if (isAuthenticated) { - try { - const response: Department[] = - await fetchWithToken(`/api/departments`); - return response; - } catch (err) { - console.error(err); - return []; - } - } - // If not authenticated, return an empty array - return []; - }, - refetchOnWindowFocus: false, - }); -} - -export default useDepartmentsApi; diff --git a/frontend/src/hooks/useVibesApi.ts b/frontend/src/hooks/useVibesApi.ts deleted file mode 100644 index bfbfaa89..00000000 --- a/frontend/src/hooks/useVibesApi.ts +++ /dev/null @@ -1,32 +0,0 @@ -"use client"; -import { Variant } from "@/types"; -import { fetchWithToken } from "@/auth/fetchWithToken"; -import { useIsAuthenticated } from "@azure/msal-react"; -import { useQuery } from "react-query"; - -function useVibesApi(includeOccupied: boolean) { - const isAuthenticated = - useIsAuthenticated() || process.env.NEXT_PUBLIC_NO_AUTH; - - return useQuery({ - queryKey: "vibes", - queryFn: async () => { - if (isAuthenticated) { - try { - const response: Variant[] = await fetchWithToken( - `/api/v0/variants?weeks=8&includeOccupied=${includeOccupied}`, - ); - return response; - } catch (err) { - console.error(err); - return []; - } - } - // If not authenticated, return an empty array - return []; - }, - refetchOnWindowFocus: false, - }); -} - -export default useVibesApi; diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts index f6a6dda1..5ae8a467 100644 --- a/frontend/src/middleware.ts +++ b/frontend/src/middleware.ts @@ -1,52 +1,12 @@ -import { NextRequest, NextResponse } from "next/server"; - -// nonce CSP is currently disabled because of bug: -// https://github.com/vercel/next.js/issues/55638 - -export function middleware(request: NextRequest) { - const cspHeader = ` - default-src 'self'; - script-src 'self' 'unsafe-inline' http://localhost https: ${ - process.env.NODE_ENV === "production" ? "" : `'unsafe-eval'` - }; - style-src 'self' 'unsafe-inline'; - font-src 'self' anima-uploads.s3.amazonaws.com fonts.gstatic.com; - connect-src 'self' https://login.microsoftonline.com; - `; - const requestHeaders = new Headers(request.headers); - - // requestHeaders.set('x-nonce', nonce) - requestHeaders.set( - // 'Content-Security-Policy', - "Content-Security-Policy-Report-Only", // This is used for now to not break - - // Replace newline characters and spaces - cspHeader.replace(/\s{2,}/g, " ").trim(), - ); - - return NextResponse.next({ - headers: requestHeaders, - request: { - headers: requestHeaders, - }, - }); -} - -export const config = { - matcher: [ - /* - * Match all request paths except for the ones starting with: - * - api (API routes) - * - _next/static (static files) - * - _next/image (image optimization files) - * - favicon.ico (favicon file) - */ - { - source: "/((?!api|_next/static|_next/image|favicon.ico).*)", - missing: [ - { type: "header", key: "next-router-prefetch" }, - { type: "header", key: "purpose", value: "prefetch" }, - ], +import { withAuth } from "next-auth/middleware"; + +export default withAuth({ + callbacks: { + authorized: ({ req, token }) => { + return ( + (!!token && !req.nextUrl.basePath.startsWith("/login")) || + !!process.env.NEXT_PUBLIC_NO_AUTH + ); }, - ], -}; + }, +}); diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 3645ad2c..78af805c 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -12,26 +12,6 @@ resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30" integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== -"@azure/msal-browser@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@azure/msal-browser/-/msal-browser-3.1.0.tgz#8addaaf67211302c1e64dba4975f3691ad390175" - integrity sha512-zQpL/mR6Xif+WltcRMej8wRsBQu+lW8e1/su8SMkF41i+Pnn/a/a/l+VoT4D+cQrBAq3PL5IaLfuUoNVTRUjKw== - dependencies: - "@azure/msal-common" "14.0.3" - -"@azure/msal-common@14.0.3": - version "14.0.3" - resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-14.0.3.tgz#4e273c72b7a9dde4e79f84db0e6c51c1665fcd2f" - integrity sha512-Vl5SsC3zvQ8913GnO5Typox+35M6CaXmO/2FXi35LfMAV3ZB/HLCsldLxylI01c3CmtOm7pICWpOjp/DlQ9RWA== - -"@azure/msal-react@^2.0.3": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@azure/msal-react/-/msal-react-2.0.3.tgz#1cf041738cadde6bcf4b2814270b95fb490429a8" - integrity sha512-ysS9vdEkP9UcEAyMsblAZUhU7y74yEjJSg0wYTw+2vLzgGAKoIGw2t4awUVQaWrIEHeROGg7Tpudaz/caOxc7w== - dependencies: - "@rollup/plugin-typescript" "^11.1.0" - rollup "^3.20.2" - "@babel/code-frame@^7.0.0": version "7.22.13" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" @@ -73,7 +53,7 @@ dependencies: regenerator-runtime "^0.14.0" -"@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2": +"@babel/runtime@^7.20.13", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2": version "7.23.1" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.1.tgz#72741dc4d413338a91dcb044a86f3c0bc402646d" integrity sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g== @@ -475,35 +455,23 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@panva/hkdf@^1.0.2": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@panva/hkdf/-/hkdf-1.1.1.tgz#ab9cd8755d1976e72fc77a00f7655a64efe6cd5d" + integrity sha512-dhPeilub1NuIG0X5Kvhh9lH4iW3ZsHlnzwgwbOlgwQ2wG1IqFzsgHqmKPk3WzsdWAeaxKJxgM0+W433RmN45GA== + "@playwright/test@^1.38.1": - version "1.38.1" - resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.38.1.tgz#8ef4263e355cd1d8ad7905d471d268e8acb82ed6" - integrity sha512-NqRp8XMwj3AK+zKLbZShl0r/9wKgzqI/527bkptKXomtuo+dOjU9NdMASQ8DNC9z9zLOMbG53T4eihYr3XR+BQ== + version "1.39.0" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.39.0.tgz#d10ba8e38e44104499e25001945f07faa9fa91cd" + integrity sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ== dependencies: - playwright "1.38.1" + playwright "1.39.0" "@popperjs/core@^2.11.8": version "2.11.8" resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== -"@rollup/plugin-typescript@^11.1.0": - version "11.1.3" - resolved "https://registry.yarnpkg.com/@rollup/plugin-typescript/-/plugin-typescript-11.1.3.tgz#8172858a1e5f4c181aebc61f8920002fd5e04b91" - integrity sha512-8o6cNgN44kQBcpsUJTbTXMTtb87oR1O0zgP3Dxm71hrNgparap3VujgofEilTYJo+ivf2ke6uy3/E5QEaiRlDA== - dependencies: - "@rollup/pluginutils" "^5.0.1" - resolve "^1.22.1" - -"@rollup/pluginutils@^5.0.1": - version "5.0.4" - resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.0.4.tgz#74f808f9053d33bafec0cc98e7b835c9667d32ba" - integrity sha512-0KJnIoRI8A+a1dqOYLxH8vBf8bphDmty5QvIm2hqm7oFCFYKCAZWWd2hXgMibaPsNDhI0AtpYfQZJG47pt/k4g== - dependencies: - "@types/estree" "^1.0.0" - estree-walker "^2.0.2" - picomatch "^2.3.1" - "@rushstack/eslint-patch@^1.3.3": version "1.4.0" resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.4.0.tgz#77e948b9760bd22736a5d26e335a690f76fda37b" @@ -516,11 +484,6 @@ dependencies: tslib "^2.4.0" -"@types/estree@^1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194" - integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA== - "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" @@ -991,6 +954,11 @@ convert-source-map@^1.5.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== +cookie@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + cosmiconfig@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" @@ -1447,11 +1415,6 @@ estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== -estree-walker@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" - integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== - esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" @@ -1996,6 +1959,11 @@ jiti@^1.18.2: resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.20.0.tgz#2d823b5852ee8963585c8dd8b7992ffc1ae83b42" integrity sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA== +jose@^4.11.4, jose@^4.15.1: + version "4.15.2" + resolved "https://registry.yarnpkg.com/jose/-/jose-4.15.2.tgz#61f97383f0b433d45da26d35094155a30a672d92" + integrity sha512-IY73F228OXRl9ar3jJagh7Vnuhj/GzBunPiZP13K0lOl7Am9SoWW3kEzq3MCllJMTtZqHTiDXQvoRd4U95aU6A== + js-sha3@0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" @@ -2187,6 +2155,21 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== +next-auth@^4.23.2: + version "4.23.2" + resolved "https://registry.yarnpkg.com/next-auth/-/next-auth-4.23.2.tgz#6a93ec8bb59890dd43ed149a367852c7d12d0f7c" + integrity sha512-VRmInu0r/yZNFQheDFeOKtiugu3bt90Po3owAQDnFQ3YLQFmUKgFjcE2+3L0ny5jsJpBXaKbm7j7W2QTc6Ye2A== + dependencies: + "@babel/runtime" "^7.20.13" + "@panva/hkdf" "^1.0.2" + cookie "^0.5.0" + jose "^4.11.4" + oauth "^0.9.15" + openid-client "^5.4.0" + preact "^10.6.3" + preact-render-to-string "^5.1.19" + uuid "^8.3.2" + next@^13.5.2: version "13.5.2" resolved "https://registry.yarnpkg.com/next/-/next-13.5.2.tgz#809dd84e481049e298fe79d28b1d66b587483fca" @@ -2226,11 +2209,21 @@ normalize-range@^0.1.2: resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== +oauth@^0.9.15: + version "0.9.15" + resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1" + integrity sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA== + object-assign@^4.0.1, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== +object-hash@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" + integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== + object-hash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" @@ -2306,6 +2299,11 @@ oblivious-set@1.0.0: resolved "https://registry.yarnpkg.com/oblivious-set/-/oblivious-set-1.0.0.tgz#c8316f2c2fb6ff7b11b6158db3234c49f733c566" integrity sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw== +oidc-token-hash@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz#9a229f0a1ce9d4fc89bcaee5478c97a889e7b7b6" + integrity sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw== + once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -2313,6 +2311,16 @@ once@^1.3.0: dependencies: wrappy "1" +openid-client@^5.4.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/openid-client/-/openid-client-5.6.0.tgz#3cc084b9a93a5e810a7f309d23812204d08286d9" + integrity sha512-uFTkN/iqgKvSnmpVAS/T6SNThukRMBcmymTQ71Ngus1F60tdtKVap7zCrleocY+fogPtpmoxi5Q1YdrgYuTlkA== + dependencies: + jose "^4.15.1" + lru-cache "^6.0.0" + object-hash "^2.2.0" + oidc-token-hash "^5.0.3" + optionator@^0.9.3: version "0.9.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" @@ -2401,17 +2409,17 @@ pirates@^4.0.1: resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== -playwright-core@1.38.1: - version "1.38.1" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.38.1.tgz#75a3c470aa9576b7d7c4e274de3d79977448ba08" - integrity sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg== +playwright-core@1.39.0: + version "1.39.0" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.39.0.tgz#efeaea754af4fb170d11845b8da30b2323287c63" + integrity sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw== -playwright@1.38.1: - version "1.38.1" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.38.1.tgz#82ecd9bc4f4f64dbeee8a11c31793748e2528130" - integrity sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow== +playwright@1.39.0: + version "1.39.0" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.39.0.tgz#184c81cd6478f8da28bcd9e60e94fcebf566e077" + integrity sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw== dependencies: - playwright-core "1.38.1" + playwright-core "1.39.0" optionalDependencies: fsevents "2.3.2" @@ -2477,6 +2485,18 @@ postcss@^8.4.23, postcss@^8.4.31: picocolors "^1.0.0" source-map-js "^1.0.2" +preact-render-to-string@^5.1.19: + version "5.2.6" + resolved "https://registry.yarnpkg.com/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz#0ff0c86cd118d30affb825193f18e92bd59d0604" + integrity sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw== + dependencies: + pretty-format "^3.8.0" + +preact@^10.6.3: + version "10.18.1" + resolved "https://registry.yarnpkg.com/preact/-/preact-10.18.1.tgz#3b84bb305f0b05f4ad5784b981d15fcec4e105da" + integrity sha512-mKUD7RRkQQM6s7Rkmi7IFkoEHjuFqRQUaXamO61E6Nn7vqF/bo7EZCmSyrUnp2UWHw0O7XjZ2eeXis+m7tf4lg== + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -2487,6 +2507,11 @@ prettier@3.0.3: resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.3.tgz#432a51f7ba422d1469096c0fdc28e235db8f9643" integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg== +pretty-format@^3.8.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-3.8.0.tgz#bfbed56d5e9a776645f4b1ff7aa1a3ac4fa3c385" + integrity sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew== + prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" @@ -2612,7 +2637,7 @@ resolve-pkg-maps@^1.0.0: resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== -resolve@^1.1.7, resolve@^1.19.0, resolve@^1.22.1, resolve@^1.22.2, resolve@^1.22.4: +resolve@^1.1.7, resolve@^1.19.0, resolve@^1.22.2, resolve@^1.22.4: version "1.22.6" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.6.tgz#dd209739eca3aef739c626fea1b4f3c506195362" integrity sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw== @@ -2642,13 +2667,6 @@ rimraf@3.0.2, rimraf@^3.0.2: dependencies: glob "^7.1.3" -rollup@^3.20.2: - version "3.29.2" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.2.tgz#cbc76cd5b03b9f9e93be991d23a1dff9c6d5b740" - integrity sha512-CJouHoZ27v6siztc21eEQGo0kIcE5D1gVPA571ez0mMYb25LGYGKnVNXpEj5MGlepmDWGXNjDB5q7uNiPHC11A== - optionalDependencies: - fsevents "~2.3.2" - run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -3030,6 +3048,11 @@ util-deprecate@^1.0.2: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + watchpack@2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"