diff --git a/src/api/functions/auditLog.ts b/src/api/functions/auditLog.ts index 32164a8c..77609e3b 100644 --- a/src/api/functions/auditLog.ts +++ b/src/api/functions/auditLog.ts @@ -5,6 +5,7 @@ import { } from "@aws-sdk/client-dynamodb"; import { marshall } from "@aws-sdk/util-dynamodb"; import { genericConfig } from "common/config.js"; +import { AUDIT_LOG_RETENTION_DAYS } from "common/constants.js"; import { AuditLogEntry } from "common/types/logs.js"; type AuditLogParams = { @@ -12,13 +13,12 @@ type AuditLogParams = { entry: AuditLogEntry; }; -const RETENTION_DAYS = 365; - function buildMarshalledAuditLogItem(entry: AuditLogEntry) { const baseNow = Date.now(); const timestamp = Math.floor(baseNow / 1000); const expireAt = - timestamp + Math.floor((RETENTION_DAYS * 24 * 60 * 60 * 1000) / 1000); + timestamp + + Math.floor((AUDIT_LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000) / 1000); return marshall( { diff --git a/src/api/routes/roomRequests.ts b/src/api/routes/roomRequests.ts index f875b65a..d2d29e79 100644 --- a/src/api/routes/roomRequests.ts +++ b/src/api/routes/roomRequests.ts @@ -36,6 +36,7 @@ import { getDefaultFilteringQuerystring, nonEmptyCommaSeparatedStringSchema, } from "common/utils.js"; +import { ROOM_RESERVATION_RETENTION_DAYS } from "common/constants.js"; const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => { await fastify.register(rateLimiter, { @@ -104,6 +105,9 @@ const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => { semesterId, "createdAt#status": `${createdAt}#${request.body.status}`, createdBy: request.username, + expiresAt: + Math.floor(Date.now() / 1000) + + 86400 * ROOM_RESERVATION_RETENTION_DAYS, ...request.body, }, { removeUndefinedValues: true }, @@ -315,6 +319,9 @@ const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => { userId: request.username, "userId#requestId": `${request.username}#${requestId}`, semesterId: request.body.semester, + expiresAt: + Math.floor(Date.now() / 1000) + + 86400 * ROOM_RESERVATION_RETENTION_DAYS, }; const logStatement = buildAuditLogTransactPut({ entry: { @@ -344,6 +351,9 @@ const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => { "createdAt#status": `${createdAt}#${RoomRequestStatus.CREATED}`, createdBy: request.username, status: RoomRequestStatus.CREATED, + expiresAt: + Math.floor(Date.now() / 1000) + + 86400 * ROOM_RESERVATION_RETENTION_DAYS, notes: "This request was created by the user.", }), }, diff --git a/src/api/routes/stripe.ts b/src/api/routes/stripe.ts index 6e3653b7..edbba46c 100644 --- a/src/api/routes/stripe.ts +++ b/src/api/routes/stripe.ts @@ -45,6 +45,7 @@ import { AvailableSQSFunctions, SQSPayload } from "common/types/sqsMessage.js"; import { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs"; import * as z from "zod/v4"; import { getAllUserEmails } from "common/utils.js"; +import { STRIPE_LINK_RETENTION_DAYS } from "common/constants.js"; const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => { await fastify.register(rawbody, { @@ -249,6 +250,9 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => { message: "Deactivated Stripe payment link", }, }); + // expire deleted links at 90 days + const expiresAt = + Math.floor(Date.now() / 1000) + 86400 * STRIPE_LINK_RETENTION_DAYS; const dynamoCommand = new TransactWriteItemsCommand({ TransactItems: [ ...(logStatement ? [logStatement] : []), @@ -259,11 +263,12 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => { userId: { S: unmarshalledEntry.userId }, linkId: { S: linkId }, }, - UpdateExpression: "SET active = :new_val", + UpdateExpression: "SET active = :new_val, expiresAt = :ttl", ConditionExpression: "active = :old_val", ExpressionAttributeValues: { ":new_val": { BOOL: false }, ":old_val": { BOOL: true }, + ":ttl": { N: expiresAt.toString() }, }, }, }, @@ -666,11 +671,18 @@ Please contact Officer Board with any questions.`, userId: { S: unmarshalledEntry.userId }, linkId: { S: paymentLinkId }, }, - UpdateExpression: "SET active = :new_val", + UpdateExpression: + "SET active = :new_val, expiresAt = :ttl", ConditionExpression: "active = :old_val", ExpressionAttributeValues: { ":new_val": { BOOL: false }, ":old_val": { BOOL: true }, + ":ttl": { + N: ( + Math.floor(Date.now() / 1000) + + 86400 * STRIPE_LINK_RETENTION_DAYS + ).toString(), + }, }, }, }, diff --git a/src/api/routes/tickets.ts b/src/api/routes/tickets.ts index 7126d6bc..109fe3bd 100644 --- a/src/api/routes/tickets.ts +++ b/src/api/routes/tickets.ts @@ -26,6 +26,7 @@ import { createAuditLogEntry } from "api/functions/auditLog.js"; import { Modules } from "common/modules.js"; import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi"; import { withRoles, withTags } from "api/components/index.js"; +import { FULFILLED_PURCHASES_RETENTION_DAYS } from "common/constants.js"; const postMerchSchema = z.object({ type: z.literal("merch"), @@ -355,6 +356,9 @@ const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => { message: "Could not find username.", }); } + const expiresAt = + Math.floor(Date.now() / 1000) + + 86400 * FULFILLED_PURCHASES_RETENTION_DAYS; switch (request.body.type) { case "merch": ticketId = request.body.stripePi; @@ -363,7 +367,7 @@ const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => { Key: { stripe_pi: { S: ticketId }, }, - UpdateExpression: "SET fulfilled = :true_val", + UpdateExpression: "SET fulfilled = :true_val, expiresAt = :ttl", ConditionExpression: "#email = :email_val AND (attribute_not_exists(fulfilled) OR fulfilled = :false_val) AND (attribute_not_exists(refunded) OR refunded = :false_val)", ExpressionAttributeNames: { @@ -373,6 +377,7 @@ const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => { ":true_val": { BOOL: true }, ":false_val": { BOOL: false }, ":email_val": { S: request.body.email }, + ":ttl": { N: expiresAt.toString() }, }, ReturnValuesOnConditionCheckFailure: "ALL_OLD", ReturnValues: "ALL_OLD", @@ -385,7 +390,7 @@ const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => { Key: { ticket_id: { S: ticketId }, }, - UpdateExpression: "SET #used = :trueValue", + UpdateExpression: "SET #used = :trueValue, expiresAt = :ttl", ConditionExpression: "(attribute_not_exists(#used) OR #used = :falseValue) AND (attribute_not_exists(refunded) OR refunded = :falseValue)", ExpressionAttributeNames: { @@ -394,6 +399,7 @@ const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => { ExpressionAttributeValues: { ":trueValue": { BOOL: true }, ":falseValue": { BOOL: false }, + ":ttl": { N: expiresAt.toString() }, }, ReturnValuesOnConditionCheckFailure: "ALL_OLD", ReturnValues: "ALL_OLD", diff --git a/src/common/constants.ts b/src/common/constants.ts new file mode 100644 index 00000000..fd6bc66e --- /dev/null +++ b/src/common/constants.ts @@ -0,0 +1,4 @@ +export const STRIPE_LINK_RETENTION_DAYS = 90; // this number of days after the link is deactivated. +export const AUDIT_LOG_RETENTION_DAYS = 365; +export const ROOM_RESERVATION_RETENTION_DAYS = 730; +export const FULFILLED_PURCHASES_RETENTION_DAYS = 365; // ticketing/merch: after the purchase is marked as fulfilled. diff --git a/src/ui/pages/stripe/CurrentLinks.tsx b/src/ui/pages/stripe/CurrentLinks.tsx index a10a90f6..57e92ddb 100644 --- a/src/ui/pages/stripe/CurrentLinks.tsx +++ b/src/ui/pages/stripe/CurrentLinks.tsx @@ -18,6 +18,7 @@ import { notifications } from "@mantine/notifications"; import { useAuth } from "@ui/components/AuthContext"; import pluralize from "pluralize"; import dayjs from "dayjs"; +import { STRIPE_LINK_RETENTION_DAYS } from "@common/constants"; const HumanFriendlyDate = ({ date }: { date: string | Date }) => { return {dayjs(date).format("MMMM D, YYYY")}; @@ -146,7 +147,8 @@ export const StripeCurrentLinksPanel: React.FC< } if (result.success > 0) { notifications.show({ - message: `Deactivated ${pluralize("link", result.success, true)}!`, + title: `Deactivated ${pluralize("link", result.success, true)}!`, + message: `Links will be permanently removed from this page after ${STRIPE_LINK_RETENTION_DAYS} days.`, color: "green", }); } diff --git a/terraform/modules/dynamo/main.tf b/terraform/modules/dynamo/main.tf index b8ba1cbf..a7edbef7 100644 --- a/terraform/modules/dynamo/main.tf +++ b/terraform/modules/dynamo/main.tf @@ -17,7 +17,7 @@ resource "aws_dynamodb_table" "app_audit_log" { } ttl { attribute_name = "expiresAt" - enabled = true + enabled = false } } @@ -80,6 +80,10 @@ resource "aws_dynamodb_table" "room_requests" { hash_key = "requestId" projection_type = "ALL" } + ttl { + attribute_name = "expiresAt" + enabled = true + } } @@ -110,6 +114,10 @@ resource "aws_dynamodb_table" "room_requests_status" { range_key = "requestId" projection_type = "ALL" } + ttl { + attribute_name = "expiresAt" + enabled = true + } } @@ -224,6 +232,10 @@ resource "aws_dynamodb_table" "stripe_links" { hash_key = "linkId" projection_type = "ALL" } + ttl { + attribute_name = "expiresAt" + enabled = true + } } resource "aws_dynamodb_table" "linkry_records" {