Skip to content

[DO NOT MERGE] Siglead management #154

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

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d678eb6
Adding Siglead Management Option in the Side Menu & Adding a draft Page
tarashagarwal Feb 18, 2025
a837785
siglead management screen done
eeen17 Mar 1, 2025
c75ec3c
column headers and text color
eeen17 Mar 8, 2025
f3bea09
merge main conflicts
eeen17 Apr 4, 2025
ac54829
UI updates for the main screen #99
eeen17 Apr 4, 2025
2d9ceda
Merge branch 'main' of github.com:acm-uiuc/core into siglead-management
Apr 18, 2025
9ac6842
New SigView page
EthM370 Apr 22, 2025
f42b44a
Update ViewSigLead.page.tsx
tarashagarwal Apr 24, 2025
9c5627a
Merge pull request #124 from acm-uiuc/EthM370/siglead-management
tarashagarwal Apr 24, 2025
1ed991a
Merge branch 'main' of github.com:acm-uiuc/core into siglead-management
Apr 24, 2025
8a57e54
Resolving Merge Conflicts with Main
May 20, 2025
5ab43ab
Fixes
May 20, 2025
2336194
Merge branch 'main' of github.com:acm-uiuc/core into siglead-management
Jun 7, 2025
c7873c8
Adding a new sig ui implemented + integration with Ethan's backend fu…
EthM370 Jun 7, 2025
22bf9dd
Eeen17/siglead mainscreen api (#152)
eeen17 Jun 10, 2025
965a171
undo mistake on entraId
eeen17 Jun 11, 2025
58cc39e
fixed typo oops
eeen17 Jun 11, 2025
9e2d2eb
give permissions to access sig details and sig member dynamodb tables
eeen17 Jun 11, 2025
e48c149
testing iam patch for sigs
eeen17 Jun 12, 2025
2437ec1
log iam patch response
eeen17 Jun 12, 2025
d0ea8e5
remove colon in request and check for empty string
eeen17 Jun 12, 2025
e7c77d9
remove rate limiter table
devksingh4 Jun 15, 2025
173b541
remove orgs file
devksingh4 Jun 15, 2025
e7e9f6b
Merge branch 'main' into siglead-management
devksingh4 Jun 15, 2025
39f84ed
Auto-update feature branch with changes from the main branch
github-actions[bot] Jun 15, 2025
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
10 changes: 10 additions & 0 deletions cloudformation/iam.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ Resources:
- Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-room-requests-status
- Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-linkry
- Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-keys
- Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-sig-member-details
- Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-sig-details
# Index accesses
- Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-stripe-links/index/*
- Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-events/index/*
Expand All @@ -99,6 +101,14 @@ Resources:
Resource:
- Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-cache

- Sid: DynamoDBRateLimitTableAccess
Effect: Allow
Action:
- dynamodb:DescribeTable
- dynamodb:UpdateItem
Resource:
- Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-rate-limiter

- Sid: DynamoDBAuditLogTableAccess
Effect: Allow
Action:
Expand Down
172 changes: 172 additions & 0 deletions src/api/functions/siglead.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import {
AttributeValue,
DynamoDBClient,
GetItemCommand,
PutItemCommand,
PutItemCommandInput,
QueryCommand,
ScanCommand,
} from "@aws-sdk/client-dynamodb";
import { unmarshall } from "@aws-sdk/util-dynamodb";
import { DatabaseInsertError } from "common/errors/index.js";
import { OrganizationList, orgIds2Name } from "common/orgs.js";
import {
SigDetailRecord,
SigMemberCount,
SigMemberRecord,
SigMemberUpdateRecord,
} from "common/types/siglead.js";
import { transformSigLeadToURI } from "common/utils.js";
import { KeyObject } from "crypto";
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 counts: Record<string, number> = {};
// Object.entries(orgIds2Name).forEach(([id, _]) => {
// counts[id] = 0;
// });

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

const countsArray: SigMemberCount[] = Object.entries(counts).map(
([id, count]) => ({
sigid: id,
signame: orgIds2Name[id],
count,
}),
);
console.log(countsArray);
return countsArray;
}

export async function addMemberToSigDynamo(
sigMemberTableName: string,
sigMemberUpdateRequest: SigMemberUpdateRecord,
dynamoClient: DynamoDBClient,
) {
const item: Record<string, AttributeValue> = {};
Object.entries(sigMemberUpdateRequest).forEach(([k, v]) => {
item[k] = { S: v };
});

// put into table
const put = new PutItemCommand({
Item: item,
ReturnConsumedCapacity: "TOTAL",
TableName: sigMemberTableName,
});
try {
const response = await dynamoClient.send(put);
console.log(response);
} catch (e) {
console.error("Put to dynamo db went wrong.");
throw e;
}

// fetch from db and check if fetched item update time = input item update time
const validatePutQuery = new GetItemCommand({
TableName: sigMemberTableName,
Key: {
sigGroupId: { S: sigMemberUpdateRequest.sigGroupId },
email: { S: sigMemberUpdateRequest.email },
},
ProjectionExpression: "updatedAt",
});

try {
const response = await dynamoClient.send(validatePutQuery);
const item = response.Item;

if (!item || !item.updatedAt?.S) {
throw new Error("Item not found or missing 'updatedAt'");
}

if (item.updatedAt.S !== sigMemberUpdateRequest.updatedAt) {
throw new DatabaseInsertError({
message: "The member exists, but was updated by someone else!",
});
}
} catch (e) {
console.error("Validate DynamoDB get went wrong.", e);
throw e;
}
}

export async function addMemberToSigEntra() {
// uuid validation not implemented yet
}
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 @@ -289,6 +290,7 @@ async function init(prettyPrint: boolean = false) {
api.register(linkryRoutes, { prefix: "/linkry" });
api.register(mobileWalletRoute, { prefix: "/mobileWallet" });
api.register(stripeRoutes, { prefix: "/stripe" });
api.register(sigleadRoutes, { prefix: "/siglead" });
api.register(roomRequestRoutes, { prefix: "/roomRequests" });
api.register(logsPlugin, { prefix: "/logs" });
api.register(apiKeyRoute, { prefix: "/apiKey" });
Expand Down
151 changes: 151 additions & 0 deletions src/api/routes/siglead.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { FastifyPluginAsync } from "fastify";
import { DatabaseFetchError } from "../../common/errors/index.js";

import { genericConfig } from "../../common/config.js";

import {
SigDetailRecord,
SigleadGetRequest,
SigMemberCount,
SigMemberRecord,
SigMemberUpdateRecord,
} from "common/types/siglead.js";
import {
addMemberToSigDynamo,
fetchMemberRecords,
fetchSigCounts,
fetchSigDetail,
} from "api/functions/siglead.js";
import { intersection } from "api/plugins/auth.js";
import { request } from "http";

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", 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);
});

// add member
fastify.post<{ Body: SigMemberUpdateRecord }>(
"/addMember",
async (request, reply) => {
try {
await addMemberToSigDynamo(
genericConfig.SigleadDynamoSigMemberTableName,
request.body,
fastify.dynamoClient,
);
} catch (error) {
request.log.error(
`Failed to add member: ${error instanceof Error ? error.toString() : "Unknown error"}`,
);
throw new DatabaseFetchError({
message: "Failed to add sig member record to Dynamo table.",
});
}
reply.code(200);
},
);
};

fastify.register(limitedRoutes);
};

export default sigleadRoutes;
10 changes: 10 additions & 0 deletions src/common/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ export type GenericConfigType = {
EntraReadOnlySecretName: string;
AuditLogTable: string;
ApiKeyTable: string;

RateLimiterDynamoTableName: string;
SigleadDynamoSigDetailTableName: string;
SigleadDynamoSigMemberTableName: string;
};

type EnvironmentConfigType = {
Expand All @@ -63,6 +67,8 @@ export const commChairsTestingGroupId = "d714adb7-07bb-4d4d-a40a-b035bc2a35a3";
export const commChairsGroupId = "105e7d32-7289-435e-a67a-552c7f215507";
export const miscTestingGroupId = "ff25ec56-6a33-420d-bdb0-51d8a3920e46";

export const orgsGroupId = "0b3be7c2-748e-46ce-97e7-cf86f9ca7337";

const genericConfig: GenericConfigType = {
EventsDynamoTableName: "infra-core-api-events",
StripeLinksDynamoTableName: "infra-core-api-stripe-links",
Expand All @@ -86,6 +92,10 @@ const genericConfig: GenericConfigType = {
RoomRequestsStatusTableName: "infra-core-api-room-requests-status",
AuditLogTable: "infra-core-api-audit-log",
ApiKeyTable: "infra-core-api-keys",

RateLimiterDynamoTableName: "infra-core-api-rate-limiter",
SigleadDynamoSigDetailTableName: "infra-core-api-sig-details",
SigleadDynamoSigMemberTableName: "infra-core-api-sig-member-details",
} as const;

const environmentConfig: EnvironmentConfigType = {
Expand Down
Loading