Skip to content

Adding a new sig ui implemented + integration with Ethan's backend functionalities #139

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions src/api/functions/siglead.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import {
DynamoDBClient,
QueryCommand,
ScanCommand,
} from "@aws-sdk/client-dynamodb";
import { unmarshall } from "@aws-sdk/util-dynamodb";
import { OrganizationList } from "common/orgs.js";
import {
SigDetailRecord,
SigMemberCount,
SigMemberRecord,
} from "common/types/siglead.js";
import { transformSigLeadToURI } from "common/utils.js";
import { string } from "zod";

export async function fetchMemberRecords(
sigid: string,
tableName: string,
dynamoClient: DynamoDBClient,
) {
const fetchSigMemberRecords = new QueryCommand({
TableName: tableName,
KeyConditionExpression: "#sigid = :accessVal",
ExpressionAttributeNames: {
"#sigid": "sigGroupId",
},
ExpressionAttributeValues: {
":accessVal": { S: sigid },
},
ScanIndexForward: false,
});

const result = await dynamoClient.send(fetchSigMemberRecords);

// Process the results
return (result.Items || []).map((item) => {
const unmarshalledItem = unmarshall(item);
return unmarshalledItem as SigMemberRecord;
});
}

export async function fetchSigDetail(
sigid: string,
tableName: string,
dynamoClient: DynamoDBClient,
) {
const fetchSigDetail = new QueryCommand({
TableName: tableName,
KeyConditionExpression: "#sigid = :accessVal",
ExpressionAttributeNames: {
"#sigid": "sigid",
},
ExpressionAttributeValues: {
":accessVal": { S: sigid },
},
ScanIndexForward: false,
});

const result = await dynamoClient.send(fetchSigDetail);

// Process the results
return (result.Items || [{}]).map((item) => {
const unmarshalledItem = unmarshall(item);

// Strip '#' from access field
delete unmarshalledItem.leadGroupId;
delete unmarshalledItem.memberGroupId;

return unmarshalledItem as SigDetailRecord;
})[0];
}

// select count(sigid)
// from table
// groupby sigid
export async function fetchSigCounts(
sigMemberTableName: string,
dynamoClient: DynamoDBClient,
) {
const scan = new ScanCommand({
TableName: sigMemberTableName,
ProjectionExpression: "sigGroupId",
});

const result = await dynamoClient.send(scan);

const ids2Name: Record<string, string> = {};
OrganizationList.forEach((org) => {
const sigid = transformSigLeadToURI(org);
ids2Name[sigid] = org;
});

const counts: Record<string, number> = {};
(result.Items || []).forEach((item) => {
const sigGroupId = item.sigGroupId?.S;
if (sigGroupId) {
counts[sigGroupId] = (counts[sigGroupId] || 0) + 1;
}
});

const joined: Record<string, [string, number]> = {};
Object.keys(counts).forEach((sigid) => {
joined[sigid] = [ids2Name[sigid], counts[sigid]];
});

const countsArray: SigMemberCount[] = Object.entries(joined).map(
([sigid, [signame, count]]) => ({
sigid,
signame,
count,
}),
);
return countsArray;
}
2 changes: 2 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import * as dotenv from "dotenv";
import iamRoutes from "./routes/iam.js";
import ticketsPlugin from "./routes/tickets.js";
import linkryRoutes from "./routes/linkry.js";
import sigleadRoutes from "./routes/siglead.js";
import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts";
import NodeCache from "node-cache";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
Expand Down Expand Up @@ -287,6 +288,7 @@ async function init(prettyPrint: boolean = false) {
api.register(iamRoutes, { prefix: "/iam" });
api.register(ticketsPlugin, { prefix: "/tickets" });
api.register(linkryRoutes, { prefix: "/linkry" });
api.register(sigleadRoutes, { prefix: "/siglead" });
api.register(mobileWalletRoute, { prefix: "/mobileWallet" });
api.register(stripeRoutes, { prefix: "/stripe" });
api.register(roomRequestRoutes, { prefix: "/roomRequests" });
Expand Down
134 changes: 134 additions & 0 deletions src/api/routes/siglead.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { FastifyPluginAsync } from "fastify";
import { DatabaseFetchError } from "../../common/errors/index.js";
import { genericConfig } from "../../common/config.js";
import {
SigDetailRecord,
SigleadGetRequest,
SigMemberCount,
SigMemberRecord,
} from "common/types/siglead.js";
import {
fetchMemberRecords,
fetchSigCounts,
fetchSigDetail,
} from "api/functions/siglead.js";

const sigleadRoutes: FastifyPluginAsync = async (fastify, _options) => {
const limitedRoutes: FastifyPluginAsync = async (fastify) => {
/*fastify.register(rateLimiter, {
limit: 30,
duration: 60,
rateLimitIdentifier: "linkry",
});*/

fastify.get<SigleadGetRequest>(
"/sigmembers/:sigid",
{
onRequest: async (request, reply) => {
/*await fastify.authorize(request, reply, [
AppRoles.LINKS_MANAGER,
AppRoles.LINKS_ADMIN,
]);*/
},
},
async (request, reply) => {
const { sigid } = request.params;
const tableName = genericConfig.SigleadDynamoSigMemberTableName;

// First try-catch: Fetch owner records
let memberRecords: SigMemberRecord[];
try {
memberRecords = await fetchMemberRecords(
sigid,
tableName,
fastify.dynamoClient,
);
} catch (error) {
request.log.error(
`Failed to fetch member records: ${error instanceof Error ? error.toString() : "Unknown error"}`,
);
throw new DatabaseFetchError({
message: "Failed to fetch member records from Dynamo table.",
});
}

// Send the response
reply.code(200).send(memberRecords);
},
);

fastify.get<SigleadGetRequest>(
"/sigdetail/:sigid",
{
onRequest: async (request, reply) => {
/*await fastify.authorize(request, reply, [
AppRoles.LINKS_MANAGER,
AppRoles.LINKS_ADMIN,
]);*/
},
},
async (request, reply) => {
const { sigid } = request.params;
const tableName = genericConfig.SigleadDynamoSigDetailTableName;

// First try-catch: Fetch owner records
let sigDetail: SigDetailRecord;
try {
sigDetail = await fetchSigDetail(
sigid,
tableName,
fastify.dynamoClient,
);
} catch (error) {
request.log.error(
`Failed to fetch sig detail record: ${error instanceof Error ? error.toString() : "Unknown error"}`,
);
throw new DatabaseFetchError({
message: "Failed to fetch sig detail record from Dynamo table.",
});
}

// Send the response
reply.code(200).send(sigDetail);
},
);

// fetch sig count
fastify.get<SigleadGetRequest>(
"/sigcount",
{
onRequest: async (request, reply) => {
/*await fastify.authorize(request, reply, [
AppRoles.LINKS_MANAGER,
AppRoles.LINKS_ADMIN,
]);*/
},
},
async (request, reply) => {
// First try-catch: Fetch owner records
let sigMemCounts: SigMemberCount[];
try {
sigMemCounts = await fetchSigCounts(
genericConfig.SigleadDynamoSigMemberTableName,
fastify.dynamoClient,
);
} catch (error) {
request.log.error(
`Failed to fetch sig member counts record: ${error instanceof Error ? error.toString() : "Unknown error"}`,
);
throw new DatabaseFetchError({
message:
"Failed to fetch sig member counts record from Dynamo table.",
});
}

// Send the response
reply.code(200).send(sigMemCounts);
},
);
};

fastify.register(limitedRoutes);
};

export default sigleadRoutes;
4 changes: 4 additions & 0 deletions src/common/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export type GenericConfigType = {
EventsDynamoTableName: string;
CacheDynamoTableName: string;
LinkryDynamoTableName: string;
SigleadDynamoSigDetailTableName: string;
SigleadDynamoSigMemberTableName: string;
StripeLinksDynamoTableName: string;
ConfigSecretName: string;
EntraSecretName: string;
Expand Down Expand Up @@ -68,6 +70,8 @@ const genericConfig: GenericConfigType = {
StripeLinksDynamoTableName: "infra-core-api-stripe-links",
CacheDynamoTableName: "infra-core-api-cache",
LinkryDynamoTableName: "infra-core-api-linkry",
SigleadDynamoSigDetailTableName: "infra-core-api-sig-details",
SigleadDynamoSigMemberTableName: "infra-core-api-sig-member-details",
ConfigSecretName: "infra-core-api-config",
EntraSecretName: "infra-core-api-entra",
EntraReadOnlySecretName: "infra-core-api-ro-entra",
Expand Down
2 changes: 1 addition & 1 deletion src/common/orgs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export const SIGList = [
"GameBuilders",
"SIGAIDA",
"SIGGRAPH",
"ICPC",
"SIGICPC",
"SIGMobile",
"SIGMusic",
"GLUG",
Expand Down
24 changes: 24 additions & 0 deletions src/common/types/siglead.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export type SigDetailRecord = {
sigid: string;
signame: string;
description: string;
};

export type SigMemberRecord = {
sigGroupId: string;
email: string;
designation: string;
memberName: string;
};

export type SigleadGetRequest = {
Params: { sigid: string };
Querystring: undefined;
Body: undefined;
};

export type SigMemberCount = {
sigid: string;
signame: string;
count: number;
};
44 changes: 44 additions & 0 deletions src/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,47 @@ export function transformCommaSeperatedName(name: string) {
}
return name;
}

const notUnreservedCharsRegex = /[^a-zA-Z0-9\-._~]/g;
const reservedCharsRegex = /[:\/?#\[\]@!$&'()*+,;=]/g;
/**
* Transforms an organization name (sig lead) into a URI-friendly format.
* The function performs the following transformations:
* - Removes characters that are reserved or not unreserved.
* - Adds spaces between camel case words.
* - Converts reserved characters to spaces.
* - Converts all characters to lowercase and replaces all types of whitespace with hyphens.
* - Replaces any sequence of repeated hyphens with a single hyphen.
* - Refer to RFC 3986 https://datatracker.ietf.org/doc/html/rfc3986#section-2.3
*
* @param {string} org - The organization (sig lead) name to be transformed.
* @returns {string} - The transformed organization name, ready for use as a URL.
*/
export function transformSigLeadToURI(org: string) {
org = org
// change not reserved chars to spaces
.trim()
.replace(notUnreservedCharsRegex, " ")
.trim()
.replace(/\s/g, "-")

// remove all that is reserved or not unreserved
.replace(reservedCharsRegex, "")

// convert SIG -> sig for camel case
.replace(/SIG/g, "sig")

// add hyphen for camel case
.replace(/([a-z])([A-Z])/g, "$1-$2")

// lower
.toLowerCase()

// add spaces between chars and numbers (seq2seq -> seq-2-seq)
.replace(/(?<=[a-z])([0-9]+)(?=[a-z])/g, "-$1-")

// remove duplicate hyphens
.replace(/-{2,}/g, "-");

return org === "-" ? "" : org;
}
6 changes: 6 additions & 0 deletions src/ui/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
RouterProvider,
useLocation,
} from "react-router-dom";

import { AcmAppShell } from "./components/AppShell";
import { useAuth } from "./components/AuthContext";
import AuthCallback from "./components/AuthContext/AuthCallbackHandler.page";
Expand All @@ -24,6 +25,7 @@ import { ManageIamPage } from "./pages/iam/ManageIam.page";
import { ManageProfilePage } from "./pages/profile/ManageProfile.page";
import { ManageStripeLinksPage } from "./pages/stripe/ViewLinks.page";
import { ManageRoomRequestsPage } from "./pages/roomRequest/RoomRequestLanding.page";
import { EditSigLeadsPage } from "./pages/siglead/EditSigLeads.page";
import { ManageSigLeadsPage } from "./pages/siglead/ManageSigLeads.page";
import { ViewSigLeadPage } from "./pages/siglead/ViewSigLead.page";
import { ViewRoomRequest } from "./pages/roomRequest/ViewRoomRequest.page";
Expand Down Expand Up @@ -193,6 +195,10 @@ const authenticatedRouter = createBrowserRouter([
path: "/siglead-management",
element: <ManageSigLeadsPage />,
},
{
path: "/siglead-management/edit",
element: <EditSigLeadsPage />,
},
{
path: "/siglead-management/:sigId",
element: <ViewSigLeadPage />,
Expand Down
Loading