Add: /api/admin/bulk-close-pre-cutoff route with audit logging
Marks all open tasks with dueDate < 2026-04-01 as COMPLETED. Supports ?dryRun=true for safe preview before execution. Writes batched AuditLog entries + a summary entry per run. Admin-only (403 for non-admins).
This commit is contained in:
parent
c20d6b8f1a
commit
79b6991b31
2 changed files with 125 additions and 1 deletions
|
|
@ -34,7 +34,10 @@
|
||||||
"Bash(curl -s 'https://api.pangolin.wulfconsulting.cloud/v1/org/seubert-and-associates/domains?limit=1000' -H 'Authorization: Bearer uqfgsbiou7j9fvt.fcznfl7wtfi7ap3267asgqxgsjzwyzej5tdiun62')",
|
"Bash(curl -s 'https://api.pangolin.wulfconsulting.cloud/v1/org/seubert-and-associates/domains?limit=1000' -H 'Authorization: Bearer uqfgsbiou7j9fvt.fcznfl7wtfi7ap3267asgqxgsjzwyzej5tdiun62')",
|
||||||
"Bash(curl -s https://api.pangolin.wulfconsulting.cloud/v1/target/48 -H 'Authorization: Bearer uqfgsbiou7j9fvt.fcznfl7wtfi7ap3267asgqxgsjzwyzej5tdiun62')",
|
"Bash(curl -s https://api.pangolin.wulfconsulting.cloud/v1/target/48 -H 'Authorization: Bearer uqfgsbiou7j9fvt.fcznfl7wtfi7ap3267asgqxgsjzwyzej5tdiun62')",
|
||||||
"Bash(curl -s https://api.pangolin.wulfconsulting.cloud/v1/resource/42/rules -H 'Authorization: Bearer uqfgsbiou7j9fvt.fcznfl7wtfi7ap3267asgqxgsjzwyzej5tdiun62')",
|
"Bash(curl -s https://api.pangolin.wulfconsulting.cloud/v1/resource/42/rules -H 'Authorization: Bearer uqfgsbiou7j9fvt.fcznfl7wtfi7ap3267asgqxgsjzwyzej5tdiun62')",
|
||||||
"Bash(curl -s -X PUT https://api.pangolin.wulfconsulting.cloud/v1/resource/42/rule -H 'Authorization: Bearer uqfgsbiou7j9fvt.fcznfl7wtfi7ap3267asgqxgsjzwyzej5tdiun62' -H 'Content-Type: application/json' -d '{:*)"
|
"Bash(curl -s -X PUT https://api.pangolin.wulfconsulting.cloud/v1/resource/42/rule -H 'Authorization: Bearer uqfgsbiou7j9fvt.fcznfl7wtfi7ap3267asgqxgsjzwyzej5tdiun62' -H 'Content-Type: application/json' -d '{:*)",
|
||||||
|
"Bash(PGPASSWORD='ondeck_password_2026!' psql *)",
|
||||||
|
"Bash(docker exec *)",
|
||||||
|
"Bash(DATABASE_URL=\"postgresql://ondeck_user:ondeck_password_2026!@localhost:5432/ondeck\" node *)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
121
ondeck/src/app/api/admin/bulk-close-pre-cutoff/route.ts
Normal file
121
ondeck/src/app/api/admin/bulk-close-pre-cutoff/route.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { getServerSession } from 'next-auth'
|
||||||
|
import { authOptions } from '@/lib/auth'
|
||||||
|
import { prisma } from '@/lib/db'
|
||||||
|
|
||||||
|
const CUTOFF_DATE = new Date('2026-04-01T00:00:00.000Z')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/admin/bulk-close-pre-cutoff
|
||||||
|
* Admin-only.
|
||||||
|
*
|
||||||
|
* Marks all NOT_STARTED tasks with dueDate < 2026-04-01 as COMPLETED,
|
||||||
|
* with naReason noting they were bulk-closed as pre-cutoff historical data.
|
||||||
|
* Writes a single summarising AuditLog entry + one entry per batch chunk.
|
||||||
|
*
|
||||||
|
* Query param: ?dryRun=true — count only, no mutations.
|
||||||
|
*/
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const session = await getServerSession(authOptions)
|
||||||
|
if (!session?.user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
const userRoles = (session.user as any).roles || []
|
||||||
|
if (!userRoles.includes('Admin')) return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
|
||||||
|
const userId = (session.user as any).id as string
|
||||||
|
const dryRun = new URL(request.url).searchParams.get('dryRun') === 'true'
|
||||||
|
|
||||||
|
// ── Count affected rows ────────────────────────────────────────────────
|
||||||
|
const affected = await prisma.task.count({
|
||||||
|
where: {
|
||||||
|
dueDate: { lt: CUTOFF_DATE },
|
||||||
|
status: { notIn: ['COMPLETED', 'CANCELLED', 'NA'] },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (dryRun) {
|
||||||
|
return NextResponse.json({
|
||||||
|
dryRun: true,
|
||||||
|
wouldClose: affected,
|
||||||
|
cutoff: CUTOFF_DATE.toISOString().slice(0, 10),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (affected === 0) {
|
||||||
|
return NextResponse.json({ closed: 0, message: 'No eligible tasks found.' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Bulk-update in batches of 500 to avoid lock contention ────────────
|
||||||
|
const BATCH = 500
|
||||||
|
let closed = 0
|
||||||
|
const now = new Date()
|
||||||
|
const naReason = 'Bulk-closed: pre-cutoff historical data (tasks due before 2026-04-01)'
|
||||||
|
|
||||||
|
while (closed < affected) {
|
||||||
|
// Fetch a batch of IDs
|
||||||
|
const ids = await prisma.task.findMany({
|
||||||
|
where: {
|
||||||
|
dueDate: { lt: CUTOFF_DATE },
|
||||||
|
status: { notIn: ['COMPLETED', 'CANCELLED', 'NA'] },
|
||||||
|
},
|
||||||
|
select: { id: true },
|
||||||
|
take: BATCH,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (ids.length === 0) break
|
||||||
|
|
||||||
|
await prisma.task.updateMany({
|
||||||
|
where: { id: { in: ids.map((t) => t.id) } },
|
||||||
|
data: {
|
||||||
|
status: 'COMPLETED',
|
||||||
|
completedAt: now,
|
||||||
|
completedBy: userId,
|
||||||
|
naReason,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
closed += ids.length
|
||||||
|
|
||||||
|
await prisma.auditLog.create({
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
action: 'BULK_CLOSE_PRE_CUTOFF',
|
||||||
|
entityType: 'Task',
|
||||||
|
newValues: {
|
||||||
|
batchSize: ids.length,
|
||||||
|
runningTotal: closed,
|
||||||
|
cutoffDate: CUTOFF_DATE.toISOString().slice(0, 10),
|
||||||
|
reason: naReason,
|
||||||
|
taskIds: ids.map((t) => t.id),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Summary audit entry ───────────────────────────────────────────────
|
||||||
|
await prisma.auditLog.create({
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
action: 'BULK_CLOSE_PRE_CUTOFF_SUMMARY',
|
||||||
|
entityType: 'Task',
|
||||||
|
newValues: {
|
||||||
|
totalClosed: closed,
|
||||||
|
cutoffDate: CUTOFF_DATE.toISOString().slice(0, 10),
|
||||||
|
executedAt: now.toISOString(),
|
||||||
|
executedBy: (session.user as any).email ?? userId,
|
||||||
|
reason: naReason,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
closed,
|
||||||
|
cutoff: CUTOFF_DATE.toISOString().slice(0, 10),
|
||||||
|
executedAt: now.toISOString(),
|
||||||
|
message: `${closed} tasks marked COMPLETED (pre-cutoff bulk close).`,
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('bulk-close-pre-cutoff error:', error)
|
||||||
|
return NextResponse.json({ error: 'Internal server error', detail: error.message }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue