diff --git a/package.json b/package.json index a9d428df..a3f6e8c8 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "test:e2e-ui": "playwright test --ui" }, "dependencies": { - "@acm-uiuc/js-shared": "^2.4.0" + "@acm-uiuc/js-shared": "^3.1.0" }, "devDependencies": { "@eslint/compat": "^1.3.2", diff --git a/src/api/components/index.ts b/src/api/components/index.ts index 32f3d349..154a282a 100644 --- a/src/api/components/index.ts +++ b/src/api/components/index.ts @@ -1,7 +1,7 @@ import { AppRoleHumanMapper, AppRoles } from "common/roles.js"; import { FastifyZodOpenApiSchema } from "fastify-zod-openapi"; import * as z from "zod/v4"; -import { AllOrganizationList } from "@acm-uiuc/js-shared"; +import { AllOrganizationNameList } from "@acm-uiuc/js-shared"; export { illinoisSemesterId as semesterId, illinoisNetId, @@ -21,7 +21,7 @@ export const groupId = z.string().min(1).meta({ }); export const acmCoreOrganization = z - .enum(AllOrganizationList as [string, ...string[]]) + .enum(AllOrganizationNameList as [string, ...string[]]) .meta({ description: "ACM Organization", id: "AcmOrganization", diff --git a/src/api/functions/organizations.ts b/src/api/functions/organizations.ts index 682248cb..4244b18d 100644 --- a/src/api/functions/organizations.ts +++ b/src/api/functions/organizations.ts @@ -1,4 +1,4 @@ -import { AllOrganizationList } from "@acm-uiuc/js-shared"; +import { AllOrganizationNameList, OrganizationName } from "@acm-uiuc/js-shared"; import { QueryCommand, ScanCommand, @@ -154,14 +154,14 @@ export async function getUserOrgRoles({ logger.warn(`Invalid role in role definition: ${JSON.stringify(item)}`); continue; } - if (!AllOrganizationList.includes(org)) { + if (!AllOrganizationNameList.includes(org as OrganizationName)) { logger.warn(`Invalid org in role definition: ${JSON.stringify(item)}`); continue; } cleanedRoles.push({ org, role, - } as { org: (typeof AllOrganizationList)[number]; role: OrgRole }); + } as { org: OrganizationName; role: OrgRole }); } return cleanedRoles; } catch (e) { diff --git a/src/api/routes/ics.ts b/src/api/routes/ics.ts index 690ccd8d..4081afd6 100644 --- a/src/api/routes/ics.ts +++ b/src/api/routes/ics.ts @@ -14,7 +14,7 @@ import ical, { } from "ical-generator"; import moment from "moment"; import { getVtimezoneComponent } from "@touch4it/ical-timezones"; -import { CoreOrganizationList } from "@acm-uiuc/js-shared"; +import { AllOrganizationNameList, OrganizationName } from "@acm-uiuc/js-shared"; import { CLIENT_HTTP_CACHE_POLICY, EventRepeatOptions } from "./events.js"; import rateLimiter from "api/plugins/rateLimiter.js"; import { getCacheCounter } from "api/functions/cache.js"; @@ -43,7 +43,7 @@ function generateHostName(host: string) { const icalPlugin: FastifyPluginAsync = async (fastify, _options) => { fastify.register(rateLimiter, { - limit: CoreOrganizationList.length, + limit: AllOrganizationNameList.length, duration: 30, rateLimitIdentifier: "ical", }); @@ -87,7 +87,7 @@ const icalPlugin: FastifyPluginAsync = async (fastify, _options) => { reply.header("etag", etag); } if (host) { - if (!CoreOrganizationList.includes(host)) { + if (!AllOrganizationNameList.includes(host as OrganizationName)) { throw new ValidationError({ message: `Invalid host parameter "${host}" in path.`, }); diff --git a/src/api/routes/organizations.ts b/src/api/routes/organizations.ts index 6798db25..03e22e80 100644 --- a/src/api/routes/organizations.ts +++ b/src/api/routes/organizations.ts @@ -1,8 +1,8 @@ import { FastifyError, FastifyPluginAsync } from "fastify"; import { - ACMOrganization, - AllOrganizationList, - OrganizationShortIdentifierMapping, + AllOrganizationNameList, + getOrgByName, + Organizations, } from "@acm-uiuc/js-shared"; import rateLimiter from "api/plugins/rateLimiter.js"; import { withRoles, withTags } from "api/components/index.js"; @@ -142,7 +142,7 @@ const organizationsPlugin: FastifyPluginAsync = async (fastify, _options) => { isAuthenticated = false; } } - const promises = AllOrganizationList.map((x) => + const promises = AllOrganizationNameList.map((x) => getOrgInfo({ id: x, dynamoClient: fastify.dynamoClient, @@ -161,7 +161,7 @@ const organizationsPlugin: FastifyPluginAsync = async (fastify, _options) => { leadsEntraGroupId: undefined, })); } - const unknownIds = AllOrganizationList.filter( + const unknownIds = AllOrganizationNameList.filter( (x) => !successIds.includes(x), ).map((x) => ({ id: x })); return reply.send([...successOnly, ...unknownIds]); @@ -185,7 +185,7 @@ const organizationsPlugin: FastifyPluginAsync = async (fastify, _options) => { "Get information about a specific ACM @ UIUC sub-organization.", params: z.object({ orgId: z - .enum(AllOrganizationList) + .enum(AllOrganizationNameList) .meta({ description: "ACM @ UIUC organization to query." }), }), response: { @@ -236,7 +236,7 @@ const organizationsPlugin: FastifyPluginAsync = async (fastify, _options) => { summary: "Set metadata for an ACM @ UIUC sub-organization.", params: z.object({ orgId: z - .enum(AllOrganizationList) + .enum(AllOrganizationNameList) .meta({ description: "ACM @ UIUC organization to modify." }), }), body: setOrganizationMetaBody, @@ -325,7 +325,7 @@ const organizationsPlugin: FastifyPluginAsync = async (fastify, _options) => { summary: "Set leads for an ACM @ UIUC sub-organization.", params: z.object({ orgId: z - .enum(AllOrganizationList) + .enum(AllOrganizationNameList) .meta({ description: "ACM @ UIUC organization to modify." }), }), body: patchOrganizationLeadsBody, @@ -424,7 +424,13 @@ const organizationsPlugin: FastifyPluginAsync = async (fastify, _options) => { const shouldCreateNewEntraGroup = !entraGroupId; const grpDisplayName = `${request.params.orgId} Admin`; - const grpShortName = `${OrganizationShortIdentifierMapping[request.params.orgId as keyof typeof OrganizationShortIdentifierMapping]}-adm`; + const orgInfo = getOrgByName(request.params.orgId); + if (!orgInfo) { + throw new InternalServerError({ + message: `Organization ${request.params.orgId} could not be resolved.`, + }); + } + const grpShortName = `${orgInfo?.shortcode}-adm`; // Create Entra group if needed if (shouldCreateNewEntraGroup) { diff --git a/src/common/policies/events.ts b/src/common/policies/events.ts index 66cc5503..4c3a5926 100644 --- a/src/common/policies/events.ts +++ b/src/common/policies/events.ts @@ -1,12 +1,12 @@ import * as z from "zod/v4"; import { createPolicy } from "./evaluator.js"; -import { CoreOrganizationList } from "@acm-uiuc/js-shared"; +import { AllOrganizationNameList, OrganizationName } from "@acm-uiuc/js-shared"; import { FastifyRequest } from "fastify"; export const hostRestrictionPolicy = createPolicy( "EventsHostRestrictionPolicy", - z.object({ host: z.array(z.enum(CoreOrganizationList)) }), - (request: FastifyRequest & {username?: string;}, params) => { + z.object({ host: z.array(z.enum(AllOrganizationNameList)) }), + (request: FastifyRequest & { username?: string; }, params) => { if (request.method === "GET") { return { allowed: true, @@ -21,7 +21,7 @@ export const hostRestrictionPolicy = createPolicy( cacheKey: null }; } - const typedBody = request.body as {host: string;featured: boolean;}; + const typedBody = request.body as { host: string; featured: boolean; }; if (!typedBody || !typedBody["host"]) { return { allowed: true, @@ -36,7 +36,7 @@ export const hostRestrictionPolicy = createPolicy( cacheKey: request.username || null }; } - if (!params.host.includes(typedBody["host"])) { + if (!params.host.includes(typedBody["host"] as OrganizationName)) { return { allowed: false, message: `Denied by policy "EventsHostRestrictionPolicy". Host must be one of: ${params.host.toString()}.`, @@ -49,4 +49,4 @@ export const hostRestrictionPolicy = createPolicy( cacheKey: request.username || null }; } -); \ No newline at end of file +); diff --git a/src/common/roles.ts b/src/common/roles.ts index 1f0f8e0e..8416cc69 100644 --- a/src/common/roles.ts +++ b/src/common/roles.ts @@ -1,4 +1,4 @@ -import { AllOrganizationList } from "@acm-uiuc/js-shared"; +import { AllOrganizationNameList } from "@acm-uiuc/js-shared"; /* eslint-disable import/prefer-default-export */ export const runEnvironments = ["dev", "prod"] as const; @@ -27,7 +27,7 @@ export const PSUEDO_ROLES = [AppRoles.AT_LEAST_ONE_ORG_MANAGER] export const orgRoles = ["LEAD", "MEMBER"] as const; export type OrgRole = typeof orgRoles[number]; export type OrgRoleDefinition = { - org: typeof AllOrganizationList[number], + org: typeof AllOrganizationNameList[number], role: OrgRole } diff --git a/src/common/types/organizations.ts b/src/common/types/organizations.ts index 421c9b2e..ccce5ece 100644 --- a/src/common/types/organizations.ts +++ b/src/common/types/organizations.ts @@ -1,4 +1,4 @@ -import { AllOrganizationList } from "@acm-uiuc/js-shared"; +import { AllOrganizationNameList } from "@acm-uiuc/js-shared"; import { AppRoleHumanMapper, AppRoles } from "../roles.js"; import { z } from "zod/v4"; @@ -28,7 +28,7 @@ export const orgLinkEntry = z.object({ export const enforcedOrgLeadEntry = orgLeadEntry.extend({ name: z.string().min(1), title: z.string().min(1) }) export const getOrganizationInfoResponse = z.object({ - id: z.enum(AllOrganizationList), + id: z.enum(AllOrganizationNameList), description: z.optional(z.string()), website: z.optional(z.url()), leads: z.optional(z.array(orgLeadEntry)), diff --git a/src/common/types/roomRequest.ts b/src/common/types/roomRequest.ts index 0693f238..1334a021 100644 --- a/src/common/types/roomRequest.ts +++ b/src/common/types/roomRequest.ts @@ -1,5 +1,5 @@ import * as z from "zod/v4"; -import { AllOrganizationList } from "@acm-uiuc/js-shared"; +import { AllOrganizationNameList } from "@acm-uiuc/js-shared"; import { illinoisSemesterId } from "./generic.js" export const eventThemeOptions = [ @@ -146,7 +146,7 @@ export const roomRequestPostResponse = z.object({ }); export const roomRequestBaseSchema = z.object({ - host: z.enum(AllOrganizationList), + host: z.enum(AllOrganizationNameList), title: z.string().min(2, "Title must have at least 2 characters"), semester: illinoisSemesterId }); diff --git a/src/ui/pages/events/ManageEvent.page.tsx b/src/ui/pages/events/ManageEvent.page.tsx index 7d294943..6e07526d 100644 --- a/src/ui/pages/events/ManageEvent.page.tsx +++ b/src/ui/pages/events/ManageEvent.page.tsx @@ -22,7 +22,7 @@ import { useNavigate, useParams } from "react-router-dom"; import * as z from "zod/v4"; import { AuthGuard } from "@ui/components/AuthGuard"; import { useApi } from "@ui/util/api"; -import { AllOrganizationList as orgList } from "@acm-uiuc/js-shared"; +import { AllOrganizationNameList as orgList } from "@acm-uiuc/js-shared"; import { AppRoles } from "@common/roles"; import { EVENT_CACHED_DURATION } from "@common/config"; import { @@ -176,7 +176,7 @@ export const ManageEventPage: React.FC = () => { end: new Date(startDate + 3.6e6), location: "ACM Room (Siebel CS 1104)", locationLink: "https://maps.app.goo.gl/dwbBBBkfjkgj8gvA8", - host: userPrimaryOrg, + host: userPrimaryOrg || "", featured: false, repeats: undefined, repeatEnds: undefined, diff --git a/src/ui/pages/organization/OrgInfo.page.tsx b/src/ui/pages/organization/OrgInfo.page.tsx index 96b11b33..8b0a15c7 100644 --- a/src/ui/pages/organization/OrgInfo.page.tsx +++ b/src/ui/pages/organization/OrgInfo.page.tsx @@ -6,7 +6,7 @@ import { AppRoles } from "@common/roles"; import { notifications } from "@mantine/notifications"; import { IconAlertCircle } from "@tabler/icons-react"; import FullScreenLoader from "@ui/components/AuthContext/LoadingScreen"; -import { AllOrganizationList } from "@acm-uiuc/js-shared"; +import { AllOrganizationNameList, OrganizationName } from "@acm-uiuc/js-shared"; import { useAuth } from "@ui/components/AuthContext"; import { ManageOrganizationForm } from "./ManageOrganizationForm"; import { @@ -17,21 +17,24 @@ import { import * as z from "zod/v4"; import { useSearchParams } from "react-router-dom"; -type AcmOrg = (typeof AllOrganizationList)[number]; type OrganizationData = z.infer; export const OrgInfoPage = () => { const api = useApi("core"); const { orgRoles } = useAuth(); const [searchParams, setSearchParams] = useSearchParams(); - const [manageableOrgs, setManagableOrgs] = useState(null); + const [manageableOrgs, setManagableOrgs] = useState< + OrganizationName[] | null + >(null); // Get org from URL query parameter - const orgFromUrl = searchParams.get("org") as AcmOrg | null; - const [selectedOrg, setSelectedOrg] = useState(orgFromUrl); + const orgFromUrl = searchParams.get("org") as OrganizationName | null; + const [selectedOrg, setSelectedOrg] = useState( + orgFromUrl, + ); const getOrganizationData = async ( - org: AcmOrg, + org: OrganizationName, ): Promise => { try { const response = await api.get( @@ -51,7 +54,7 @@ export const OrgInfoPage = () => { }; const updateOrganizationData = async ( - org: AcmOrg, + org: OrganizationName, data: OrganizationData, ): Promise => { try { @@ -78,7 +81,7 @@ export const OrgInfoPage = () => { } }; const updateLeads = async ( - org: AcmOrg, + org: OrganizationName, toAdd: LeadEntry[], toRemove: string[], ): Promise => { @@ -110,7 +113,7 @@ export const OrgInfoPage = () => { (async () => { const appRoles = await getUserRoles("core"); if (appRoles?.includes(AppRoles.ALL_ORG_MANAGER)) { - setManagableOrgs(AllOrganizationList); + setManagableOrgs(AllOrganizationNameList); return; } setManagableOrgs( @@ -120,7 +123,7 @@ export const OrgInfoPage = () => { }, [orgRoles]); // Update URL when selected org changes - const handleOrgChange = (org: AcmOrg | null) => { + const handleOrgChange = (org: OrganizationName | null) => { setSelectedOrg(org); if (org) { setSearchParams({ org }); @@ -182,7 +185,7 @@ export const OrgInfoPage = () => { placeholder="Select organization" data={manageableOrgs} value={selectedOrg} - onChange={handleOrgChange} + onChange={(i) => handleOrgChange(i as OrganizationName)} mt="md" searchable maw={400} @@ -192,7 +195,9 @@ export const OrgInfoPage = () => { {selectedOrg && ( + getOrganizationData(i as OrganizationName) + } updateOrganizationData={(data) => updateOrganizationData(selectedOrg, data) } diff --git a/src/ui/pages/roomRequest/NewRoomRequest.tsx b/src/ui/pages/roomRequest/NewRoomRequest.tsx index 8de53429..8c3f3df5 100644 --- a/src/ui/pages/roomRequest/NewRoomRequest.tsx +++ b/src/ui/pages/roomRequest/NewRoomRequest.tsx @@ -18,7 +18,7 @@ import { } from "@mantine/core"; import { useForm } from "@mantine/form"; import { DateInput, DateTimePicker } from "@mantine/dates"; -import { AllOrganizationList } from "@acm-uiuc/js-shared"; +import { AllOrganizationNameList } from "@acm-uiuc/js-shared"; import { eventThemeOptions, spaceTypeOptions, @@ -405,7 +405,7 @@ const NewRoomRequest: React.FC = ({ placeholder="Select host organization" withAsterisk searchable - data={AllOrganizationList.map((org) => ({ + data={AllOrganizationNameList.map((org) => ({ value: org, label: org, }))} diff --git a/src/ui/pages/roomRequest/RoomRequestLanding.page.tsx b/src/ui/pages/roomRequest/RoomRequestLanding.page.tsx index 13756e98..01688ddb 100644 --- a/src/ui/pages/roomRequest/RoomRequestLanding.page.tsx +++ b/src/ui/pages/roomRequest/RoomRequestLanding.page.tsx @@ -13,6 +13,7 @@ import { RoomRequestPostResponse, type RoomRequestStatus, } from "@common/types/roomRequest"; +import { OrganizationName } from "@acm-uiuc/js-shared"; export const ManageRoomRequestsPage: React.FC = () => { const api = useApi("core"); @@ -34,7 +35,7 @@ export const ManageRoomRequestsPage: React.FC = () => { { requestId: string; title: string; - host: string; + host: OrganizationName; status: RoomRequestStatus; }[] >( diff --git a/src/ui/util/index.ts b/src/ui/util/index.ts index bf844542..b0fde5f2 100644 --- a/src/ui/util/index.ts +++ b/src/ui/util/index.ts @@ -1,4 +1,4 @@ -import { ACMOrganization } from "@acm-uiuc/js-shared"; +import { OrganizationName } from "@acm-uiuc/js-shared"; import { OrgRoleDefinition } from "@common/roles"; export type NonEmptyArray = [T, ...T[]]; @@ -15,13 +15,13 @@ export function min(items: NonEmptyArray): T { export function getPrimarySuggestedOrg( orgRoles: OrgRoleDefinition[] | null | undefined, -): ACMOrganization { +): OrganizationName | null { if (!orgRoles || orgRoles.length === 0) { - return ""; + return null; } const leadOrgs = orgRoles.filter((x) => x.role === "LEAD").map((x) => x.org); if (leadOrgs.length > 0) { - return min(leadOrgs as NonEmptyArray); + return min(leadOrgs as NonEmptyArray); } - return ""; + return null; } diff --git a/tests/live/ical.test.ts b/tests/live/ical.test.ts index 890d1dca..39e679d5 100644 --- a/tests/live/ical.test.ts +++ b/tests/live/ical.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vitest"; -import { CoreOrganizationList } from "@acm-uiuc/js-shared"; +import { AllOrganizationNameList } from "@acm-uiuc/js-shared"; import ical from "node-ical"; import { getBaseEndpoint } from "./utils.js"; const baseEndpoint = getBaseEndpoint(); @@ -26,27 +26,6 @@ const fetchWithRateLimit = async (url: string) => { return response; }; -describe( - "Get calendars per organization with rate limit handling", - { timeout: 450000 }, - async () => { - for (const org of CoreOrganizationList) { - test(`Get ${org} calendar`, async () => { - await delay(Math.random() * 200); - const response = await fetchWithRateLimit( - `${baseEndpoint}/api/v1/ical/${org}`, - ); - expect(response.status).toBe(200); - expect(response.headers.get("Content-Disposition")).toEqual( - 'attachment; filename="calendar.ics"', - ); - const calendar = ical.sync.parseICS(await response.text()); - expect(calendar["vcalendar"]["type"]).toEqual("VCALENDAR"); - }); - } - }, -); - test("Check that the ACM host works", { timeout: 45000 }, async () => { const response = await fetchWithRateLimit( `${baseEndpoint.replace("core", "ical")}/ACM`, diff --git a/yarn.lock b/yarn.lock index 6cf2ee2e..cdee839e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@acm-uiuc/js-shared@^2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@acm-uiuc/js-shared/-/js-shared-2.4.0.tgz#c8a28c8189c7a3847d8a06cb918b9f4ce256f8de" - integrity sha512-hAjns2jT0MXUdtYuxVEn3ob/sTPnCsOeBeJXt+PgRQqhg+Cu09E82Y/68FNbTPlKi/YSzhQEToDXRuQtGM+VRA== +"@acm-uiuc/js-shared@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@acm-uiuc/js-shared/-/js-shared-3.1.0.tgz#3d5c82ab28f6cb354c30fa3f2adb71dfc8bf9f6f" + integrity sha512-/j0C0L9dIw15D3H2XRzfEWEd19Z4BdFEDxdo7BHm3ZFuQTcBpfHmbcl2qhHRZ1ax3PQUL7qRZzvlYX0CULjZEw== "@adobe/css-tools@^4.4.0": version "4.4.3"