mirror of
https://github.com/lukevella/rallly.git
synced 2025-04-29 18:26:34 +02:00
♻️ Update house keeping (#1591)
This commit is contained in:
parent
b173cc78a0
commit
c25ee09855
7 changed files with 394 additions and 109 deletions
|
@ -6,4 +6,5 @@ DATABASE_URL=postgres://postgres:postgres@localhost:5450/rallly
|
|||
SUPPORT_EMAIL=support@rallly.co
|
||||
SMTP_HOST=0.0.0.0
|
||||
SMTP_PORT=1025
|
||||
QUICK_CREATE_ENABLED=true
|
||||
QUICK_CREATE_ENABLED=true
|
||||
API_SECRET=1234567890abcdef1234567890abcdef1234
|
||||
|
|
1
apps/web/next-env.d.ts
vendored
1
apps/web/next-env.d.ts
vendored
|
@ -1,6 +1,5 @@
|
|||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference types="next/navigation-types/compat/navigation" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||
|
|
|
@ -1,107 +0,0 @@
|
|||
import { prisma } from "@rallly/database";
|
||||
import { headers } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function POST(req: Request, ctx: { params: { task: string } }) {
|
||||
const headersList = headers();
|
||||
const authorization = headersList.get("authorization");
|
||||
|
||||
if (authorization !== `Bearer ${process.env.API_SECRET}`) {
|
||||
return NextResponse.json(
|
||||
{ success: false },
|
||||
{
|
||||
status: 401,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
switch (ctx.params.task) {
|
||||
case "delete-inactive-polls": {
|
||||
return await deleteInactivePolls();
|
||||
}
|
||||
case "remove-deleted-polls": {
|
||||
return await removeDeletedPolls(req);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks inactive polls as deleted. Polls are inactive if they have not been
|
||||
* touched in the last 30 days and all dates are in the past.
|
||||
*/
|
||||
async function deleteInactivePolls() {
|
||||
const markedDeleted = await prisma.$executeRaw`
|
||||
UPDATE polls p
|
||||
SET
|
||||
deleted = true,
|
||||
deleted_at = NOW()
|
||||
WHERE touched_at < NOW() - INTERVAL '30 days'
|
||||
AND deleted = false
|
||||
AND id NOT IN (
|
||||
SELECT poll_id
|
||||
FROM options
|
||||
WHERE poll_id = p.id
|
||||
AND start_time > NOW()
|
||||
)
|
||||
AND user_id NOT IN (
|
||||
SELECT id
|
||||
FROM users
|
||||
WHERE id IN (
|
||||
SELECT user_id
|
||||
FROM user_payment_data
|
||||
WHERE end_date > NOW()
|
||||
)
|
||||
OR subscription_id IN (
|
||||
SELECT subscription_id
|
||||
FROM subscriptions
|
||||
WHERE active = true
|
||||
)
|
||||
);
|
||||
`;
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
summary: {
|
||||
markedDeleted,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove polls and corresponding data that have been marked deleted for more than 7 days.
|
||||
*/
|
||||
async function removeDeletedPolls(req: Request) {
|
||||
const options = (await req.json()) as { take?: number } | undefined;
|
||||
// First get the ids of all the polls that have been marked as deleted for at least 7 days
|
||||
const deletedPolls = await prisma.poll.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
deleted: true,
|
||||
deletedAt: {
|
||||
lt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
|
||||
},
|
||||
},
|
||||
take: options?.take ?? 1000,
|
||||
});
|
||||
|
||||
const deletedPollIds = deletedPolls.map((poll) => poll.id);
|
||||
|
||||
const { count: deletedPollCount } = await prisma.poll.deleteMany({
|
||||
where: {
|
||||
id: {
|
||||
in: deletedPollIds,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
summary: {
|
||||
deleted: {
|
||||
polls: deletedPollCount,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
import { prisma } from "@rallly/database";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { checkApiAuthorization } from "@/utils/api-auth";
|
||||
|
||||
/**
|
||||
* Marks inactive polls as deleted. Polls are inactive if they have not been
|
||||
* touched in the last 30 days and all dates are in the past.
|
||||
* Only marks polls as deleted if they belong to users without an active subscription
|
||||
* or if they don't have a user associated with them.
|
||||
*/
|
||||
export async function POST() {
|
||||
const unauthorized = checkApiAuthorization();
|
||||
if (unauthorized) return unauthorized;
|
||||
|
||||
// Mark inactive polls as deleted in a single query
|
||||
const { count: markedDeleted } = await prisma.poll.updateMany({
|
||||
where: {
|
||||
touchedAt: {
|
||||
lt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), // 30 days ago
|
||||
},
|
||||
deleted: false,
|
||||
options: {
|
||||
none: {
|
||||
startTime: {
|
||||
gt: new Date(),
|
||||
},
|
||||
},
|
||||
},
|
||||
// Include polls without a user or with users that don't have an active subscription
|
||||
OR: [
|
||||
{ userId: null },
|
||||
{
|
||||
user: {
|
||||
OR: [{ subscription: null }, { subscription: { active: false } }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
data: {
|
||||
deleted: true,
|
||||
deletedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
summary: {
|
||||
markedDeleted,
|
||||
},
|
||||
});
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
import { prisma } from "@rallly/database";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { checkApiAuthorization } from "@/utils/api-auth";
|
||||
|
||||
/**
|
||||
* Remove polls and corresponding data that have been marked deleted for more than 7 days.
|
||||
*/
|
||||
export async function POST(req: Request) {
|
||||
const unauthorized = checkApiAuthorization();
|
||||
if (unauthorized) return unauthorized;
|
||||
|
||||
const options = (await req.json()) as { take?: number } | undefined;
|
||||
|
||||
// First get the ids of all the polls that have been marked as deleted for at least 7 days
|
||||
const deletedPolls = await prisma.poll.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
deleted: true,
|
||||
deletedAt: {
|
||||
lt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
|
||||
},
|
||||
},
|
||||
take: options?.take ?? 1000,
|
||||
});
|
||||
|
||||
const deletedPollIds = deletedPolls.map((poll) => poll.id);
|
||||
|
||||
const { count: deletedPollCount } = await prisma.poll.deleteMany({
|
||||
where: {
|
||||
id: {
|
||||
in: deletedPollIds,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
summary: {
|
||||
deleted: {
|
||||
polls: deletedPollCount,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
20
apps/web/src/utils/api-auth.ts
Normal file
20
apps/web/src/utils/api-auth.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { headers } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
/**
|
||||
* Checks if the request is authorized using the API secret
|
||||
* @returns A NextResponse with 401 status if unauthorized, or null if authorized
|
||||
*/
|
||||
export function checkApiAuthorization() {
|
||||
const headersList = headers();
|
||||
const authorization = headersList.get("authorization");
|
||||
|
||||
if (authorization !== `Bearer ${process.env.API_SECRET}`) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "Unauthorized" },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
273
apps/web/tests/house-keeping.spec.ts
Normal file
273
apps/web/tests/house-keeping.spec.ts
Normal file
|
@ -0,0 +1,273 @@
|
|||
import { expect, test } from "@playwright/test";
|
||||
import { prisma } from "@rallly/database";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
/**
|
||||
* This test suite tests the house-keeping API endpoints:
|
||||
* 1. delete-inactive-polls: Marks inactive polls as deleted
|
||||
* 2. remove-deleted-polls: Permanently removes polls that have been marked as deleted for more than 7 days
|
||||
*/
|
||||
test.describe("House-keeping API", () => {
|
||||
// Store created poll IDs for cleanup
|
||||
const createdPollIds: string[] = [];
|
||||
const createdUserIds: string[] = [];
|
||||
|
||||
// API Secret for authentication
|
||||
const API_SECRET = process.env.API_SECRET;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
// Clean up any existing test data
|
||||
await cleanup();
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
// Clean up test data
|
||||
await cleanup();
|
||||
});
|
||||
|
||||
async function cleanup() {
|
||||
// Delete test users - related polls will be deleted automatically due to cascade
|
||||
if (createdUserIds.length > 0) {
|
||||
await prisma.user.deleteMany({
|
||||
where: {
|
||||
id: {
|
||||
in: createdUserIds,
|
||||
},
|
||||
},
|
||||
});
|
||||
createdUserIds.length = 0;
|
||||
}
|
||||
|
||||
// Delete polls that don't have a user (not covered by cascade delete)
|
||||
if (createdPollIds.length > 0) {
|
||||
await prisma.poll.deleteMany({
|
||||
where: {
|
||||
id: {
|
||||
in: createdPollIds,
|
||||
},
|
||||
userId: null,
|
||||
},
|
||||
});
|
||||
createdPollIds.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
test("should mark inactive polls as deleted for users without active subscriptions", async ({
|
||||
request,
|
||||
baseURL,
|
||||
}) => {
|
||||
// Create test users
|
||||
const regularUser = await prisma.user.create({
|
||||
data: {
|
||||
name: "Regular User",
|
||||
email: "regular-user@example.com",
|
||||
},
|
||||
});
|
||||
createdUserIds.push(regularUser.id);
|
||||
|
||||
const proUser = await prisma.user.create({
|
||||
data: {
|
||||
name: "Pro User",
|
||||
email: "pro-user@example.com",
|
||||
subscription: {
|
||||
create: {
|
||||
id: "sub_test_pro",
|
||||
priceId: "price_test",
|
||||
amount: 1000,
|
||||
status: "active",
|
||||
active: true,
|
||||
currency: "USD",
|
||||
interval: "month",
|
||||
periodStart: new Date(),
|
||||
periodEnd: dayjs().add(30, "day").toDate(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
createdUserIds.push(proUser.id);
|
||||
|
||||
// Create test polls
|
||||
|
||||
// 1. Old poll from regular user (should be marked as deleted)
|
||||
const oldPollRegularUser = await prisma.poll.create({
|
||||
data: {
|
||||
id: "old-poll-regular-user",
|
||||
title: "Old Poll Regular User",
|
||||
participantUrlId: "old-poll-regular-user-participant",
|
||||
adminUrlId: "old-poll-regular-user-admin",
|
||||
userId: regularUser.id,
|
||||
touchedAt: dayjs().subtract(35, "day").toDate(), // 35 days old
|
||||
},
|
||||
});
|
||||
createdPollIds.push(oldPollRegularUser.id);
|
||||
|
||||
// 2. Old poll from pro user (should NOT be marked as deleted)
|
||||
const oldPollProUser = await prisma.poll.create({
|
||||
data: {
|
||||
id: "old-poll-pro-user",
|
||||
title: "Old Poll Pro User",
|
||||
participantUrlId: "old-poll-pro-user-participant",
|
||||
adminUrlId: "old-poll-pro-user-admin",
|
||||
userId: proUser.id,
|
||||
touchedAt: dayjs().subtract(35, "day").toDate(), // 35 days old
|
||||
},
|
||||
});
|
||||
createdPollIds.push(oldPollProUser.id);
|
||||
|
||||
// 3. Recent poll from regular user (should NOT be marked as deleted)
|
||||
const recentPollRegularUser = await prisma.poll.create({
|
||||
data: {
|
||||
id: "recent-poll-regular-user",
|
||||
title: "Recent Poll Regular User",
|
||||
participantUrlId: "recent-poll-regular-user-participant",
|
||||
adminUrlId: "recent-poll-regular-user-admin",
|
||||
userId: regularUser.id,
|
||||
touchedAt: dayjs().subtract(15, "day").toDate(), // 15 days old
|
||||
},
|
||||
});
|
||||
createdPollIds.push(recentPollRegularUser.id);
|
||||
|
||||
// 4. Old poll with future options from regular user (should NOT be marked as deleted)
|
||||
const oldPollWithFutureOptions = await prisma.poll.create({
|
||||
data: {
|
||||
id: "old-poll-with-future-options",
|
||||
title: "Old Poll With Future Options",
|
||||
participantUrlId: "old-poll-with-future-options-participant",
|
||||
adminUrlId: "old-poll-with-future-options-admin",
|
||||
userId: regularUser.id,
|
||||
touchedAt: dayjs().subtract(35, "day").toDate(), // 35 days old
|
||||
options: {
|
||||
create: {
|
||||
startTime: dayjs().add(10, "day").toDate(), // Future date
|
||||
duration: 60,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
createdPollIds.push(oldPollWithFutureOptions.id);
|
||||
|
||||
// 5. Old poll without a user (should be marked as deleted)
|
||||
const oldPollNoUser = await prisma.poll.create({
|
||||
data: {
|
||||
id: "old-poll-no-user",
|
||||
title: "Old Poll No User",
|
||||
participantUrlId: "old-poll-no-user-participant",
|
||||
adminUrlId: "old-poll-no-user-admin",
|
||||
touchedAt: dayjs().subtract(35, "day").toDate(), // 35 days old
|
||||
},
|
||||
});
|
||||
createdPollIds.push(oldPollNoUser.id);
|
||||
|
||||
// Call the delete-inactive-polls endpoint
|
||||
const response = await request.post(
|
||||
`${baseURL}/api/house-keeping/delete-inactive-polls`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${API_SECRET}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const responseData = await response.json();
|
||||
expect(responseData.success).toBeTruthy();
|
||||
|
||||
// We expect 2 polls to be marked as deleted:
|
||||
// - Old poll from regular user
|
||||
// - Old poll without a user
|
||||
expect(responseData.summary.markedDeleted).toBe(2);
|
||||
|
||||
// Verify the state of each poll
|
||||
const updatedOldPollRegularUser = await prisma.poll.findUnique({
|
||||
where: { id: oldPollRegularUser.id },
|
||||
});
|
||||
expect(updatedOldPollRegularUser?.deleted).toBe(true);
|
||||
expect(updatedOldPollRegularUser?.deletedAt).not.toBeNull();
|
||||
|
||||
const updatedOldPollProUser = await prisma.poll.findUnique({
|
||||
where: { id: oldPollProUser.id },
|
||||
});
|
||||
expect(updatedOldPollProUser?.deleted).toBe(false);
|
||||
expect(updatedOldPollProUser?.deletedAt).toBeNull();
|
||||
|
||||
const updatedRecentPollRegularUser = await prisma.poll.findUnique({
|
||||
where: { id: recentPollRegularUser.id },
|
||||
});
|
||||
expect(updatedRecentPollRegularUser?.deleted).toBe(false);
|
||||
expect(updatedRecentPollRegularUser?.deletedAt).toBeNull();
|
||||
|
||||
const updatedOldPollWithFutureOptions = await prisma.poll.findUnique({
|
||||
where: { id: oldPollWithFutureOptions.id },
|
||||
});
|
||||
expect(updatedOldPollWithFutureOptions?.deleted).toBe(false);
|
||||
expect(updatedOldPollWithFutureOptions?.deletedAt).toBeNull();
|
||||
|
||||
const updatedOldPollNoUser = await prisma.poll.findUnique({
|
||||
where: { id: oldPollNoUser.id },
|
||||
});
|
||||
expect(updatedOldPollNoUser?.deleted).toBe(true);
|
||||
expect(updatedOldPollNoUser?.deletedAt).not.toBeNull();
|
||||
});
|
||||
|
||||
test("should permanently remove polls that have been marked as deleted for more than 7 days", async ({
|
||||
request,
|
||||
baseURL,
|
||||
}) => {
|
||||
// Create a poll that was marked as deleted more than 7 days ago
|
||||
const oldDeletedPoll = await prisma.poll.create({
|
||||
data: {
|
||||
id: "old-deleted-poll",
|
||||
title: "Old Deleted Poll",
|
||||
participantUrlId: "old-deleted-poll-participant",
|
||||
adminUrlId: "old-deleted-poll-admin",
|
||||
deleted: true,
|
||||
deletedAt: dayjs().subtract(8, "day").toDate(), // Deleted 8 days ago
|
||||
},
|
||||
});
|
||||
createdPollIds.push(oldDeletedPoll.id);
|
||||
|
||||
// Create a poll that was marked as deleted less than 7 days ago
|
||||
const recentDeletedPoll = await prisma.poll.create({
|
||||
data: {
|
||||
id: "recent-deleted-poll",
|
||||
title: "Recent Deleted Poll",
|
||||
participantUrlId: "recent-deleted-poll-participant",
|
||||
adminUrlId: "recent-deleted-poll-admin",
|
||||
deleted: true,
|
||||
deletedAt: dayjs().subtract(3, "day").toDate(), // Deleted 3 days ago
|
||||
},
|
||||
});
|
||||
createdPollIds.push(recentDeletedPoll.id);
|
||||
|
||||
// Call the remove-deleted-polls endpoint
|
||||
const response = await request.post(
|
||||
`${baseURL}/api/house-keeping/remove-deleted-polls`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${API_SECRET}`,
|
||||
},
|
||||
data: {}, // Empty JSON body
|
||||
},
|
||||
);
|
||||
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const responseData = await response.json();
|
||||
expect(responseData.success).toBeTruthy();
|
||||
|
||||
// We expect 1 poll to be permanently deleted
|
||||
expect(responseData.summary.deleted.polls).toBe(1);
|
||||
|
||||
// Verify that the old deleted poll is gone
|
||||
const checkOldDeletedPoll = await prisma.poll.findUnique({
|
||||
where: { id: oldDeletedPoll.id },
|
||||
});
|
||||
expect(checkOldDeletedPoll).toBeNull();
|
||||
|
||||
// Verify that the recent deleted poll still exists
|
||||
const checkRecentDeletedPoll = await prisma.poll.findUnique({
|
||||
where: { id: recentDeletedPoll.id },
|
||||
});
|
||||
expect(checkRecentDeletedPoll).not.toBeNull();
|
||||
expect(checkRecentDeletedPoll?.deleted).toBe(true);
|
||||
});
|
||||
});
|
Loading…
Add table
Reference in a new issue