feat: trash (soft delete) with admin tool management
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:
opencode
2026-08-02 02:02:16 +02:00
parent 32df998399
commit 2f20b1e5c2
25 changed files with 1404 additions and 28 deletions
+80 -1
View File
@@ -122,7 +122,9 @@ export const UpdateToolResponse = zod.object({
"features": zod.array(zod.string()).optional(),
"tags": zod.array(zod.string()).optional(),
"createdAt": zod.coerce.date(),
"updatedAt": zod.coerce.date()
"updatedAt": zod.coerce.date(),
"deletedAt": zod.coerce.date().nullish(),
"deletedBy": zod.string().nullish()
})
@@ -134,6 +136,83 @@ export const DeleteToolParams = zod.object({
})
/**
* @summary List trashed (soft-deleted) tools
*/
export const ListTrashedToolsQueryParams = zod.object({
"search": zod.coerce.string().optional()
})
export const ListTrashedToolsResponseItem = zod.object({
"id": zod.number(),
"name": zod.string(),
"description": zod.string(),
"category": zod.string(),
"websiteUrl": zod.string().nullish(),
"iconUrl": zod.string().nullish(),
"createdBy": zod.string().nullish(),
"features": zod.array(zod.string()).optional(),
"tags": zod.array(zod.string()).optional(),
"createdAt": zod.coerce.date(),
"updatedAt": zod.coerce.date(),
"deletedAt": zod.coerce.date().nullish(),
"deletedBy": zod.string().nullish()
})
export const ListTrashedToolsResponse = zod.array(ListTrashedToolsResponseItem)
/**
* @summary Move tools to trash (admin)
*/
export const trashToolsBodyIdsMax = 500;
export const TrashToolsBody = zod.object({
"ids": zod.array(zod.number()).min(1).max(trashToolsBodyIdsMax)
})
export const TrashToolsResponse = zod.object({
"trashed": zod.number().optional()
})
/**
* @summary Permanently delete trashed tools (admin)
*/
export const deleteTrashedToolsBodyIdsMax = 500;
export const DeleteTrashedToolsBody = zod.object({
"ids": zod.array(zod.number()).min(1).max(deleteTrashedToolsBodyIdsMax)
})
/**
* @summary Restore trashed tools
*/
export const restoreToolsBodyIdsMax = 500;
export const RestoreToolsBody = zod.object({
"ids": zod.array(zod.number()).min(1).max(restoreToolsBodyIdsMax)
})
export const RestoreToolsResponse = zod.object({
"restored": zod.number().optional()
})
/**
* @summary Permanently delete all trashed tools (admin)
*/
export const EmptyTrashResponse = zod.object({
"deleted": zod.number().optional()
})
/**
* @summary List ratings for a tool
*/