Skip to content

Commit a885ba6

Browse files
committed
fix code
1 parent c0a3142 commit a885ba6

File tree

5 files changed

+142
-31
lines changed

5 files changed

+142
-31
lines changed

src/api/functions/membership.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import Redis from "ioredis";
2121
import { getKey, setKey } from "./redisCache.js";
2222
import { FastifyBaseLogger } from "fastify";
2323
import type pino from "pino";
24+
import { createAuditLogEntry } from "./auditLog.js";
25+
import { Modules } from "common/modules.js";
2426

2527
export const MEMBER_CACHE_SECONDS = 43200; // 12 hours
2628

@@ -30,12 +32,14 @@ export async function patchExternalMemberList({
3032
remove: oldRemove,
3133
clients: { dynamoClient, redisClient },
3234
logger,
35+
auditLogData: { actor, requestId },
3336
}: {
3437
listId: string;
3538
add: string[];
3639
remove: string[];
3740
clients: { dynamoClient: DynamoDBClient; redisClient: Redis.default };
3841
logger: pino.Logger | FastifyBaseLogger;
42+
auditLogData: { actor: string; requestId: string };
3943
}) {
4044
const listId = oldListId.toLowerCase();
4145
const add = oldAdd.map((x) => x.toLowerCase());
@@ -104,11 +108,41 @@ export async function patchExternalMemberList({
104108
logger,
105109
}),
106110
);
111+
const auditLogPromises = [];
112+
if (add.length > 0) {
113+
auditLogPromises.push(
114+
createAuditLogEntry({
115+
dynamoClient,
116+
entry: {
117+
module: Modules.EXTERNAL_MEMBERSHIP,
118+
actor,
119+
requestId,
120+
message: `Added ${add.length} member(s) to target list.`,
121+
target: listId,
122+
},
123+
}),
124+
);
125+
}
126+
if (remove.length > 0) {
127+
auditLogPromises.push(
128+
createAuditLogEntry({
129+
dynamoClient,
130+
entry: {
131+
module: Modules.EXTERNAL_MEMBERSHIP,
132+
actor,
133+
requestId,
134+
message: `Removed ${remove.length} member(s) from target list.`,
135+
target: listId,
136+
},
137+
}),
138+
);
139+
}
107140
await Promise.all([
108141
...removeCacheInvalidation,
109142
...addCacheInvalidation,
110143
...batchPromises,
111144
]);
145+
await Promise.all(auditLogPromises);
112146
}
113147
export async function getExternalMemberList(
114148
list: string,

src/api/routes/membership.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,10 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
307307
listId,
308308
clients: { dynamoClient, redisClient },
309309
logger: request.log,
310+
auditLogData: {
311+
actor: request.username!,
312+
requestId: request.id,
313+
},
310314
});
311315
return reply.status(201).send();
312316
},

src/common/modules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export enum Modules {
1010
AUDIT_LOG = "auditLog",
1111
API_KEY = "apiKey",
1212
ROOM_RESERVATIONS = "roomReservations",
13+
EXTERNAL_MEMBERSHIP = "externalMembership",
1314
}
1415

1516

@@ -25,4 +26,5 @@ export const ModulesToHumanName: Record<Modules, string> = {
2526
[Modules.AUDIT_LOG]: "Audit Log",
2627
[Modules.API_KEY]: "API Keys",
2728
[Modules.ROOM_RESERVATIONS]: "Room Reservations",
29+
[Modules.EXTERNAL_MEMBERSHIP]: "External Membership",
2830
}

src/common/types/logs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Modules } from "../modules.js";
22
import * as z from "zod/v4";
33

44
export const loggingEntry = z.object({
5-
module: z.nativeEnum(Modules),
5+
module: z.enum(Modules),
66
actor: z.string().min(1),
77
target: z.string().min(1),
88
requestId: z.optional(z.string().min(1).uuid()),

src/ui/pages/externalMembership/ExternalMemberListManagement.tsx

Lines changed: 101 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,16 @@ import {
1313
Select,
1414
Box,
1515
Textarea,
16+
Accordion,
17+
ScrollArea,
18+
Stack,
1619
} from "@mantine/core";
1720
import { IconUserPlus, IconTrash } from "@tabler/icons-react";
1821
import { notifications } from "@mantine/notifications";
1922
import { illinoisNetId } from "@common/types/generic";
2023
import { AuthGuard } from "@ui/components/AuthGuard";
2124
import { AppRoles } from "@common/roles";
25+
import pluralize from "pluralize";
2226

2327
interface ExternalMemberListManagementProps {
2428
fetchMembers: (listId: string) => Promise<string[]>;
@@ -32,6 +36,7 @@ interface ExternalMemberListManagementProps {
3236
}
3337

3438
const ITEMS_PER_PAGE = 10;
39+
const CHANGE_DISPLAY_LIMIT = 10;
3540

3641
const ExternalMemberListManagement: React.FC<
3742
ExternalMemberListManagementProps
@@ -202,25 +207,40 @@ const ExternalMemberListManagement: React.FC<
202207
};
203208

204209
const handleReplaceList = () => {
205-
const rawNetIds = replacementList
210+
const allLines = replacementList
206211
.split("\n")
207-
.map((id) => id.trim())
212+
.map((line) => line.trim())
208213
.filter(Boolean);
209214
const validNetIds = new Set<string>();
210215
const invalidEntries: string[] = [];
211216

212-
for (const netId of rawNetIds) {
213-
if (illinoisNetId.safeParse(netId).success) {
214-
validNetIds.add(netId);
217+
for (const line of allLines) {
218+
// Rule: If it contains "@" but is not a valid "@illinois.edu" email, it's invalid.
219+
if (line.includes("@") && !line.endsWith("@illinois.edu")) {
220+
invalidEntries.push(line);
221+
continue;
222+
}
223+
224+
// Strip the domain to get the potential NetID for Zod validation
225+
const potentialNetId = line.replace("@illinois.edu", "");
226+
227+
if (illinoisNetId.safeParse(potentialNetId).success) {
228+
validNetIds.add(potentialNetId);
215229
} else {
216-
invalidEntries.push(netId);
230+
invalidEntries.push(line); // Add the original failing line
217231
}
218232
}
219233

220234
if (invalidEntries.length > 0) {
235+
const pluralize = (singular: string, plural: string, count: number) =>
236+
count === 1 ? singular : plural;
237+
const verbIs = pluralize("is", "are", invalidEntries.length);
238+
const verbHas = pluralize("has", "have", invalidEntries.length);
239+
const entriesStr = invalidEntries.join(", ");
240+
221241
notifications.show({
222242
title: "Invalid Entries Skipped",
223-
message: `The following ${invalidEntries.length} entries were invalid and have been ignored.`,
243+
message: `${entriesStr} ${verbIs} invalid and ${verbHas} been ignored.`,
224244
color: "orange",
225245
});
226246
}
@@ -238,11 +258,19 @@ const ExternalMemberListManagement: React.FC<
238258

239259
setReplaceModalOpened(false);
240260
setReplacementList("");
241-
notifications.show({
242-
title: "Changes Computed",
243-
message: `Queued ${membersToAdd.length} additions and ${membersToRemove.length} removals. Click 'Save Changes' to apply.`,
244-
color: "blue",
245-
});
261+
if (membersToAdd.length + membersToRemove.length > 0) {
262+
notifications.show({
263+
title: "Changes Computed",
264+
message: `Queued ${membersToAdd.length} additions and ${membersToRemove.length} removals. Click 'Save Changes' to apply.`,
265+
color: "blue",
266+
});
267+
} else {
268+
notifications.show({
269+
title: "No Changes Found",
270+
message: `Both lists are the same.`,
271+
color: "green",
272+
});
273+
}
246274
};
247275

248276
const rows = paginatedData.map((member) => {
@@ -357,7 +385,7 @@ const ExternalMemberListManagement: React.FC<
357385
<Table verticalSpacing="sm" highlightOnHover>
358386
<Table.Thead>
359387
<Table.Tr>
360-
<Table.Th>Member</Table.Th>
388+
<Table.Th>Member NetID</Table.Th>
361389
<Table.Th>Status</Table.Th>
362390
<Table.Th>Actions</Table.Th>
363391
</Table.Tr>
@@ -398,16 +426,19 @@ const ExternalMemberListManagement: React.FC<
398426
</Table.Tbody>
399427
</Table>
400428

401-
{totalPages > 1 && (
402-
<Group justify="center" mt="md">
429+
<Stack justify="center" align="center" mt="md">
430+
<Text size="sm" c="dimmed">
431+
Found {members.length} {pluralize("member", members.length)}.
432+
</Text>
433+
{totalPages > 1 && (
403434
<Pagination
404435
total={totalPages}
405436
value={activePage}
406437
onChange={setPage}
407438
disabled={isLoading}
408439
/>
409-
</Group>
410-
)}
440+
)}
441+
</Stack>
411442

412443
<AuthGuard
413444
isAppShell={false}
@@ -461,25 +492,65 @@ const ExternalMemberListManagement: React.FC<
461492
<Text fw={500} size="sm" mb="xs">
462493
Members to Add:
463494
</Text>
464-
{toAdd.map((netId) => (
465-
<Text key={netId} fz="sm">
466-
{" "}
467-
- {netId}
468-
</Text>
469-
))}
495+
{toAdd.length > CHANGE_DISPLAY_LIMIT ? (
496+
<Accordion variant="separated" radius="md">
497+
<Accordion.Item value="add-list">
498+
<Accordion.Control>
499+
<Text fz="sm">{toAdd.length} members</Text>
500+
</Accordion.Control>
501+
<Accordion.Panel>
502+
<ScrollArea h={200}>
503+
{toAdd.map((netId) => (
504+
<Text key={netId} fz="sm" py={2}>
505+
- {netId}
506+
</Text>
507+
))}
508+
</ScrollArea>
509+
</Accordion.Panel>
510+
</Accordion.Item>
511+
</Accordion>
512+
) : (
513+
toAdd.map((netId) => (
514+
<Text key={netId} fz="sm">
515+
{" "}
516+
- {netId}
517+
</Text>
518+
))
519+
)}
470520
</Box>
471521
)}
472522
{toRemove.length > 0 && (
473523
<Box>
474524
<Text fw={500} size="sm" mb="xs">
475525
Members to Remove:
476526
</Text>
477-
{toRemove.map((netId) => (
478-
<Text key={netId} fz="sm" c="red">
479-
{" "}
480-
- {netId}
481-
</Text>
482-
))}
527+
{toRemove.length > CHANGE_DISPLAY_LIMIT ? (
528+
<Accordion variant="separated" radius="md">
529+
<Accordion.Item value="remove-list">
530+
<Accordion.Control>
531+
<Text fz="sm" c="red">
532+
{toRemove.length} members
533+
</Text>
534+
</Accordion.Control>
535+
<Accordion.Panel>
536+
<ScrollArea h={200}>
537+
{toRemove.map((netId) => (
538+
<Text key={netId} fz="sm" c="red" py={2}>
539+
- {netId}
540+
</Text>
541+
))}
542+
</ScrollArea>
543+
</Accordion.Panel>
544+
</Accordion.Item>
545+
</Accordion>
546+
) : (
547+
toRemove.map((netId) => (
548+
<Text key={netId} fz="sm" c="red">
549+
{" "}
550+
- {netId}
551+
</Text>
552+
))
553+
)}
483554
</Box>
484555
)}
485556
<Group justify="flex-end" mt="lg">
@@ -544,7 +615,7 @@ const ExternalMemberListManagement: React.FC<
544615
necessary additions and removals to match the list you provide.
545616
</Text>
546617
<Textarea
547-
placeholder={"jdoe1\nasmith2\njohnson3"}
618+
placeholder={"jdoe2\[email protected]\njohns4"}
548619
value={replacementList}
549620
onChange={(e) => setReplacementList(e.currentTarget.value)}
550621
autosize

0 commit comments

Comments
 (0)