feat: trash (soft delete) with admin tool management
Build & Push Docker Image / build (push) Successful in 2m15s
Build & Push Docker Image / build (push) Successful in 2m15s
- tools: add deletedAt/deletedBy, soft delete via DELETE /tools/:id when actor has trash entitlement, else immediate hard delete - trash endpoints: GET /tools/trash, POST /tools/trash (admin bulk), POST /tools/trash/restore, DELETE /tools/trash, POST /tools/trash/empty - trash feature for premium/enterprise; exclude trashed from all public surfaces (browse, categories, features, tags, similar, ratings, costs, analytics, redundancy) - TRASH_RETENTION_DAYS env (0 = keep forever) with hourly purge job - frontend: /trash page (premium+, restore for all, permanent delete + empty for admin), admin Tools tab with multi-select bulk trash, sidebar Trash link, tool-detail delete hint
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import app from "./app";
|
||||
import { logger } from "./lib/logger";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { db, usersTable } from "@workspace/db";
|
||||
import { db, usersTable, toolsTable } from "@workspace/db";
|
||||
import { sql } from "drizzle-orm";
|
||||
|
||||
const rawPort = process.env["PORT"];
|
||||
@@ -178,6 +178,23 @@ async function ensureAdminTier(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function purgeTrash(): Promise<void> {
|
||||
const retentionDays = Number(process.env["TRASH_RETENTION_DAYS"] ?? "0");
|
||||
if (!Number.isFinite(retentionDays) || retentionDays <= 0) return;
|
||||
try {
|
||||
const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000);
|
||||
const result = await db
|
||||
.delete(toolsTable)
|
||||
.where(sql`${toolsTable.deletedAt} IS NOT NULL AND ${toolsTable.deletedAt} < ${cutoff}`)
|
||||
.returning({ id: toolsTable.id });
|
||||
if (result.length > 0) {
|
||||
logger.info({ count: result.length }, "Purged expired trash entries");
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error({ err }, "Failed to purge trash");
|
||||
}
|
||||
}
|
||||
|
||||
async function start(): Promise<void> {
|
||||
await ensureSessionsTable();
|
||||
await ensureToolRelationsTable();
|
||||
@@ -187,6 +204,15 @@ async function start(): Promise<void> {
|
||||
await ensureUserColumns();
|
||||
await seedAdminUser();
|
||||
await ensureAdminTier();
|
||||
await purgeTrash();
|
||||
|
||||
const retentionDays = Number(process.env["TRASH_RETENTION_DAYS"] ?? "0");
|
||||
if (Number.isFinite(retentionDays) && retentionDays > 0) {
|
||||
setInterval(() => {
|
||||
void purgeTrash();
|
||||
}, 60 * 60 * 1000);
|
||||
logger.info({ retentionDays }, "Trash auto-purge enabled");
|
||||
}
|
||||
|
||||
app.listen(port, (err) => {
|
||||
if (err) {
|
||||
|
||||
Reference in New Issue
Block a user