Skip to content

feat(lists): delete icon to remove contacts from a list - #8

Merged
sagnik11 merged 1 commit into
mainfrom
feat/delete-contact-from-list
Oct 2, 2026
Merged

sagnik11 merged 1 commit into
mainfrom
feat/delete-contact-from-list

Conversation

@sagnik11

@sagnik11 sagnik11 commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

What

Adds a trash icon to every contact row on the list detail page (/lists/[id]) so a contact can be removed without going through the GDPR flow.

  • Trash icon in the row actions cell, next to the existing ... menu.
  • Clicking it opens a confirmation dialog naming the contact and warning that the contact plus its send and engagement history are permanently removed.
  • On confirm, the contact is deleted, a toast confirms, the table and list counts refresh, and if the last row on a later page was removed it steps back one page.
  • The icon is disabled while that row is being deleted and the dialog cannot be dismissed mid-request.

API

New route: DELETE /api/internal/contacts/[id]

  • Reuses hardDeleteContact from lib/gdpr, so campaign_sends and campaign_events cascade away with the contact.
  • Writes a contact.delete audit log entry with the email, list id, and how many sends and events were removed.
  • Returns 404 when the contact does not exist.

Route sits under /api/internal, so the existing session middleware guard applies.

Notes

  • The GDPR delete flow (email typed to confirm) is untouched and still available in the ... menu.
  • tsc --noEmit and next lint are clean. Existing warnings elsewhere are pre-existing.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


View code changes stack in Autter

Summary

Summary generated by Autter.

Why this change

The list detail view lacked a lightweight way to remove a single contact, leaving only the GDPR delete flow that requires email confirmation and deletes all related data. This change adds a per-row removal action for routine list management.

What changes for users

The contacts table on app/(dashboard)/lists/[id]/page.tsx now shows a delete (Trash2) icon with a confirmation dialog that removes the contact via DELETE /api/internal/contacts/[id]. After deletion the contacts list and counts refresh with success or error toasts.

Implementation and review notes

  • New app/api/internal/contacts/[id]/route.ts (+34 lines) backs the delete action; check auth, validation, and NextResponse.json error shape.
  • Updated app/(dashboard)/lists/[id]/page.tsx (+103/-22) with deleteTarget/deleting state, row Trash2 action, and confirmation Dialog.
  • Confirm separation from existing gdpr-delete and gdpr-export handlers in ListDetailPage.
  • Verify post-delete refresh of contacts, pagination, and list counts plus toast error paths.

Changes

  • Added DELETE /api/internal/contacts/[id] in app/api/internal/contacts/[id]/route.ts to delete a single contact by ID with NextResponse.json error handling.
  • Updated app/(dashboard)/lists/[id]/page.tsx to add per-row Trash2 delete action in the contacts table.
  • Added deleteTarget and deleting state plus confirmation Dialog for the non-GDPR removal flow, distinct from existing gdprDeleteContact and gdpr-export logic.
  • Wired post-delete refresh of contacts, pagination meta, and list counts with success and destructive toast feedback.
  • No breaking changes: new route is additive and existing GDPR export and delete endpoints and contracts are unchanged.

Acceptance Criteria

  • Contacts table on list detail shows a delete icon for each row without altering existing GDPR actions.
  • Clicking delete opens a confirmation dialog that requires explicit confirm before calling the API.
  • Confirming calls DELETE /api/internal/contacts/[id] and removes the contact from the list on success.
  • Contacts list, pagination total, and header counts refresh after successful deletion.
  • Failed deletes surface a destructive toast and leave list state unchanged.
  • npx tsc --noEmit and npm run lint pass.

Test Plan

  • Open a list with contacts at /lists/[id] and confirm a delete icon appears in each contact row.
  • Click delete on a test contact and confirm the confirmation dialog appears with correct email.
  • Confirm deletion and verify success toast plus contact removed without full page reload.
  • Verify list header counts and pagination totals update after deletion.
  • Test error path by deleting with invalid session or non-existent ID and verify destructive toast.
  • Verify GDPR export and GDPR delete flows still work unchanged on the same page.
  • Run npx tsc --noEmit and npm run lint.

Rollback Plan

  • Revert commit 68b2768e65de7ba98a80b2e80a94e62a515ede75 to remove the new route and UI action, then redeploy the web app.
  • No migration rollback needed: no changes to lib/db/schema.ts or drizzle/migrations/.
  • No feature flag or config cleanup required.
  • Note: contacts deleted before rollback are not auto-restored and must be re-added via Add contact or list upload if needed.

Related Issues

  • No linked issue was identified from branch name, commit messages, or diff context.

Written for commit 68b2768. Summary will update on new commits.

Adds a trash icon to each contact row on the list detail page, backed by a
confirmation dialog and a new DELETE /api/internal/contacts/[id] route.

The route reuses hardDeleteContact so the contact and its send/event history
are removed via cascade, and writes a contact.delete audit log entry.

The existing GDPR delete flow is unchanged.
@autter-dev

autter-dev Bot commented Oct 2, 2026

Copy link
Copy Markdown

🚦 Pre-merge checks · ⚠️ 4 warning, ✅ 161 passed

Needs attention

Check Status Explanation
Rate limiting not detected ⚠️ Warning 1 potential issue(s) detected (max risk 45/100): app/api/internal/contacts/[id]/route.ts:5.
PII in logs ⚠️ Warning 1 potential issue(s) detected (max risk 70/100): app/api/internal/contacts/[id]/route.ts:20.
Source changes without matching tests ⚠️ Warning 6 root-cause issue(s) detected (max risk 65/100): app/api/internal/contacts/[id]/route.ts:5, app/api/internal/contacts/[id]/route.ts:15, app/(dashboard)/lists/[id]/page.tsx:33, app/(dashboard)/lists/[id]/page.tsx:163, app/(dashboard)/lists/[id]/page.tsx:107.
Release Notes Curator ⚠️ Warning 1 finding(s) on changed lines.
✅ Passed checks (161)
Check Status Explanation
Too many files changed ✅ Passed Changed 2 file(s), within the limit of 50.
Too many lines changed ✅ Passed Changed 159 line(s), within the limit of 1000.
Too many unrelated chapters ✅ Passed 2 chapter(s) detected, within the limit of 6.
Generated files hiding real changes ✅ Passed Generated-file volume (0 lines) does not obscure the 159 hand-written line(s).
Missing PR context ✅ Passed PR context looks sufficient.
Mixed concerns (refactor + behavior change) ✅ Passed PR is single behavior feature (new DELETE route + delete UI), no renames, moves, or cleanup refactor.
Migration + app logic + UI combined in one PR ✅ Passed No migration files present; only app logic and UI changes with zero fan-in.
Sensitive data in logs ✅ Passed No sensitive data in logs issues detected.
Log injection ✅ Passed No log injection issues detected.
Missing audit logging ✅ Passed No missing audit logging issues detected.
Removed observability ✅ Passed No removed observability issues detected.
Silent exception swallowing ✅ Passed No silent exception swallowing issues detected.
Unhandled promise rejection ✅ Passed No unhandled promise rejection issues remaining after verification.
Circuit breaker not detected ✅ Passed No circuit breaker not detected issues detected.
Stack trace leakage ✅ Passed No stack trace leakage issues detected.
Possible TOCTOU in critical path ✅ Passed No possible toctou in critical path issues detected.
Idempotency key not detected ✅ Passed No idempotency key not detected issues detected.
Possible non-atomic read-modify-write ✅ Passed No possible non-atomic read-modify-write issues detected.
Optimistic locking not detected ✅ Passed No optimistic locking not detected issues detected.
Rate limiting removed ✅ Passed No rate limiting removed issues detected.
Batch size limit not detected ✅ Passed No batch size limit not detected issues detected.
Pagination not detected ✅ Passed No pagination not detected issues detected.
Prompt injection risk ✅ Passed No LLM/AI-integration code touched by this diff.
LLM output used in a dangerous sink ✅ Passed No LLM/AI-integration code touched by this diff.
Sensitive data in prompt or system-prompt leakage ✅ Passed No LLM/AI-integration code touched by this diff.
Over-privileged LLM tool / excessive agency ✅ Passed No LLM/AI-integration code touched by this diff.
Missing validation on an LLM-driven decision ✅ Passed No LLM/AI-integration code touched by this diff.
Unbounded LLM usage (denial-of-wallet) ✅ Passed No LLM/AI-integration code touched by this diff.
Table exposed without row-level security ✅ Passed No row-level-security-related code touched by this diff.
Over-broad row-level security policy ✅ Passed No row-level-security-related code touched by this diff.
Code path that bypasses row-level security ✅ Passed No row-level-security-related code touched by this diff.
Privileged database credential reachable from the client ✅ Passed No row-level-security-related code touched by this diff.
Privileged query without row-level scoping ✅ Passed No row-level-security-related code touched by this diff.
Row-level security enabled with no policy ✅ Passed No row-level-security-related code touched by this diff.
Template-default gradient styling ✅ Passed No added frontend pages or design-slop markers in this diff.
Interchangeable AI marketing copy ✅ Passed No added frontend pages or design-slop markers in this diff.
Placeholder content shipped to users ✅ Passed No added frontend pages or design-slop markers in this diff.
Emoji standing in for an icon system ✅ Passed No added frontend pages or design-slop markers in this diff.
Call-to-action that goes nowhere ✅ Passed No added frontend pages or design-slop markers in this diff.
Templated page composition ✅ Passed No added frontend pages or design-slop markers in this diff.
Color outside the brand palette ✅ Passed No color outside the brand palette issues detected across 1 changed UI file(s).
Hardcoded style bypassing design tokens ✅ Passed No hardcoded style bypassing design tokens issues detected across 1 changed UI file(s).
Typography outside the brand type system ✅ Passed No typography outside the brand type system issues detected across 1 changed UI file(s).
One-off UI instead of the shared component ✅ Passed No one-off ui instead of the shared component issues detected across 1 changed UI file(s).
Copy that does not match brand voice ✅ Passed Not in scope for this repo's brand baseline.
Screen does not match the rest of the product ✅ Passed No screen does not match the rest of the product issues detected across 1 changed UI file(s).
Merge-blocking marker left in the change ✅ Passed No pending-work markers added by this diff.
Known-defect marker shipped in code ✅ Passed No pending-work markers added by this diff.
Untracked TODO without an issue reference ✅ Passed No pending-work markers added by this diff.
Test disabled or left pending ✅ Passed No pending-work markers added by this diff.
PII or internals leaked in error response ✅ Passed No pii or internals leaked in error response issues detected.
PII stored without application-level encryption ✅ Passed No pii stored without application-level encryption issues detected.
User data stored without retention controls ✅ Passed No user data stored without retention controls issues detected.
PII sent to external / cross-border destination ✅ Passed No pii sent to external / cross-border destination issues detected.
Lockfile resolution / integrity tampered ✅ Passed No lockfile resolution / integrity tampered issues detected.
Dependency runs install-time lifecycle script ✅ Passed No dependency runs install-time lifecycle script issues detected.
Possible dependency-confusion attack ✅ Passed No possible dependency-confusion attack issues detected.
Lockfile resolves a dependency the manifest does not declare ✅ Passed No lockfile resolves a dependency the manifest does not declare issues detected.
Checked-in build artefact modified without source change ✅ Passed No checked-in build artefact modified without source change issues detected.
Dockerfile build-step is insecure ✅ Passed No dockerfile build-step is insecure issues detected.
External artefact pulled in without integrity pinning ✅ Passed No external artefact pulled in without integrity pinning issues detected.
Changed export, importer not updated ✅ Passed No changed export with an un-updated importer detected.
Missing security-team review on sensitive path ✅ Passed No missing security-team review on sensitive path issues detected.
Migration missing rollback / down step ✅ Passed No migration missing rollback / down step issues detected.
Frontend importing database client directly ✅ Passed No frontend importing database client directly issues detected.
Route handler bypassing service layer ✅ Passed No route handler bypassing service layer issues detected.
Backend service importing UI module ✅ Passed No backend service importing ui module issues remaining after verification.
Cross-context internals import ✅ Passed No cross-context internals import issues detected.
Workspace package rule violation ✅ Passed No workspace package rule violation issues detected.
Inconsistent logging pattern ✅ Passed No inconsistent logging pattern issues detected.
Inconsistent error handling ✅ Passed No inconsistent error handling issues detected.
Endpoint missing input validation ✅ Passed No endpoint missing input validation issues remaining after verification.
Multi-write without transaction wrapper ✅ Passed No multi-write without transaction wrapper issues detected.
New feature shipped without feature flag ✅ Passed No new feature shipped without feature flag issues detected.
Module placed in the wrong workspace package ✅ Passed No module placed in the wrong workspace package issues detected.
Hallucinated import (package not installed) ✅ Passed No hallucinated import (package not installed) issues detected.
Nonexistent package (not found in registry) ✅ Passed No nonexistent package (not found in registry) issues detected.
Call to function that does not exist ✅ Passed No call to function that does not exist issues detected.
Generic placeholder identifier in production logic ✅ Passed No generic placeholder identifier in production logic issues remaining after verification.
Repetitive boilerplate (duplicated block) ✅ Passed No repetitive boilerplate (duplicated block) issues detected.
Overbroad try/catch swallowing all exceptions ✅ Passed No overbroad try/catch swallowing all exceptions issues detected.
TODO / FIXME on critical path ✅ Passed No todo / fixme on critical path issues detected.
Comment contradicts or fabricates code behaviour ✅ Passed No comment contradicts or fabricates code behaviour issues detected.
Abstraction defined but never used ✅ Passed No abstraction defined but never used issues detected.
Code style differs from rest of codebase ✅ Passed No code style differs from rest of codebase issues detected.
Established pattern ignored ✅ Passed No established pattern ignored issues detected.
Unhandled edge case (null / empty / zero / boundary) ✅ Passed No unhandled edge case (null / empty / zero / boundary) issues detected.
Insecure training-data / doc-example pattern ✅ Passed No insecure training-data / doc-example pattern issues detected.
Dead code (defined but never referenced) ✅ Passed No dead code (defined but never referenced) issues detected.
Deprecated API call ✅ Passed No deprecated api call issues detected.
API pattern from wrong library version ✅ Passed No api pattern from wrong library version issues detected.
API endpoint removed ✅ Passed No api endpoint removed issues detected.
HTTP method changed (GET ↔ POST etc.) ✅ Passed No http method changed (get ↔ post etc.) issues detected.
New required field added to request ✅ Passed No new required field added to request issues remaining after verification.
Field removed from response schema ✅ Passed No field removed from response schema issues detected.
Response field type changed ✅ Passed No response field type changed issues detected.
HTTP status code changed ✅ Passed No http status code changed issues detected.
Auth requirement added / removed / changed ✅ Passed No auth requirement added / removed / changed issues detected.
Error response shape changed ✅ Passed No error response shape changed issues detected.
Pagination behaviour changed ✅ Passed No pagination behaviour changed issues detected.
Outbound webhook payload schema changed ✅ Passed No outbound webhook payload schema changed issues detected.
GraphQL field removed without deprecation ✅ Passed No graphql field removed without deprecation issues detected.
GraphQL enum value removed ✅ Passed No graphql enum value removed issues detected.
Vendor API consumer ✅ Passed This PR does not touch call sites of an open vendor API change.
SQL injection ✅ Passed No sql injection issues detected.
Cross-site scripting (XSS) ✅ Passed No cross-site scripting (xss) issues detected.
Path traversal ✅ Passed No path traversal issues detected.
Command injection ✅ Passed No command injection issues detected.
Insecure deserialization ✅ Passed No insecure deserialization issues detected.
Weak cryptography ✅ Passed No weak cryptography issues detected.
Hardcoded secret ✅ Passed No hardcoded secret issues detected.
Insecure randomness for security material ✅ Passed No insecure randomness for security material issues detected.
Unsafe file upload ✅ Passed No unsafe file upload issues detected.
Missing input validation ✅ Passed No missing input validation issues detected.
Unsafe CORS configuration ✅ Passed No unsafe cors configuration issues detected.
Unsafe / open redirect ✅ Passed No unsafe / open redirect issues detected.
Missing CSRF protection ✅ Passed No missing csrf protection issues detected.
Unsafe cookie / session settings ✅ Passed No unsafe cookie / session settings issues detected.
Sensitive data exposure ✅ Passed No sensitive data exposure issues detected.
Invisible Unicode / Trojan Source characters ✅ Passed No invisible unicode / trojan source characters issues detected.
API key in source ✅ Passed No api key in source detected.
Access token in source ✅ Passed No access token in source detected.
Private key in source ✅ Passed No private key in source detected.
Database connection URL with embedded credentials ✅ Passed No database connection url with embedded credentials detected.
Cloud credential in source ✅ Passed No cloud credential in source detected.
Webhook signing secret in source ✅ Passed No webhook signing secret in source detected.
OAuth client secret in source ✅ Passed No oauth client secret in source detected.
JWT signing secret in source ✅ Passed No jwt signing secret in source detected.
Hardcoded password ✅ Passed No hardcoded password detected.
Auth middleware removed from route ✅ Passed No auth middleware removed from route issues detected.
Route protection changed (protected → public) ✅ Passed No route protection changed (protected → public) issues detected.
Permission / RBAC check removed ✅ Passed No permission / rbac check removed issues detected.
Required role weakened ✅ Passed No required role weakened issues detected.
Admin-only route exposed to lower privilege ✅ Passed No admin-only route exposed to lower privilege issues detected.
Token validation skipped in middleware chain ✅ Passed No token validation skipped in middleware chain issues detected.
JWT verification weakened or changed ✅ Passed No jwt verification weakened or changed issues detected.
Session expiration / TTL changed ✅ Passed No session expiration / ttl changed issues detected.
Password reset flow changed ✅ Passed No password reset flow changed issues detected.
OAuth callback / redirect handling changed ✅ Passed No oauth callback / redirect handling changed issues detected.
Webhook endpoint missing signature verification ✅ Passed No webhook endpoint missing signature verification issues detected.
Public route touches private/PII data ✅ Passed No public route touches private/pii data issues detected.
Frontend performance issue ✅ Passed No additional explanation was reported.
Frontend security issue ✅ Passed No additional explanation was reported.
Frontend correctness issue ✅ Passed No additional explanation was reported.
Accessibility issue ✅ Passed No additional explanation was reported.
Frontend maintainability issue ✅ Passed No additional explanation was reported.
Code correctness issue ✅ Passed No additional explanation was reported.
Runtime error risk ✅ Passed No runtime error risk issues remaining after verification.
Resource leak risk ✅ Passed No additional explanation was reported.
Data integrity risk ✅ Passed No additional explanation was reported.
Maintainability issue ✅ Passed No additional explanation was reported.
Co-change coupling ✅ Passed No additional explanation was reported.
Redundant alias / duplicate import ✅ Passed No additional explanation was reported.
Redundant type construct ✅ Passed No additional explanation was reported.
Unnecessary type assertion ✅ Passed No additional explanation was reported.
Module smell ✅ Passed No additional explanation was reported.
Dead export (no callers) ✅ Passed No additional explanation was reported.
Code duplication / DRY violation ✅ Passed No additional explanation was reported.
Intent vs. implementation ✅ Passed The delete icon, confirmation dialog, and backing delete API match everything the PR promised. Requirements: 8 implemented. (Intent from PR description.)
Complexity Guard ✅ Passed No additional explanation was reported.
Bundle Size Monitor ✅ Passed No additional explanation was reported.

This comment is updated automatically whenever Autter reviews a new PR revision.

@autter-dev

autter-dev Bot commented Oct 2, 2026

Copy link
Copy Markdown
⚠️ 8 unconfirmed finding(s) — flagged by a detector but not proven by Autter's verification pass

Inline comments are reserved for findings that survived verification. These are plausible but could not be confirmed from the available context, so they are listed here as FYIs instead — review the ones that look real to you.

  • 🟡 Multi-write without detected transaction (risk 48/100) — app/api/internal/contacts/[id]/route.ts:5 — The implementations of hardDeleteContact and logAudit are not provided. The evidence does not establish which persistence operations occur, whether deletion is transactional internally, or whether audit failures are caught. Multiple awaited helper calls alone do not prove an unsafe partial write.
  • 🟡 Rate limiting not detected (risk 45/100) — app/api/internal/contacts/[id]/route.ts:5 — The handler has no local rate limit, but the provided excerpts omit middleware and the implementations of hardDeleteContact and the audit helpers, so application-level throttling cannot be ruled out. The 404 guard also skips logAudit when deletion returns no result, so repeated requests for an already-deleted contact do not necessarily produce repeated audit writes. The evidence does not establish
  • 🟡 Missing linked tracker issue (risk 49/100) — app/(dashboard)/lists/[id]/page.tsx:33 — The title contains no issue reference, but the PR body and linked-issue metadata are not provided. The supplied repository rules also do not establish an issue-linking requirement.
  • 🟡 Source changes without matching tests (risk 49/100) — app/api/internal/contacts/[id]/route.ts:1 — The provided windows do not include the full PR file list or repository tests, so they cannot establish that no tests were added or modified. The supplied repository rules do not require sibling test files.
  • 🟡 PII in logs (risk 49/100) — app/api/internal/contacts/[id]/route.ts:20 — The endpoint passes result.email to logAudit after a successful deletion, but the provided excerpts do not include logAudit, hardDeleteContact, or audit-log access controls. Without those implementations, it is not possible to establish whether the email is persisted unredacted or whether this is an intended, restricted audit record.
  • 🟡 Source changes without matching tests (risk 45/100) — app/(dashboard)/lists/[id]/page.tsx:163 — The excerpts show the new deletion flow but do not include a complete changed-file list or test inventory, so the absence of matching tests cannot be verified. The supplied contribution requirements specify type-checking, linting, and local verification, not mandatory sibling tests.
  • 🟡 Broken invariant (risk 45/100) — app/(dashboard)/lists/[id]/page.tsx:178 — The handler bases its page decrement on the stale rendered contacts count, so the concurrent-deletion scenario is plausible. However, the provided excerpts omit fetchContacts and the contacts API implementation; either could reconcile an out-of-range page after the refresh. The evidence therefore does not establish that the user remains on an empty page. The claimed broader impact on unrelated han
  • 🟡 Missing CODEOWNERS reviewer approval (risk 40/100) — app/(dashboard)/lists/[id]/page.tsx:107 — No CODEOWNERS file, branch-protection configuration, or reviewer approval records are provided. Single-author ownership does not establish a CODEOWNERS requirement or prove that approval is missing.

@autter-dev

autter-dev Bot commented Oct 2, 2026

Copy link
Copy Markdown

🤖 Release Notes Curator

Impact: minor — Adds new user-facing ability to remove contacts from a list.

Changelog:

You can now remove a contact from a list using the delete action in the contacts table, making it easy to clean up mistaken or unwanted entries.

Custom agent · runs after review · configured in Autter

@autter-dev autter-dev Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Autter posted 1 finding(s) as review threads below (🟠 1). Each carries a copy-paste AI fix prompt.

@@ -0,0 +1,34 @@
import { NextRequest, NextResponse } from 'next/server'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [deterministic] Biome: lint/style/useImportType — Risk: 55/100

Some named imports are only used as types.

🛠 AI fix prompt (copy & paste into your coding agent)
Fix the Biome `lint/style/useImportType` issue at app/api/internal/contacts/[id]/route.ts:1: Some named imports are only used as types.

Flagged by Autter security & observability checks.

@autter-dev autter-dev Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Autter posted 2 finding(s) as review threads below (🟠 1 · 🟡 1). Each carries a copy-paste AI fix prompt.

req: NextRequest,
{ params }: { params: { id: string } }
) {
const result = await hardDeleteContact(params.id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [ai] Unthrottled contact deletion can amplify database work — Risk: 52/100

This new DELETE route is session-authenticated by the global middleware, but it has no route-level rate limit and immediately calls hardDeleteContact. That helper loads every send for the contact and then every related event before cascading the deletion, so repeated requests for contacts with large histories can drive unbounded database reads and delete work from one authenticated session. The repository's explicit limiter is attached by withApiAuth for API-key routes, and public form submissions use an IP token bucket; neither covers this internal route. Add an appropriately keyed limit for this expensive operation before merge.

⚠ Downstream affected — if this fails, it cascades to the usage that depends on this file:

  • Dependent files: app/api/internal/contacts/[id]/route.ts, lib/gdpr/index.ts, middleware.ts, lib/api-auth.ts
🛠 AI fix prompt (copy & paste into your coding agent)
Protect DELETE /api/internal/contacts/[id] with a server-side per-authenticated-user rate limit appropriate for destructive, history-dependent work, and reject over-limit requests before calling hardDeleteContact.

Flagged by Autter security & observability checks.

if (contacts.length === 1 && page > 1) {
setPage(page - 1)
} else {
fetchContacts()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [ai] Post-delete refresh failures leave stale UI or report a false deletion failure — Risk: 32/100

Once DELETE succeeds, a failed contacts GET is swallowed by fetchContacts() and leaves the deleted contact in the displayed array, while a thrown list-info refresh at line 182 reaches this handler's catch and shows Failed to delete contact even though the destructive operation has already completed. In the latter case, the user is told deletion failed and may retry an irreversible operation; in the former, the deleted row remains actionable and a retry returns 404. Keep the delete result separate from refresh errors, remove the deleted row/update counts locally after success, and report refresh failure as a refresh issue rather than a delete failure.

⚠ Downstream affected — if this fails, it cascades to the usage that depends on this file:

  • Dependent files: app/(dashboard)/lists/[id]/page.tsx, app/api/internal/contacts/[id]/route.ts
🛠 AI fix prompt (copy & paste into your coding agent)
Separate the successful DELETE outcome from subsequent refreshes. Remove the deleted contact and update pagination/counts immediately, then handle contact/list refresh errors with a distinct refresh warning rather than reporting that deletion failed.

Flagged by Autter security & observability checks.

@autter-dev

autter-dev Bot commented Oct 2, 2026

Copy link
Copy Markdown

🧪 Autter test run

Autter checked 68b2768e.

This PR changes 2 source files. No project test command ran for the touched workspace. It wrote 1 temporary test for changed files that had no matching test, so the new behavior is still checked. It also executed 1 check from the PR's test plan.

Targeted agent checks: Autter ran targeted checks against the files this PR changed — it executed uncovered test-plan items and, where a changed file had no matching test, wrote a temporary test to verify the new behavior.

Execution summary: 2 checks executed · 2 passed.

Project test commands

No project test commands were detected in this repository.

Autter targeted verification

2 tests executed · 2 passed.

Critical feature verification

Plan: Delete a contact from a list in the dashboard, including server-side persistence and audit logging — On staging, use the authenticated dashboard to create a disposable list and one synthetic contact, then delete that contact from /lists/{listId}. Verify the confirmation UI, DELETE API result, removal from subsequent contact queries, updated list counts, and contact.delete audit event. This exercises the browser-to-API-to-Postgres boundary without sending email. (Configured APP_URL is https://app.autter.dev (production; do not use for this destructive test). The safe target must be an isolated staging deployment running this PR with its own staging database. No staging URL or authenticated test identity is configured in this session.)

  • Setup: Have the test operator provide an isolated staging base URL, staging admin login/session, and a synthetic contact address; do not use production data or credentials.
  • Setup: Sign in at {stagingBaseUrl}/login, open {stagingBaseUrl}/lists, and create a uniquely named disposable list with double opt-in disabled.
  • Setup: On the disposable list page, add one contact using the operator-provided synthetic address. Record the list ID and the created contact ID/email from the UI or the authenticated POST /api/internal/lists/{listId}/contacts response. Do not send a campaign or test email.
  • Setup: Confirm the fixture is in the active contact tab and record the pre-delete active and total counts from GET /api/internal/lists/{listId} and GET /api/internal/lists/{listId}/contacts?status=active&page=1&limit=50.
  • Action: In the signed-in browser, navigate to {stagingBaseUrl}/lists/{listId} and locate the fixture contact row.
  • Action: Verify the row's accessible delete button is named Delete {contactEmail}; click it.
  • Action: Verify the confirmation dialog identifies {contactEmail} and warns that the contact and send/engagement history will be permanently removed. Click Delete.
  • Action: In browser network inspection, verify the UI issued DELETE /api/internal/contacts/{contactId} with the authenticated session. Inspect the response, then allow the UI's follow-up contact and list refresh requests to complete.
  • Action: Repeat DELETE /api/internal/contacts/{contactId} once using the same authenticated browser session to verify the already-removed ID error path; do not substitute a real contact ID.
  • Assert: The confirmation dialog displays the fixture email exactly, and confirming deletion closes the dialog and displays the success toast '{contactEmail} removed from this list'.
  • Assert: The first DELETE returns HTTP 200 with JSON fields deleted=true, id={contactId}, email={contactEmail}, sendCount, and eventCount.
  • Assert: The refreshed GET /api/internal/lists/{listId}/contacts?status=active&page=1&limit=50 contains no row with the fixture contact ID/email; its meta.total and the list's counts.active/counts.total each decrease by one from the recorded pre-delete value.
  • Assert: GET /api/internal/audit-logs?action=contact.delete&resourceType=contact&resourceId={contactId} includes the deletion audit event with resource type contact, resource ID {contactId}, and metadata for the fixture email and list ID.
  • Assert: The repeated DELETE for the deleted ID returns HTTP 404 with JSON error 'Contact not found'; if exercised through the UI, the failure is shown as a destructive toast rather than a success toast.
  • Assert: No email is sent by this scenario.
  • Cleanup: After capturing assertions, delete only the uniquely named disposable staging list via the dashboard and verify it disappears from /lists; if setup failed before contact deletion, remove the fixture contact first using the same staging dashboard.
  • Cleanup: Do not clean up or alter any production list, contact, database, or audit record.

Run could not complete: The only configured APP_URL is production https://app.autter.dev; no staging target or authenticated staging test identity is supplied (ADMIN_EMAIL and ADMIN_PASSWORD are unset), and the documented local target http://localhost:3000 is not running. Contact deletion is irreversible, so there is no safe configured target on which to execute this scenario.

Code-level simulations

Revision 68b2768e65de · 900.1s · up to 2 scenarios in parallel

  • Confirm quick delete and refresh list data — execution incomplete (code_diff, code_diff, spec)
    Source: app/(dashboard)/lists/[id]/page.tsx — toast({ title: ${deleteTarget.email} removed from this list })
    Source: app/(dashboard)/lists/[id]/page.tsx — The contact and its send and engagement history are permanently removed. This cannot be undone.
    Source: PR description — On confirm, the contact is deleted, a toast confirms, the table and list counts refresh
    Indexed blast radius: 0 direct / 0 transitive dependents (indexed 2026-08-31T13:52:28.495Z).
    Execution could not complete: Error: opencode session timed out after 300000ms; stderr: …sage=evaluated permission=bash pattern="git diff --stat" action.permission=bash action.action=allow action.pattern=*
    timestamp=2026-10-02T15:32:39.220Z level=INFO run=ceb8be32 message=evaluated permission=grep pattern="onResolve|simulation-shims|no-reload|routeOk|state\.pageLoads|fixtureContacts" action.permission=* action.action=allow action.pattern=*
    �[0m$ �[0mgit status --short && git diff --stat
    (no output)
    �[0m
    �[0m✱ �[0mGrep "onResolve|simulation-shims|no-reload|routeOk|state.pageLoads|fixtureContacts"�[90m in .autter/scratch · 10 matches�[0m
    �[0m
    �[0m$ �[0mnode --check .autter/scratch/quick-delete-refresh.mjs
    (no output)
    �[0m
    timestamp=2026-10-02T15:32:39.327Z level=INFO run=ceb8be32 message=tracking hash=7f41b709387106a89ae342c8978d4c0918946aa0 cwd=/tmp/autter-wt-sim-ba8b855a1424846ad56c-ZDw1ip/tree git=/tmp/autter-wt-sim-ba8b855a1424846ad56c-ZDw1ip/tree/.opencode-xdg/data/opencode/snapshot/a4d634be0c18939cdd1b81424dc6c8c18ad5487d/803824912aaad55356560727397dc1ed4ae647b6
    timestamp=2026-10-02T15:32:39.360Z level=INFO run=ceb8be32 message=loop session.id=ses_f02c58839ffeeX6HpJI2F1YdSA step=19
    timestamp=2026-10-02T15:32:39.398Z level=INFO run=ceb8be32 message=tracking hash=7f41b709387106a89ae342c8978d4c0918946aa0 cwd=/tmp/autter-wt-sim-ba8b855a1424846ad56c-ZDw1ip/tree git=/tmp/autter-wt-sim-ba8b855a1424846ad56c-ZDw1ip/tree/.opencode-xdg/data/opencode/snapshot/a4d634be0c18939cdd1b81424dc6c8c18ad5487d/803824912aaad55356560727397dc1ed4ae647b6
    timestamp=2026-10-02T15:32:39.403Z level=INFO run=ceb8be32 message=process session.id=ses_f02c58839ffeeX6HpJI2F1YdSA messageID=msg_0fd3eff44001E2CmuerePwWzt2
    timestamp=2026-10-02T15:32:39.404Z level=INFO run=ceb8be32 message=stream providerID=autter-gateway modelID=openai__gpt-6-luna session.id=ses_f02c58839ffeeX6HpJI2F1YdSA small=false agent=build mode=primary
    timestamp=2026-10-02T15:32:39.417Z level=INFO run=ceb8be32 message="llm runtime selected" llm.runtime=ai-s
  • Step back after deleting the only row on a later page — execution incomplete (code_diff, spec)
    Source: app/(dashboard)/lists/[id]/page.tsx — if (contacts.length === 1 && page > 1) {
    setPage(page - 1)
    Source: PR description — if the last row on a later page was removed it steps back one page.
    Indexed blast radius: 0 direct / 0 transitive dependents (indexed 2026-08-31T13:52:28.495Z).
    Execution could not complete: Error: Cannot mount ListDetailPage: no DOM implementation (jsdom, happy-dom, or linkedom) is installed. Installing dependencies is disallowed for this simulation.
  • Delete persisted contact, sends, events, and record audit metadata — execution incomplete (code_diff, code_diff, spec, spec)
    Source: app/api/internal/contacts/[id]/route.ts — 'contact.delete',
    { type: 'contact', id: params.id },
    Source: app/api/internal/contacts/[id]/route.ts — sendCount: result.sendCount,
    eventCount: result.eventCount,
    Source: docs/gdpr.md — Cascades: deletes the contact, all related campaign_sends, and all related campaign_events.
    Source: docs/audit-log.md — metadata: JSON blob with action-specific context
    Execution could not complete: Error: opencode session timed out after 300000ms; stderr: …d" llm.runtime=ai-sdk llm.provider=autter-gateway llm.model=openai__gpt-6-luna
    timestamp=2026-10-02T15:34:12.427Z level=INFO run=78944173 message=evaluated permission=edit pattern=.autter/scratch/contact-delete.mjs action.permission=edit action.action=allow action.pattern=*
    timestamp=2026-10-02T15:34:12.434Z level=INFO run=78944173 message=formatting file=/tmp/autter-wt-sim-dfbc02af27497a88c9b6-3m60eY/tree/.autter/scratch/contact-delete.mjs
    timestamp=2026-10-02T15:34:12.435Z level=INFO run=78944173 message="touching file" file=/tmp/autter-wt-sim-dfbc02af27497a88c9b6-3m60eY/tree/.autter/scratch/contact-delete.mjs
    �[0m% �[0mPatch 1 file
    timestamp=2026-10-02T15:34:12.879Z level=INFO run=78944173 message=tracking hash=7f41b709387106a89ae342c8978d4c0918946aa0 cwd=/tmp/autter-wt-sim-dfbc02af27497a88c9b6-3m60eY/tree git=/tmp/autter-wt-sim-dfbc02af27497a88c9b6-3m60eY/tree/.opencode-xdg/data/opencode/snapshot/a4d634be0c18939cdd1b81424dc6c8c18ad5487d/54564caaf45e37d432994910d9dac631ad534f21
    timestamp=2026-10-02T15:34:12.903Z level=INFO run=78944173 message=loop session.id=ses_f02c3ebc7ffeTVYUa2ig8wCXrJ step=13
    timestamp=2026-10-02T15:34:12.942Z level=INFO run=78944173 message=tracking hash=7f41b709387106a89ae342c8978d4c0918946aa0 cwd=/tmp/autter-wt-sim-dfbc02af27497a88c9b6-3m60eY/tree git=/tmp/autter-wt-sim-dfbc02af27497a88c9b6-3m60eY/tree/.opencode-xdg/data/opencode/snapshot/a4d634be0c18939cdd1b81424dc6c8c18ad5487d/54564caaf45e37d432994910d9dac631ad534f21
    timestamp=2026-10-02T15:34:12.948Z level=INFO run=78944173 message=process session.id=ses_f02c3ebc7ffeTVYUa2ig8wCXrJ messageID=msg_0fd406cab001pfNZsOw8m4p8hM
    timestamp=2026-10-02T15:34:12.950Z level=INFO run=78944173 message=stream providerID=autter-gateway modelID=openai__gpt-6-luna session.id=ses_f02c3ebc7ffeTVYUa2ig8wCXrJ small=false agent=build mode=primary
    timestamp=2026-10-02T15:34:12.962Z level=INFO run=78944173 message="llm runtime selected" llm.runtime=ai-s
  • Return not found without audit for an absent contact — execution incomplete (code_diff, spec)
    Source: app/api/internal/contacts/[id]/route.ts — return NextResponse.json({ error: 'Contact not found' }, { status: 404 })
    Source: PR description — Returns 404 when the contact does not exist.
    Execution could not complete: Error: AUTTER_TEST_DATABASE_URL is not set in this environment.
  • Keep quick delete separate from session guard and typed GDPR deletion — execution incomplete (code_diff, code_diff, spec, spec, spec)
    Source: app/(dashboard)/lists/[id]/page.tsx — const res = await fetch(/api/internal/contacts/${deleteTarget.id}, {
    method: 'DELETE',
    })
    Source: app/(dashboard)/lists/[id]/page.tsx — const url = /api/internal/contacts/${gdprDeleteContact.id}/gdpr-delete?confirm=${encodeURIComponent(gdprDeleteContact.email)}
    Source: docs/gdpr.md — Requires ?confirm=<contact-email> query string. The route 400s if it does not match the contact's stored email.
    Source: PR description — The GDPR delete flow (email typed to confirm) is untouched and still available in the ... menu.
    Source: PR description — Route sits under /api/internal, so the existing session middleware guard applies.
    Indexed blast radius: 0 direct / 0 transitive dependents (indexed 2026-08-31T13:52:28.495Z).
    Execution could not complete: Error: Cannot implement the required mounted-page interaction harness: no browser binary or browser automation/DOM package is installed, and dependencies are read-only. A source-text or copied-logic substitute would violate the harness requirements.
  • Keep dialog and list state on failed quick deletion — execution incomplete (code_diff, code_diff, spec)
    Source: app/(dashboard)/lists/[id]/page.tsx — toast({ title: data.error || 'Failed to delete contact', variant: 'destructive' })
    return
    Source: app/(dashboard)/lists/[id]/page.tsx — if (!open && !deleting) setDeleteTarget(null)
    Source: PR description — The icon is disabled while that row is being deleted and the dialog cannot be dismissed mid-request.
    Indexed blast radius: 0 direct / 0 transitive dependents (indexed 2026-08-31T13:52:28.495Z).
    Execution could not complete: Error: opencode session timed out after 144570ms; stderr: …nst m of ['tsx/cjs/api','tsx/esm/api','react','react/jsx-runtime','react-dom/server','esbuild']) { try { console.log(m, require.resolve(m)) } catch (e) { console.log(m, e.code) } }"
    v24.21.0
    tsx/cjs/api /tmp/autter-agentic-PWyCRY/node_modules/tsx/dist/cjs/api/index.cjs
    tsx/esm/api /tmp/autter-agentic-PWyCRY/node_modules/tsx/dist/esm/api/index.cjs
    react /tmp/autter-agentic-PWyCRY/node_modules/react/index.js
    react/jsx-runtime /tmp/autter-agentic-PWyCRY/node_modules/react/jsx-runtime.js
    react-dom/server /tmp/autter-agentic-PWyCRY/node_modules/react-dom/server.node.js
    esbuild /tmp/autter-agentic-PWyCRY/node_modules/esbuild/lib/main.js
    �[0m
    timestamp=2026-10-02T15:37:59.999Z level=INFO run=fdc2850d message=tracking hash=7f41b709387106a89ae342c8978d4c0918946aa0 cwd=/tmp/autter-wt-sim-05c64d9755fe849a5df6-hHq5es/tree git=/tmp/autter-wt-sim-05c64d9755fe849a5df6-hHq5es/tree/.opencode-xdg/data/opencode/snapshot/a4d634be0c18939cdd1b81424dc6c8c18ad5487d/0864c798425e456b3a00ed53d888275666b93da7
    timestamp=2026-10-02T15:38:00.024Z level=INFO run=fdc2850d message=loop session.id=ses_f02bdd88affeM9fJ1vtSBydaSz step=7
    timestamp=2026-10-02T15:38:00.059Z level=INFO run=fdc2850d message=tracking hash=7f41b709387106a89ae342c8978d4c0918946aa0 cwd=/tmp/autter-wt-sim-05c64d9755fe849a5df6-hHq5es/tree git=/tmp/autter-wt-sim-05c64d9755fe849a5df6-hHq5es/tree/.opencode-xdg/data/opencode/snapshot/a4d634be0c18939cdd1b81424dc6c8c18ad5487d/0864c798425e456b3a00ed53d888275666b93da7
    timestamp=2026-10-02T15:38:00.065Z level=INFO run=fdc2850d message=process session.id=ses_f02bdd88affeM9fJ1vtSBydaSz messageID=msg_0fd43e3db0011HoSc4b8SgwzGr
    timestamp=2026-10-02T15:38:00.066Z level=INFO run=fdc2850d message=stream providerID=autter-gateway modelID=openai__gpt-6-luna session.id=ses_f02bdd88affeM9fJ1vtSBydaSz small=false agent=build mode=primary
    timestamp=2026-10-02T15:38:00.079Z level=INFO run=fdc2850d message="llm runtime selected" llm.runtime=ai-s
    Source unavailable: Production route aggregates are unavailable: Runtime queries are not configured. No retained production error patterns were available for this repository.
    Coverage limitation: No production route/runtime evidence was supplied, so scenarios use only local fixtures and fakes.
    Coverage limitation: No dedicated automated test files or test-runner script were found in this checkout; UI simulations require a local React component-test harness, and endpoint simulations require the stated isolated app/database setup.
    Coverage limitation: The checked-out docs describe GDPR deletion and audit behavior but do not specify quick-delete authorization response formatting; the auth scenario asserts rejection and unchanged state rather than assuming a particular status or response body.

Tests for this change

Autter wrote 1 temporary test for changed files that had none (1 passed, 0 failed). 1 changed source file stayed unchecked.

Changed files with no execution-backed check
  • app/(dashboard)/lists/[id]/page.tsx

Change coverage (is each changed file's behavior verified by a test?)

Changed file Related test Result
app/(dashboard)/lists/[id]/page.tsx — ⬜ not verifiable
app/api/internal/contacts/[id]/route.ts — ✅ no declared test — temporary test passed
🤖 Coverage-check evidence

app/(dashboard)/lists/[id]/page.tsx
Ran: Attempted 'npx tsx .autter/scratch/verify-pr8-page.tsx'; ran 'npm run lint -- --file "app/(dashboard)/lists/[id]/page.tsx" --file "app/api/internal/contacts/[id]/route.ts"'. No declared tests exist.

UI interaction check unavailable: `Cannot find module jsdom`; no browser/DOM harness is installed. Both changed files pass ESLint. Techniques: attempted rendered UI interaction check. Cases selected: open confirmation, verify no deletion before confirmation, confirm deletion request and success feedback. Limitations: no declared tests, package test script, or browser/DOM harness.

app/api/internal/contacts/[id]/route.ts
Ran: 'DATABASE_URL="$AUTTER_TEST_DATABASE_URL" npx tsx .autter/scratch/verify-pr8-route.ts'; 'npm run lint -- --file "app/(dashboard)/lists/[id]/page.tsx" --file "app/api/internal/contacts/[id]/route.ts"'. No declared tests exist.

Disposable-DB temp integration check: `delete_existing=passed status=200 sendCount=1 eventCount=1 audit=true`; `delete_missing=passed status=404`. Lint passed. Database cleanup verified zero lists, contacts, sends, or events remaining. Technique: direct route-handler integration test against disposable Postgres. Cases: contact with one send/event verifies response, cascade deletion, and audit log; nonexistent ID verifies 404/error. Limitations: no declared tests or package test script. An initial scratch scenario had a fixture FK error (contact inserted before parent list); replaced with a correctly isolated test.

Temporary tests are written under .autter/scratch/ for verification only — they are never committed to the repository.

Test plan (from the PR description)

  • ⬜ Open a list with contacts at /lists/[id] and confirm a delete icon appears in each contact row. — needs manual verification
  • ⬜ Click delete on a test contact and confirm the confirmation dialog appears with correct email. — needs manual verification
  • ⬜ Confirm deletion and verify success toast plus contact removed without full page reload. — needs manual verification
  • ⬜ Verify list header counts and pagination totals update after deletion. — needs manual verification
  • ⬜ Test error path by deleting with invalid session or non-existent ID and verify destructive toast. — needs manual verification
  • ⬜ Verify GDPR export and GDPR delete flows still work unchanged on the same page. — needs manual verification
  • ✅ Run npx tsc --noEmit and npm run lint. — verified by agent execution
🤖 Agent-executed checks

✅ Run npx tsc --noEmit and npm run lint.
Ran: Ran 'npx tsc --noEmit' and 'npm run lint'.

TypeScript exited successfully with no output. Lint exited successfully with existing warnings in assets, editor, API keys, team, and asset-picker files; no errors.

⬜ items could not be verified automatically and still need a manual check.

@autter-dev

autter-dev Bot commented Oct 2, 2026

Copy link
Copy Markdown

Autter's deep review traced 1 finding(s) to file(s) this PR does not change — they can't be shown as inline comments, but the change still affects them:

🟠 [deterministic] Product walk mission blocked: Verify that / loads without a demonstrated runtime failure. (risk 55/100)

package.json:1 · product_walk_failure

Video: https://autter-app-storage.s3.ap-south-1.amazonaws.com/pr-review/Autter-dev/hedwig-mail/8/68b2768e65de7ba98a80b2e80a94e62a515ede75/walk/walk.webm

The browser controller stopped with http_5xx_observed. Failed deterministic assertions: no_console_errors, no_http_5xx.

@autter-dev

autter-dev Bot commented Oct 2, 2026

Copy link
Copy Markdown

Autter product walk

Autter opened the app, but the product walk failed before it could confirm this change.

The browser controller stopped with http_5xx_observed. Failed deterministic assertions: no_console_errors, no_http_5xx.

@autter-dev autter-dev Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Autter approved #8 with notes: the merge gate is clear, and 11 non-blocking finding(s) remain as review detail. (Also detected: 9 finding(s) dismissed as likely false positives by verification.)

@autter-dev

autter-dev Bot commented Oct 2, 2026

Copy link
Copy Markdown

Autter task list

No concrete follow-up tasks were generated for this PR.

Generated from PR diff, blast radius, and context.

Issues found

No unresolved code findings remain for this revision — 9 finding(s) dismissed as likely false positives by verification. Dismissed findings stay listed with their verdicts in the Autter review dashboard.

15 additional non-blocking finding(s) are available in the Autter review.

@sagnik11
sagnik11 merged commit cfda14f into main Oct 2, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant