Skip to content

Commit c7873c8

Browse files
EthM370Tarash Agarwal
andauthored
Adding a new sig ui implemented + integration with Ethan's backend functionalities (#139)
New page editsiglead registered, navigate by clicking add an sig button on siglead-management, implemented frontend text input and zod validation, checks that signame is not reserved and follows desired regex. All api calls are to backend functions copied over from Ethan Chang's branch. Co-authored-by: Tarash Agarwal <[email protected]>
1 parent 2336194 commit c7873c8

File tree

12 files changed

+504
-177
lines changed

12 files changed

+504
-177
lines changed

src/api/functions/siglead.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import {
2+
DynamoDBClient,
3+
QueryCommand,
4+
ScanCommand,
5+
} from "@aws-sdk/client-dynamodb";
6+
import { unmarshall } from "@aws-sdk/util-dynamodb";
7+
import { OrganizationList } from "common/orgs.js";
8+
import {
9+
SigDetailRecord,
10+
SigMemberCount,
11+
SigMemberRecord,
12+
} from "common/types/siglead.js";
13+
import { transformSigLeadToURI } from "common/utils.js";
14+
import { string } from "zod";
15+
16+
export async function fetchMemberRecords(
17+
sigid: string,
18+
tableName: string,
19+
dynamoClient: DynamoDBClient,
20+
) {
21+
const fetchSigMemberRecords = new QueryCommand({
22+
TableName: tableName,
23+
KeyConditionExpression: "#sigid = :accessVal",
24+
ExpressionAttributeNames: {
25+
"#sigid": "sigGroupId",
26+
},
27+
ExpressionAttributeValues: {
28+
":accessVal": { S: sigid },
29+
},
30+
ScanIndexForward: false,
31+
});
32+
33+
const result = await dynamoClient.send(fetchSigMemberRecords);
34+
35+
// Process the results
36+
return (result.Items || []).map((item) => {
37+
const unmarshalledItem = unmarshall(item);
38+
return unmarshalledItem as SigMemberRecord;
39+
});
40+
}
41+
42+
export async function fetchSigDetail(
43+
sigid: string,
44+
tableName: string,
45+
dynamoClient: DynamoDBClient,
46+
) {
47+
const fetchSigDetail = new QueryCommand({
48+
TableName: tableName,
49+
KeyConditionExpression: "#sigid = :accessVal",
50+
ExpressionAttributeNames: {
51+
"#sigid": "sigid",
52+
},
53+
ExpressionAttributeValues: {
54+
":accessVal": { S: sigid },
55+
},
56+
ScanIndexForward: false,
57+
});
58+
59+
const result = await dynamoClient.send(fetchSigDetail);
60+
61+
// Process the results
62+
return (result.Items || [{}]).map((item) => {
63+
const unmarshalledItem = unmarshall(item);
64+
65+
// Strip '#' from access field
66+
delete unmarshalledItem.leadGroupId;
67+
delete unmarshalledItem.memberGroupId;
68+
69+
return unmarshalledItem as SigDetailRecord;
70+
})[0];
71+
}
72+
73+
// select count(sigid)
74+
// from table
75+
// groupby sigid
76+
export async function fetchSigCounts(
77+
sigMemberTableName: string,
78+
dynamoClient: DynamoDBClient,
79+
) {
80+
const scan = new ScanCommand({
81+
TableName: sigMemberTableName,
82+
ProjectionExpression: "sigGroupId",
83+
});
84+
85+
const result = await dynamoClient.send(scan);
86+
87+
const ids2Name: Record<string, string> = {};
88+
OrganizationList.forEach((org) => {
89+
const sigid = transformSigLeadToURI(org);
90+
ids2Name[sigid] = org;
91+
});
92+
93+
const counts: Record<string, number> = {};
94+
(result.Items || []).forEach((item) => {
95+
const sigGroupId = item.sigGroupId?.S;
96+
if (sigGroupId) {
97+
counts[sigGroupId] = (counts[sigGroupId] || 0) + 1;
98+
}
99+
});
100+
101+
const joined: Record<string, [string, number]> = {};
102+
Object.keys(counts).forEach((sigid) => {
103+
joined[sigid] = [ids2Name[sigid], counts[sigid]];
104+
});
105+
106+
const countsArray: SigMemberCount[] = Object.entries(joined).map(
107+
([sigid, [signame, count]]) => ({
108+
sigid,
109+
signame,
110+
count,
111+
}),
112+
);
113+
return countsArray;
114+
}

src/api/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import * as dotenv from "dotenv";
2525
import iamRoutes from "./routes/iam.js";
2626
import ticketsPlugin from "./routes/tickets.js";
2727
import linkryRoutes from "./routes/linkry.js";
28+
import sigleadRoutes from "./routes/siglead.js";
2829
import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts";
2930
import NodeCache from "node-cache";
3031
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
@@ -287,6 +288,7 @@ async function init(prettyPrint: boolean = false) {
287288
api.register(iamRoutes, { prefix: "/iam" });
288289
api.register(ticketsPlugin, { prefix: "/tickets" });
289290
api.register(linkryRoutes, { prefix: "/linkry" });
291+
api.register(sigleadRoutes, { prefix: "/siglead" });
290292
api.register(mobileWalletRoute, { prefix: "/mobileWallet" });
291293
api.register(stripeRoutes, { prefix: "/stripe" });
292294
api.register(roomRequestRoutes, { prefix: "/roomRequests" });

src/api/routes/siglead.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { FastifyPluginAsync } from "fastify";
2+
import { DatabaseFetchError } from "../../common/errors/index.js";
3+
import { genericConfig } from "../../common/config.js";
4+
import {
5+
SigDetailRecord,
6+
SigleadGetRequest,
7+
SigMemberCount,
8+
SigMemberRecord,
9+
} from "common/types/siglead.js";
10+
import {
11+
fetchMemberRecords,
12+
fetchSigCounts,
13+
fetchSigDetail,
14+
} from "api/functions/siglead.js";
15+
16+
const sigleadRoutes: FastifyPluginAsync = async (fastify, _options) => {
17+
const limitedRoutes: FastifyPluginAsync = async (fastify) => {
18+
/*fastify.register(rateLimiter, {
19+
limit: 30,
20+
duration: 60,
21+
rateLimitIdentifier: "linkry",
22+
});*/
23+
24+
fastify.get<SigleadGetRequest>(
25+
"/sigmembers/:sigid",
26+
{
27+
onRequest: async (request, reply) => {
28+
/*await fastify.authorize(request, reply, [
29+
AppRoles.LINKS_MANAGER,
30+
AppRoles.LINKS_ADMIN,
31+
]);*/
32+
},
33+
},
34+
async (request, reply) => {
35+
const { sigid } = request.params;
36+
const tableName = genericConfig.SigleadDynamoSigMemberTableName;
37+
38+
// First try-catch: Fetch owner records
39+
let memberRecords: SigMemberRecord[];
40+
try {
41+
memberRecords = await fetchMemberRecords(
42+
sigid,
43+
tableName,
44+
fastify.dynamoClient,
45+
);
46+
} catch (error) {
47+
request.log.error(
48+
`Failed to fetch member records: ${error instanceof Error ? error.toString() : "Unknown error"}`,
49+
);
50+
throw new DatabaseFetchError({
51+
message: "Failed to fetch member records from Dynamo table.",
52+
});
53+
}
54+
55+
// Send the response
56+
reply.code(200).send(memberRecords);
57+
},
58+
);
59+
60+
fastify.get<SigleadGetRequest>(
61+
"/sigdetail/:sigid",
62+
{
63+
onRequest: async (request, reply) => {
64+
/*await fastify.authorize(request, reply, [
65+
AppRoles.LINKS_MANAGER,
66+
AppRoles.LINKS_ADMIN,
67+
]);*/
68+
},
69+
},
70+
async (request, reply) => {
71+
const { sigid } = request.params;
72+
const tableName = genericConfig.SigleadDynamoSigDetailTableName;
73+
74+
// First try-catch: Fetch owner records
75+
let sigDetail: SigDetailRecord;
76+
try {
77+
sigDetail = await fetchSigDetail(
78+
sigid,
79+
tableName,
80+
fastify.dynamoClient,
81+
);
82+
} catch (error) {
83+
request.log.error(
84+
`Failed to fetch sig detail record: ${error instanceof Error ? error.toString() : "Unknown error"}`,
85+
);
86+
throw new DatabaseFetchError({
87+
message: "Failed to fetch sig detail record from Dynamo table.",
88+
});
89+
}
90+
91+
// Send the response
92+
reply.code(200).send(sigDetail);
93+
},
94+
);
95+
96+
// fetch sig count
97+
fastify.get<SigleadGetRequest>(
98+
"/sigcount",
99+
{
100+
onRequest: async (request, reply) => {
101+
/*await fastify.authorize(request, reply, [
102+
AppRoles.LINKS_MANAGER,
103+
AppRoles.LINKS_ADMIN,
104+
]);*/
105+
},
106+
},
107+
async (request, reply) => {
108+
// First try-catch: Fetch owner records
109+
let sigMemCounts: SigMemberCount[];
110+
try {
111+
sigMemCounts = await fetchSigCounts(
112+
genericConfig.SigleadDynamoSigMemberTableName,
113+
fastify.dynamoClient,
114+
);
115+
} catch (error) {
116+
request.log.error(
117+
`Failed to fetch sig member counts record: ${error instanceof Error ? error.toString() : "Unknown error"}`,
118+
);
119+
throw new DatabaseFetchError({
120+
message:
121+
"Failed to fetch sig member counts record from Dynamo table.",
122+
});
123+
}
124+
125+
// Send the response
126+
reply.code(200).send(sigMemCounts);
127+
},
128+
);
129+
};
130+
131+
fastify.register(limitedRoutes);
132+
};
133+
134+
export default sigleadRoutes;

src/common/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ export type GenericConfigType = {
2929
EventsDynamoTableName: string;
3030
CacheDynamoTableName: string;
3131
LinkryDynamoTableName: string;
32+
SigleadDynamoSigDetailTableName: string;
33+
SigleadDynamoSigMemberTableName: string;
3234
StripeLinksDynamoTableName: string;
3335
ConfigSecretName: string;
3436
EntraSecretName: string;
@@ -68,6 +70,8 @@ const genericConfig: GenericConfigType = {
6870
StripeLinksDynamoTableName: "infra-core-api-stripe-links",
6971
CacheDynamoTableName: "infra-core-api-cache",
7072
LinkryDynamoTableName: "infra-core-api-linkry",
73+
SigleadDynamoSigDetailTableName: "infra-core-api-sig-details",
74+
SigleadDynamoSigMemberTableName: "infra-core-api-sig-member-details",
7175
ConfigSecretName: "infra-core-api-config",
7276
EntraSecretName: "infra-core-api-entra",
7377
EntraReadOnlySecretName: "infra-core-api-ro-entra",

src/common/orgs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export const SIGList = [
44
"GameBuilders",
55
"SIGAIDA",
66
"SIGGRAPH",
7-
"ICPC",
7+
"SIGICPC",
88
"SIGMobile",
99
"SIGMusic",
1010
"GLUG",

src/common/types/siglead.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
export type SigDetailRecord = {
2+
sigid: string;
3+
signame: string;
4+
description: string;
5+
};
6+
7+
export type SigMemberRecord = {
8+
sigGroupId: string;
9+
email: string;
10+
designation: string;
11+
memberName: string;
12+
};
13+
14+
export type SigleadGetRequest = {
15+
Params: { sigid: string };
16+
Querystring: undefined;
17+
Body: undefined;
18+
};
19+
20+
export type SigMemberCount = {
21+
sigid: string;
22+
signame: string;
23+
count: number;
24+
};

src/common/utils.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,47 @@ export function transformCommaSeperatedName(name: string) {
1212
}
1313
return name;
1414
}
15+
16+
const notUnreservedCharsRegex = /[^a-zA-Z0-9\-._~]/g;
17+
const reservedCharsRegex = /[:\/?#\[\]@!$&'()*+,;=]/g;
18+
/**
19+
* Transforms an organization name (sig lead) into a URI-friendly format.
20+
* The function performs the following transformations:
21+
* - Removes characters that are reserved or not unreserved.
22+
* - Adds spaces between camel case words.
23+
* - Converts reserved characters to spaces.
24+
* - Converts all characters to lowercase and replaces all types of whitespace with hyphens.
25+
* - Replaces any sequence of repeated hyphens with a single hyphen.
26+
* - Refer to RFC 3986 https://datatracker.ietf.org/doc/html/rfc3986#section-2.3
27+
*
28+
* @param {string} org - The organization (sig lead) name to be transformed.
29+
* @returns {string} - The transformed organization name, ready for use as a URL.
30+
*/
31+
export function transformSigLeadToURI(org: string) {
32+
org = org
33+
// change not reserved chars to spaces
34+
.trim()
35+
.replace(notUnreservedCharsRegex, " ")
36+
.trim()
37+
.replace(/\s/g, "-")
38+
39+
// remove all that is reserved or not unreserved
40+
.replace(reservedCharsRegex, "")
41+
42+
// convert SIG -> sig for camel case
43+
.replace(/SIG/g, "sig")
44+
45+
// add hyphen for camel case
46+
.replace(/([a-z])([A-Z])/g, "$1-$2")
47+
48+
// lower
49+
.toLowerCase()
50+
51+
// add spaces between chars and numbers (seq2seq -> seq-2-seq)
52+
.replace(/(?<=[a-z])([0-9]+)(?=[a-z])/g, "-$1-")
53+
54+
// remove duplicate hyphens
55+
.replace(/-{2,}/g, "-");
56+
57+
return org === "-" ? "" : org;
58+
}

src/ui/Router.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
RouterProvider,
66
useLocation,
77
} from "react-router-dom";
8+
89
import { AcmAppShell } from "./components/AppShell";
910
import { useAuth } from "./components/AuthContext";
1011
import AuthCallback from "./components/AuthContext/AuthCallbackHandler.page";
@@ -24,6 +25,7 @@ import { ManageIamPage } from "./pages/iam/ManageIam.page";
2425
import { ManageProfilePage } from "./pages/profile/ManageProfile.page";
2526
import { ManageStripeLinksPage } from "./pages/stripe/ViewLinks.page";
2627
import { ManageRoomRequestsPage } from "./pages/roomRequest/RoomRequestLanding.page";
28+
import { EditSigLeadsPage } from "./pages/siglead/EditSigLeads.page";
2729
import { ManageSigLeadsPage } from "./pages/siglead/ManageSigLeads.page";
2830
import { ViewSigLeadPage } from "./pages/siglead/ViewSigLead.page";
2931
import { ViewRoomRequest } from "./pages/roomRequest/ViewRoomRequest.page";
@@ -193,6 +195,10 @@ const authenticatedRouter = createBrowserRouter([
193195
path: "/siglead-management",
194196
element: <ManageSigLeadsPage />,
195197
},
198+
{
199+
path: "/siglead-management/edit",
200+
element: <EditSigLeadsPage />,
201+
},
196202
{
197203
path: "/siglead-management/:sigId",
198204
element: <ViewSigLeadPage />,

0 commit comments

Comments
 (0)