From 00e400c48d3433ca309b7362438bb5f0f6e59dca Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Mon, 3 Nov 2025 11:00:10 -0600 Subject: [PATCH 1/5] Fix org info page being empty on first load for non-admins --- src/ui/components/AuthGuard/index.tsx | 24 +++++++++++++++++++--- src/ui/pages/organization/OrgInfo.page.tsx | 16 ++++++++++----- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/ui/components/AuthGuard/index.tsx b/src/ui/components/AuthGuard/index.tsx index 412bc154..8904f521 100644 --- a/src/ui/components/AuthGuard/index.tsx +++ b/src/ui/components/AuthGuard/index.tsx @@ -5,10 +5,10 @@ import { AcmAppShell, AcmAppShellProps } from "@ui/components/AppShell"; import FullScreenLoader from "@ui/components/AuthContext/LoadingScreen"; import { getRunEnvironmentConfig, ValidService } from "@ui/config"; import { useApi } from "@ui/util/api"; -import { AppRoles } from "@common/roles"; +import { AppRoles, OrgRoleDefinition } from "@common/roles"; export const CACHE_KEY_PREFIX = "auth_response_cache_"; -const CACHE_DURATION = 2 * 60 * 60 * 1000; // 2 hours in milliseconds +const CACHE_DURATION = 30 * 60 * 1000; // 30 minutes in milliseconds type CacheData = { data: any; // Just the JSON response data @@ -87,7 +87,6 @@ export const clearAuthCache = () => { /** * Retrieves the user's roles from the session cache for a specific service. * @param service The service to check the cache for. - * @param route The authentication check route. * @returns A promise that resolves to an array of roles, or null if not found in cache. */ export const getUserRoles = async ( @@ -105,6 +104,25 @@ export const getUserRoles = async ( return null; }; +/** + * Retrieves the user's org roles from the session cache for Core API. + * @returns A promise that resolves to an array of roles, or null if not found in cache. + */ +export const getCoreOrgRoles = async (): Promise< + OrgRoleDefinition[] | null +> => { + const { authCheckRoute } = + getRunEnvironmentConfig().ServiceConfiguration.core; + if (!authCheckRoute) { + throw new Error("no auth check route"); + } + const cachedData = await getCachedResponse("core", authCheckRoute); + if (cachedData?.data?.orgRoles && Array.isArray(cachedData.data.orgRoles)) { + return cachedData.data.orgRoles; + } + return null; +}; + export const AuthGuard: React.FC< { resourceDef: ResourceDefinition; diff --git a/src/ui/pages/organization/OrgInfo.page.tsx b/src/ui/pages/organization/OrgInfo.page.tsx index 8b0a15c7..7f295d0f 100644 --- a/src/ui/pages/organization/OrgInfo.page.tsx +++ b/src/ui/pages/organization/OrgInfo.page.tsx @@ -1,13 +1,16 @@ import { useState, useEffect } from "react"; import { Title, Stack, Container, Select } from "@mantine/core"; -import { AuthGuard, getUserRoles } from "@ui/components/AuthGuard"; +import { + AuthGuard, + getUserRoles, + getCoreOrgRoles, +} from "@ui/components/AuthGuard"; import { useApi } from "@ui/util/api"; import { AppRoles } from "@common/roles"; import { notifications } from "@mantine/notifications"; import { IconAlertCircle } from "@tabler/icons-react"; import FullScreenLoader from "@ui/components/AuthContext/LoadingScreen"; import { AllOrganizationNameList, OrganizationName } from "@acm-uiuc/js-shared"; -import { useAuth } from "@ui/components/AuthContext"; import { ManageOrganizationForm } from "./ManageOrganizationForm"; import { LeadEntry, @@ -21,7 +24,6 @@ type OrganizationData = z.infer; export const OrgInfoPage = () => { const api = useApi("core"); - const { orgRoles } = useAuth(); const [searchParams, setSearchParams] = useSearchParams(); const [manageableOrgs, setManagableOrgs] = useState< OrganizationName[] | null @@ -112,7 +114,11 @@ export const OrgInfoPage = () => { useEffect(() => { (async () => { const appRoles = await getUserRoles("core"); - if (appRoles?.includes(AppRoles.ALL_ORG_MANAGER)) { + const orgRoles = await getCoreOrgRoles(); + if (appRoles === null || orgRoles === null) { + return; + } + if (appRoles.includes(AppRoles.ALL_ORG_MANAGER)) { setManagableOrgs(AllOrganizationNameList); return; } @@ -120,7 +126,7 @@ export const OrgInfoPage = () => { orgRoles.filter((x) => x.role === "LEAD").map((x) => x.org), ); })(); - }, [orgRoles]); + }, []); // Update URL when selected org changes const handleOrgChange = (org: OrganizationName | null) => { From ca8c88d302b4e7376c6088dbe3e52dc49ef3e660 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Mon, 3 Nov 2025 11:06:21 -0600 Subject: [PATCH 2/5] Store currently selected semester for room requests in query parameter --- .../roomRequest/RoomRequestLanding.page.tsx | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/ui/pages/roomRequest/RoomRequestLanding.page.tsx b/src/ui/pages/roomRequest/RoomRequestLanding.page.tsx index 01688ddb..cf7fed98 100644 --- a/src/ui/pages/roomRequest/RoomRequestLanding.page.tsx +++ b/src/ui/pages/roomRequest/RoomRequestLanding.page.tsx @@ -14,13 +14,21 @@ import { type RoomRequestStatus, } from "@common/types/roomRequest"; import { OrganizationName } from "@acm-uiuc/js-shared"; +import { useSearchParams } from "react-router-dom"; export const ManageRoomRequestsPage: React.FC = () => { const api = useApi("core"); - const [semester, setSemester] = useState(null); // TODO: Create a selector for this + const [semester, setSemesterState] = useState(null); const [isLoading, setIsLoading] = useState(false); const nextSemesters = getSemesters(); const semesterOptions = [...getPreviousSemesters(), ...nextSemesters]; + const [searchParams, setSearchParams] = useSearchParams(); + const setSemester = (semester: string | null) => { + setSemesterState(semester); + if (semester) { + setSearchParams({ semester }); + } + }; const createRoomRequest = async ( payload: RoomRequestFormValues, ): Promise => { @@ -45,7 +53,15 @@ export const ManageRoomRequestsPage: React.FC = () => { }; useEffect(() => { - setSemester(nextSemesters[0].value); + const semeseterFromUrl = searchParams.get("semester") as string | null; + if ( + semeseterFromUrl && + semesterOptions.map((x) => x.value).includes(semeseterFromUrl) + ) { + setSemester(semeseterFromUrl); + } else { + setSemester(nextSemesters[0].value); + } }, []); return ( Date: Mon, 3 Nov 2025 11:28:40 -0600 Subject: [PATCH 3/5] Add an e2e test for updating metadata --- tests/e2e/base.ts | 7 ++-- tests/e2e/orgInfo.spec.ts | 72 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 tests/e2e/orgInfo.spec.ts diff --git a/tests/e2e/base.ts b/tests/e2e/base.ts index 80d6a735..b445a949 100644 --- a/tests/e2e/base.ts +++ b/tests/e2e/base.ts @@ -4,6 +4,9 @@ import { GetSecretValueCommand, } from "@aws-sdk/client-secrets-manager"; +export interface RecursiveRecord + extends Record {} + export const getSecretValue = async ( secretId: string, ): Promise | null> => { @@ -71,12 +74,12 @@ export async function getUpcomingEvents() { const data = await fetch( "https://core.aws.qa.acmuiuc.org/api/v1/events?upcomingOnly=true", ); - return (await data.json()) as Record[]; + return (await data.json()) as RecursiveRecord[]; } export async function getAllEvents() { const data = await fetch("https://core.aws.qa.acmuiuc.org/api/v1/events"); - return (await data.json()) as Record[]; + return (await data.json()) as RecursiveRecord[]; } export const test = base.extend<{ becomeUser: (page: Page) => Promise }>({ diff --git a/tests/e2e/orgInfo.spec.ts b/tests/e2e/orgInfo.spec.ts new file mode 100644 index 00000000..55114a15 --- /dev/null +++ b/tests/e2e/orgInfo.spec.ts @@ -0,0 +1,72 @@ +import { expect } from "@playwright/test"; +import { RecursiveRecord, test } from "./base.js"; +import { describe } from "node:test"; + +describe("Organization Info Tests", () => { + test("A user can update org metadata", async ({ page, becomeUser }) => { + const date = new Date().toISOString(); + await becomeUser(page); + await expect( + page.locator("a").filter({ hasText: "Management Portal DEV ENV" }), + ).toBeVisible(); + await expect( + page.locator("a").filter({ hasText: "Organization Info" }), + ).toBeVisible(); + await page.locator("a").filter({ hasText: "Organization Info" }).click(); + await expect(page.getByRole("heading")).toContainText( + "Manage Organization Info", + ); + await page.getByRole("textbox", { name: "Select an organization" }).click(); + await page.getByText("Infrastructure Committee").click(); + await page.getByRole("textbox", { name: "Description" }).click(); + await page + .getByRole("textbox", { name: "Description" }) + .fill(`Populated by E2E tests on ${date}`); + await page + .getByRole("textbox", { name: "Website" }) + .fill(`https://infra.acm.illinois.edu?date=${date}`); + + const existingOtherLink = page.locator("text=Other").first(); + const hasExistingOther = await existingOtherLink + .isVisible() + .catch(() => false); + + if (!hasExistingOther) { + await page.getByRole("button", { name: "Add Link" }).click(); + await page.getByRole("textbox", { name: "Type" }).click(); + await page.getByRole("option", { name: "Other" }).click(); + } + + await page.getByRole("textbox", { name: "URL" }).click(); + await page + .getByRole("textbox", { name: "URL" }) + .fill(`https://infra.acm.illinois.edu/e2e?date=${date}`); + await page + .locator("form") + .getByRole("button", { name: "Save Changes" }) + .click(); + await expect( + page.getByText("Infrastructure Committee updated"), + ).toBeVisible(); + + const data = await fetch( + `https://core.aws.qa.acmuiuc.org/api/v1/organizations?date=${date}`, + ); + const json = (await data.json()) as RecursiveRecord[]; + const infraEntry = json.find((x) => x.id === "Infrastructure Committee"); + + expect(infraEntry).toBeDefined(); + expect(infraEntry?.description).toBe(`Populated by E2E tests on ${date}`); + expect(infraEntry?.website).toBe( + `https://infra.acm.illinois.edu?date=${date}`, + ); + + const links = infraEntry?.links as RecursiveRecord[]; + expect(links).toBeDefined(); + const otherLink = links.find((link) => link.type === "OTHER"); + expect(otherLink).toBeDefined(); + expect(otherLink?.url).toBe( + `https://infra.acm.illinois.edu/e2e?date=${date}`, + ); + }); +}); From 6e766202c0057c37b5373bef1db316bf463bafc6 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Mon, 3 Nov 2025 11:30:31 -0600 Subject: [PATCH 4/5] Fix state management --- src/ui/pages/roomRequest/RoomRequestLanding.page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/pages/roomRequest/RoomRequestLanding.page.tsx b/src/ui/pages/roomRequest/RoomRequestLanding.page.tsx index cf7fed98..83d0dbe2 100644 --- a/src/ui/pages/roomRequest/RoomRequestLanding.page.tsx +++ b/src/ui/pages/roomRequest/RoomRequestLanding.page.tsx @@ -62,7 +62,7 @@ export const ManageRoomRequestsPage: React.FC = () => { } else { setSemester(nextSemesters[0].value); } - }, []); + }, [searchParams, semesterOptions, nextSemesters]); return ( Date: Mon, 3 Nov 2025 11:39:03 -0600 Subject: [PATCH 5/5] Fix terraform version --- Makefile | 6 +++- terraform/envs/prod/.terraform.lock.hcl | 42 ++++++++++++------------- terraform/envs/prod/main.tf | 2 +- terraform/envs/qa/.terraform.lock.hcl | 42 ++++++++++++------------- terraform/envs/qa/main.tf | 2 +- 5 files changed, 49 insertions(+), 45 deletions(-) diff --git a/Makefile b/Makefile index 3cb08b67..0db44cb9 100644 --- a/Makefile +++ b/Makefile @@ -75,7 +75,7 @@ test_unit: install terraform -chdir=terraform/envs/qa init -reconfigure -backend=false -upgrade terraform -chdir=terraform/envs/qa fmt -check terraform -chdir=terraform/envs/qa validate - terraform -chdir=terraform/envs/prod init -reconfigure -backend=false + terraform -chdir=terraform/envs/prod init -reconfigure -backend=false -upgrade terraform -chdir=terraform/envs/prod fmt -check terraform -chdir=terraform/envs/prod validate yarn prettier @@ -96,3 +96,7 @@ prod_health_check: lock_terraform: terraform -chdir=terraform/envs/qa providers lock -platform=windows_amd64 -platform=darwin_amd64 -platform=darwin_arm64 -platform=linux_amd64 -platform=linux_arm64 terraform -chdir=terraform/envs/prod providers lock -platform=windows_amd64 -platform=darwin_amd64 -platform=darwin_arm64 -platform=linux_amd64 -platform=linux_arm64 + +upgrade_terraform: + terraform -chdir=terraform/envs/qa init -reconfigure -backend=false -upgrade + terraform -chdir=terraform/envs/prod init -reconfigure -backend=false -upgrade diff --git a/terraform/envs/prod/.terraform.lock.hcl b/terraform/envs/prod/.terraform.lock.hcl index 59086402..9d686f85 100644 --- a/terraform/envs/prod/.terraform.lock.hcl +++ b/terraform/envs/prod/.terraform.lock.hcl @@ -25,29 +25,29 @@ provider "registry.terraform.io/hashicorp/archive" { } provider "registry.terraform.io/hashicorp/aws" { - version = "6.18.0" - constraints = "~> 6.18.0" + version = "6.19.0" + constraints = "6.19.0" hashes = [ - "h1:5MWlORDu2wCba8bkjS9aYYMm62FvwPd4kY1+fQcpi0A=", - "h1:7mCQxmPY38UY0Zp4+0JqEFzWOI3ONiJFuUXmPaMlFEM=", - "h1:FFesHJDunpHRJOJsmDnMNfufZEV4pM9LshzUjtrv1dU=", - "h1:kYe4gKHOceVoJE3tShRYRdHRD3FTJPkIE5mRrejTrBI=", - "h1:xfUy7PY+vs2tI/3T9Btug0X7UQFbp1UXkX04DPfBoOQ=", - "zh:0f22732930b4f51e345a2273cd92bd419a33d07bb6add096be09771eb391ed0e", - "zh:164582e9be87e6ac2765d414269439ee2bc6f5a831e0d57d204e88006bce6cf7", - "zh:47b0b230d100a270f5cb5a4ed7b2b0b3caef66cfd407617da558318279a8bb1b", - "zh:73ac86ba28ec32357abbfd60742b9cdebfda59f65161f64f8246de1eaca45f38", - "zh:7c68d3a83da55bf63972d50c4c5a6759e0c44ed74aa7fb3d70dd7ef1850cab34", - "zh:8b0873c78e62e7930f2d0f9f232cacb6e3b34e6e4ec41989d43c591529a3f9d7", - "zh:8e8017c67317811339fed4a5fe2568b025bdd3aeaad4e16186fbb24393d09d2d", + "h1:5oDrH7uIKjvBIDd1YKaZwbB5NiJnSafMaiNaKMTy80k=", + "h1:5qq2jk+G9fymBqnOmtHR30L6TLMlMoZ7TsSXOAYl0qU=", + "h1:Dx/amdFxKZ/nPAGjjd1poAtBlD1C3RSP9aM7fy/n+pg=", + "h1:JWAZB+tbwbiFhdc/T8cfPjrMkt4zJdWTqtcRZly7ViQ=", + "h1:Nfft4GgGwEE/9aAQz+LhPf3I5YpXN5gWm0A6+0wfIQs=", + "zh:221061660f519f09e9fcd3bbe1fc5c63e81d997e8e9e759984c80095403d7fd6", + "zh:2436e7f7de4492998d7badfae37f88b042ce993f3fdb411ba7f7a47ff4cc66a2", + "zh:49e78e889bf5f9378dfacb08040553bf1529171222eda931e31fcdeac223e802", + "zh:5a07c255ac8694aebe3e166cc3d0ae5f64e0502d47610fd42be22fd907cb81fa", + "zh:68180e2839faba80b64a5e9eb03cfcc50c75dcf0adb24c6763f97dade8311835", + "zh:6c7ae7fb8d51fecdd000bdcfec60222c1f0aeac41dacf1c33aa16609e6ccaf43", + "zh:6ebea9b2eb48fc44ee5674797a5f3b093640b054803495c10a1e558ccd8fee2b", + "zh:8010d1ca1ab0f89732da3c56351779b6728707270c935bf5fd7d99fdf69bc1da", + "zh:8ca7544dbe3b2499d0179fd289e536aedac25115855434d76a4dc342409d335a", "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:ae7cc2d08ab9c6ced8291a7521ab3783d4e55cf4e18a36c8a487c6dfd3fb064e", - "zh:ce0ccfed27f03d7366ba4a199ded19f9cc47b8a8a47922263b29eb9d4b42ce66", - "zh:d165703f775c3f38891a53451fc2c11c68bd26262506aeb5354d8a75cabdfc3d", - "zh:dc57c8e2b1307bfc05bc4b525de00ada58ca14b7a1737c969413833f4db50fc6", - "zh:df09c6125eb353e4fe5ba73c16c88617a1c4b41c7fca81bd257884966d942b54", - "zh:df9c94ed15d88b921876c9cd52942693743d5b0b5b9650bae5498117a44aae4e", - "zh:fbcb0c7a5aff9eb794093c94fd98989739f0e0d377a6e8a15b4910703c13e228", + "zh:c6ed10fb06f561d6785c10ff0f0134b7bfcb9964f1bc38ed8b263480bc3cebc0", + "zh:d011d703a3b22f7e296baa8ddfd4d550875daa3f551a133988f843d6c8e6ec38", + "zh:eceb5a8e929b4b0f26e437d1181aeebfb81f376902e0677ead9b886bb41e7c08", + "zh:eda96ae2f993df469cf5dfeecd842e922de97b8a8600e7d197d884ca5179ad2f", + "zh:fb229392236c0c76214d157bb1c7734ded4fa1221e9ef7831d67258950246ff3", ] } diff --git a/terraform/envs/prod/main.tf b/terraform/envs/prod/main.tf index 35351753..b6e8e3fa 100644 --- a/terraform/envs/prod/main.tf +++ b/terraform/envs/prod/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 6.18.0" + version = "= 6.19.0" } } diff --git a/terraform/envs/qa/.terraform.lock.hcl b/terraform/envs/qa/.terraform.lock.hcl index 59086402..9d686f85 100644 --- a/terraform/envs/qa/.terraform.lock.hcl +++ b/terraform/envs/qa/.terraform.lock.hcl @@ -25,29 +25,29 @@ provider "registry.terraform.io/hashicorp/archive" { } provider "registry.terraform.io/hashicorp/aws" { - version = "6.18.0" - constraints = "~> 6.18.0" + version = "6.19.0" + constraints = "6.19.0" hashes = [ - "h1:5MWlORDu2wCba8bkjS9aYYMm62FvwPd4kY1+fQcpi0A=", - "h1:7mCQxmPY38UY0Zp4+0JqEFzWOI3ONiJFuUXmPaMlFEM=", - "h1:FFesHJDunpHRJOJsmDnMNfufZEV4pM9LshzUjtrv1dU=", - "h1:kYe4gKHOceVoJE3tShRYRdHRD3FTJPkIE5mRrejTrBI=", - "h1:xfUy7PY+vs2tI/3T9Btug0X7UQFbp1UXkX04DPfBoOQ=", - "zh:0f22732930b4f51e345a2273cd92bd419a33d07bb6add096be09771eb391ed0e", - "zh:164582e9be87e6ac2765d414269439ee2bc6f5a831e0d57d204e88006bce6cf7", - "zh:47b0b230d100a270f5cb5a4ed7b2b0b3caef66cfd407617da558318279a8bb1b", - "zh:73ac86ba28ec32357abbfd60742b9cdebfda59f65161f64f8246de1eaca45f38", - "zh:7c68d3a83da55bf63972d50c4c5a6759e0c44ed74aa7fb3d70dd7ef1850cab34", - "zh:8b0873c78e62e7930f2d0f9f232cacb6e3b34e6e4ec41989d43c591529a3f9d7", - "zh:8e8017c67317811339fed4a5fe2568b025bdd3aeaad4e16186fbb24393d09d2d", + "h1:5oDrH7uIKjvBIDd1YKaZwbB5NiJnSafMaiNaKMTy80k=", + "h1:5qq2jk+G9fymBqnOmtHR30L6TLMlMoZ7TsSXOAYl0qU=", + "h1:Dx/amdFxKZ/nPAGjjd1poAtBlD1C3RSP9aM7fy/n+pg=", + "h1:JWAZB+tbwbiFhdc/T8cfPjrMkt4zJdWTqtcRZly7ViQ=", + "h1:Nfft4GgGwEE/9aAQz+LhPf3I5YpXN5gWm0A6+0wfIQs=", + "zh:221061660f519f09e9fcd3bbe1fc5c63e81d997e8e9e759984c80095403d7fd6", + "zh:2436e7f7de4492998d7badfae37f88b042ce993f3fdb411ba7f7a47ff4cc66a2", + "zh:49e78e889bf5f9378dfacb08040553bf1529171222eda931e31fcdeac223e802", + "zh:5a07c255ac8694aebe3e166cc3d0ae5f64e0502d47610fd42be22fd907cb81fa", + "zh:68180e2839faba80b64a5e9eb03cfcc50c75dcf0adb24c6763f97dade8311835", + "zh:6c7ae7fb8d51fecdd000bdcfec60222c1f0aeac41dacf1c33aa16609e6ccaf43", + "zh:6ebea9b2eb48fc44ee5674797a5f3b093640b054803495c10a1e558ccd8fee2b", + "zh:8010d1ca1ab0f89732da3c56351779b6728707270c935bf5fd7d99fdf69bc1da", + "zh:8ca7544dbe3b2499d0179fd289e536aedac25115855434d76a4dc342409d335a", "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:ae7cc2d08ab9c6ced8291a7521ab3783d4e55cf4e18a36c8a487c6dfd3fb064e", - "zh:ce0ccfed27f03d7366ba4a199ded19f9cc47b8a8a47922263b29eb9d4b42ce66", - "zh:d165703f775c3f38891a53451fc2c11c68bd26262506aeb5354d8a75cabdfc3d", - "zh:dc57c8e2b1307bfc05bc4b525de00ada58ca14b7a1737c969413833f4db50fc6", - "zh:df09c6125eb353e4fe5ba73c16c88617a1c4b41c7fca81bd257884966d942b54", - "zh:df9c94ed15d88b921876c9cd52942693743d5b0b5b9650bae5498117a44aae4e", - "zh:fbcb0c7a5aff9eb794093c94fd98989739f0e0d377a6e8a15b4910703c13e228", + "zh:c6ed10fb06f561d6785c10ff0f0134b7bfcb9964f1bc38ed8b263480bc3cebc0", + "zh:d011d703a3b22f7e296baa8ddfd4d550875daa3f551a133988f843d6c8e6ec38", + "zh:eceb5a8e929b4b0f26e437d1181aeebfb81f376902e0677ead9b886bb41e7c08", + "zh:eda96ae2f993df469cf5dfeecd842e922de97b8a8600e7d197d884ca5179ad2f", + "zh:fb229392236c0c76214d157bb1c7734ded4fa1221e9ef7831d67258950246ff3", ] } diff --git a/terraform/envs/qa/main.tf b/terraform/envs/qa/main.tf index 4a3c3c8e..f38ffbbe 100644 --- a/terraform/envs/qa/main.tf +++ b/terraform/envs/qa/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 6.18.0" + version = "= 6.19.0" } }