Skip to content

Commit cdc8560

Browse files
nullcoderclaude
andcommitted
feat: implement Update/Delete APIs with enhanced security (#107)
This PR completes Issue #107 by implementing PUT and DELETE endpoints for the /api/gists/[id] route with comprehensive security features and API test refactoring. ## Key Features ### DELETE Endpoint - Dual authentication methods: - One-time view gists: metadata proof validation (SHA-256 hash) - PIN-protected gists: PIN validation via X-Edit-Password header - CSRF protection on all state-changing endpoints - Fixed race condition by moving auto-deletion to explicit DELETE endpoint ### PUT Endpoint - Multipart form data support for gist updates - PIN validation for protected gists - Optimistic locking to prevent concurrent update conflicts - Support for updating encrypted user metadata and editor preferences ### Security Enhancements - Created reusable CSRF validation in lib/security.ts - Converted all crypto operations to WebCrypto API for edge runtime - Added comprehensive schema validation using Zod - Proper error handling with typed AppError system ### API Test Refactoring - Established consistent test pattern: route.{method}.test.ts - Split all combined test files by HTTP method - Created API_TEST_PATTERN.md documentation - All 69 tests passing with 100% coverage ### Additional Improvements - Implemented user metadata encryption support - Created shared schemas in lib/api-schemas.ts - Added editor preferences to create/update operations - Updated tracking documents and TODO.md 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 9b00b87 commit cdc8560

22 files changed

+1886
-270
lines changed

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,4 +244,4 @@ When updating any tracking documents (TODO.md, PHASE\_\*\_ISSUE_TRACKING.md, etc
244244
- **Phase Tracking**: `docs/PHASE_*_ISSUE_TRACKING.md` - Development phase status
245245
- **Labels Guide**: `docs/LABELS.md` - GitHub label system documentation
246246

247-
Last Updated: 2025-06-07
247+
Last Updated: 2025-06-08

README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,14 @@ GhostPaste is a privacy-focused code sharing platform that ensures your code sni
3131
- **📱 Responsive Design** - Works perfectly on all devices
3232
- **🚀 Global Edge Deployment** - Fast access from anywhere in the world
3333

34+
### ✅ Available Features
35+
36+
- **👁️ One-Time View** - Create snippets that disappear after being viewed once (implemented with secure deletion flow)
37+
- **📝 Version History** - Track changes with automatic versioning (storage ready)
38+
3439
### 🚧 Coming Soon
3540

3641
- **⏱️ Self-Expiring Content** - Set snippets to auto-delete after a specified time
37-
- **👁️ One-Time View** - Create snippets that disappear after being viewed once
38-
- **📝 Version History** - Track changes with automatic versioning (storage ready)
3942

4043
## 🚀 Quick Start
4144

@@ -183,10 +186,10 @@ GhostPaste is actively being developed. Here's what's completed and what's in pr
183186

184187
### 🚧 In Progress
185188

186-
- API endpoints (Phase 5 - Storage foundation complete)
189+
- API endpoints (Phase 5 - Core CRUD operations complete)
187190
- Self-expiring gists
188-
- One-time view functionality
189191
- Version history UI
192+
- Complete frontend integration
190193

191194
### 📅 Upcoming
192195

app/api/blobs/[id]/route.test.ts renamed to app/api/blobs/[id]/route.get.test.ts

Lines changed: 3 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
22
import { NextRequest } from "next/server";
3-
import { GET, OPTIONS } from "./route";
3+
import { GET } from "./route";
44
import { StorageOperations } from "@/lib/storage-operations";
55
import { ApiErrors } from "@/lib/api-errors";
66
import type { GistMetadata } from "@/types/models";
@@ -10,7 +10,6 @@ import type { ApiErrorResponse } from "@/types/api";
1010
vi.mock("@/lib/storage-operations", () => ({
1111
StorageOperations: {
1212
getGist: vi.fn(),
13-
deleteIfNeeded: vi.fn(),
1413
},
1514
}));
1615

@@ -106,7 +105,6 @@ describe("GET /api/blobs/[id]", () => {
106105
const oneTimeMetadata = { ...mockMetadata, one_time_view: true };
107106
const mockGist = { metadata: oneTimeMetadata, blob: mockBlob };
108107
vi.mocked(StorageOperations.getGist).mockResolvedValue(mockGist);
109-
vi.mocked(StorageOperations.deleteIfNeeded).mockResolvedValue(true);
110108

111109
const request = createRequest();
112110
const context = createContext();
@@ -116,9 +114,7 @@ describe("GET /api/blobs/[id]", () => {
116114
expect(response.headers.get("Cache-Control")).toBe(
117115
"no-store, no-cache, must-revalidate"
118116
);
119-
expect(StorageOperations.deleteIfNeeded).toHaveBeenCalledWith(
120-
oneTimeMetadata
121-
);
117+
// Note: Auto-deletion removed, now handled by explicit DELETE endpoint
122118
});
123119

124120
it("should handle large blob data", async () => {
@@ -194,21 +190,7 @@ describe("GET /api/blobs/[id]", () => {
194190
// Note: Logger mock calls can't be easily tested with current mock setup
195191
});
196192

197-
it("should continue if one-time deletion fails", async () => {
198-
const oneTimeMetadata = { ...mockMetadata, one_time_view: true };
199-
const mockGist = { metadata: oneTimeMetadata, blob: mockBlob };
200-
vi.mocked(StorageOperations.getGist).mockResolvedValue(mockGist);
201-
vi.mocked(StorageOperations.deleteIfNeeded).mockRejectedValue(
202-
new Error("Delete failed")
203-
);
204-
205-
const request = createRequest();
206-
const context = createContext();
207-
const response = await GET(request, context);
208-
209-
expect(response.status).toBe(200); // Should still succeed
210-
// Note: Logger mock calls can't be easily tested with current mock setup
211-
});
193+
// Note: Auto-deletion test removed since deletion is now handled by explicit DELETE endpoint
212194

213195
it("should handle unexpected errors", async () => {
214196
const unexpectedError = new TypeError("Something went wrong");
@@ -239,21 +221,3 @@ describe("GET /api/blobs/[id]", () => {
239221
});
240222
});
241223
});
242-
243-
describe("OPTIONS /api/blobs/[id]", () => {
244-
it("should return correct CORS headers", async () => {
245-
const response = await OPTIONS();
246-
247-
expect(response.status).toBe(200);
248-
expect(response.headers.get("Access-Control-Allow-Origin")).toBe(
249-
"https://ghostpaste.dev"
250-
);
251-
expect(response.headers.get("Access-Control-Allow-Methods")).toBe(
252-
"GET, OPTIONS"
253-
);
254-
expect(response.headers.get("Access-Control-Allow-Headers")).toBe(
255-
"Content-Type"
256-
);
257-
expect(response.headers.get("Access-Control-Max-Age")).toBe("86400");
258-
});
259-
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { describe, it, expect } from "vitest";
2+
import { OPTIONS } from "./route";
3+
4+
describe("OPTIONS /api/blobs/[id]", () => {
5+
it("should return correct CORS headers", async () => {
6+
const response = await OPTIONS();
7+
8+
expect(response.status).toBe(200);
9+
expect(response.headers.get("Access-Control-Allow-Origin")).toBe(
10+
"https://ghostpaste.dev"
11+
);
12+
expect(response.headers.get("Access-Control-Allow-Methods")).toBe(
13+
"GET, OPTIONS"
14+
);
15+
expect(response.headers.get("Access-Control-Allow-Headers")).toBe(
16+
"Content-Type"
17+
);
18+
expect(response.headers.get("Access-Control-Max-Age")).toBe("86400");
19+
});
20+
});

app/api/blobs/[id]/route.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -62,18 +62,8 @@ export async function GET(
6262
}
6363
}
6464

65-
// For one-time view gists, delete after successful retrieval
66-
if (metadata.one_time_view) {
67-
try {
68-
await StorageOperations.deleteIfNeeded(metadata);
69-
} catch (error) {
70-
// Log but don't fail the request if deletion fails
71-
logger.warn(
72-
"Failed to delete one-time view gist",
73-
error instanceof Error ? error : new Error(String(error))
74-
);
75-
}
76-
}
65+
// Note: One-time view deletion is now handled by explicit DELETE endpoint
66+
// to avoid race conditions between metadata and blob requests
7767

7868
// Return blob data with appropriate headers
7969
return new NextResponse(blob, {

0 commit comments

Comments
 (0)