Skip to content

Eeen17/siglead mainscreen api #152

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 11 commits into from
Jun 10, 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
8 changes: 8 additions & 0 deletions cloudformation/iam.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,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
24 changes: 24 additions & 0 deletions cloudformation/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,30 @@ Resources:
- AttributeName: userEmail
KeyType: HASH

RateLimiterTable:
Type: "AWS::DynamoDB::Table"
DeletionPolicy: "Delete"
UpdateReplacePolicy: "Delete"
Properties:
BillingMode: "PAY_PER_REQUEST"
TableName: infra-core-api-rate-limiter
DeletionProtectionEnabled: true
PointInTimeRecoverySpecification:
PointInTimeRecoveryEnabled: false
AttributeDefinitions:
- AttributeName: PK
AttributeType: S
- AttributeName: SK
AttributeType: S
KeySchema:
- AttributeName: PK
KeyType: HASH
- AttributeName: SK
KeyType: RANGE
TimeToLiveSpecification:
AttributeName: ttl
Enabled: true

EventRecordsTable:
Type: "AWS::DynamoDB::Table"
DeletionPolicy: "Retain"
Expand Down
4 changes: 2 additions & 2 deletions src/api/functions/entraId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { checkPaidMembershipFromTable } from "./membership.js";

function validateGroupId(groupId: string): boolean {
export function validateGroupId(groupId: string): boolean {
const groupIdPattern = /^[a-zA-Z0-9-]+$/; // Adjust the pattern as needed
return groupIdPattern.test(groupId);
}
Expand Down Expand Up @@ -368,7 +368,7 @@ export async function listGroupMembers(
* @throws {EntraUserError} If fetching the user profile fails.
* @returns {Promise<UserProfileData>} The user's profile information.
*/
export async function getUserProfile(
export async function getUserProflile(
token: string,
email: string,
): Promise<UserProfileData> {
Expand Down
90 changes: 74 additions & 16 deletions src/api/functions/siglead.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import {
AttributeValue,
DynamoDBClient,
GetItemCommand,
PutItemCommand,
PutItemCommandInput,
QueryCommand,
ScanCommand,
} from "@aws-sdk/client-dynamodb";
import { unmarshall } from "@aws-sdk/util-dynamodb";
import { OrganizationList } from "common/orgs.js";
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(
Expand Down Expand Up @@ -84,31 +91,82 @@ export async function fetchSigCounts(

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> = {};
// 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 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,
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: 1 addition & 1 deletion src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,9 +288,9 @@ 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(sigleadRoutes, { prefix: "/siglead" });
api.register(roomRequestRoutes, { prefix: "/roomRequests" });
api.register(logsPlugin, { prefix: "/logs" });
api.register(apiKeyRoute, { prefix: "/apiKey" });
Expand Down
55 changes: 36 additions & 19 deletions src/api/routes/siglead.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
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) => {
Expand Down Expand Up @@ -94,36 +100,47 @@ const sigleadRoutes: FastifyPluginAsync = async (fastify, _options) => {
);

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

// Send the response
reply.code(200).send(sigMemCounts);
reply.code(200);
},
);
};
Expand Down
14 changes: 10 additions & 4 deletions src/common/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ export type GenericConfigType = {
EventsDynamoTableName: string;
CacheDynamoTableName: string;
LinkryDynamoTableName: string;
SigleadDynamoSigDetailTableName: string;
SigleadDynamoSigMemberTableName: string;
StripeLinksDynamoTableName: string;
ConfigSecretName: string;
EntraSecretName: string;
Expand All @@ -50,6 +48,10 @@ export type GenericConfigType = {
EntraReadOnlySecretName: string;
AuditLogTable: string;
ApiKeyTable: string;

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

type EnvironmentConfigType = {
Expand All @@ -65,13 +67,13 @@ 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",
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 All @@ -90,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
9 changes: 9 additions & 0 deletions src/common/orgs.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { transformSigLeadToURI } from "./utils.js";

export const SIGList = [
"SIGPwny",
"SIGCHI",
Expand Down Expand Up @@ -28,3 +30,10 @@ export const CommitteeList = [
"Marketing Committee",
] as [string, ...string[]];
export const OrganizationList = ["ACM", ...SIGList, ...CommitteeList] as [string, ...string[]];

const orgIds2Name: Record<string, string> = {};
OrganizationList.forEach((org) => {
const sigid = transformSigLeadToURI(org);
orgIds2Name[sigid] = org;
});
export { orgIds2Name };
5 changes: 4 additions & 1 deletion src/common/roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,8 @@ export enum AppRoles {
MANAGE_ORG_API_KEYS = "manage:orgApiKey"
}
export const allAppRoles = Object.values(AppRoles).filter(
(value) => typeof value === "string",
(value) => typeof value === "string",
);



Loading