Skip to content

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 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
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
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;
Loading
Loading