Skip to content

Commit 1390a04

Browse files
committed
feat: add links api
1 parent 24d2cdc commit 1390a04

File tree

14 files changed

+1500
-8
lines changed

14 files changed

+1500
-8
lines changed

README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ There may be a delay between the purchase and the refund. Additionally, the refu
9999
- View non-refunded feedbacks (read:api)
100100
- View refunded feedbacks (read:api)
101101
- Search purchases (search:api)
102+
- Generate short public links for dispute resolution (write:api)
103+
- Access dispute resolution information via public links (no permission required)
102104

103105
## Security
104106

@@ -292,6 +294,63 @@ The REST API exchanges all objects in JSON format. The API provides the followin
292294
- **GET `/api/stats/purchases`** - Get purchase statistics overview - requires read:api permission
293295
- Response: `{success: boolean, data: {totalPurchases: number, totalRefundedPurchases: number, totalRefundedAmount: number}}`
294296

297+
### Public Short Links for Dispute Resolution
298+
299+
These endpoints enable testers to generate secure short links for sharing dispute resolution information publicly. This is useful for cases where proof of purchase, feedback, and publication needs to be shared with a third party or dispute resolution authority.
300+
301+
#### Link Generation
302+
303+
- **POST `/api/link/public`** - Generate a short public link for dispute resolution - requires write:api permission
304+
- Query parameters:
305+
- `duration` (required): Duration in seconds for which the link should be valid (minimum 60, maximum 31536000 / 1 year)
306+
- `purchase` (required): The UUID of the purchase to create a link for
307+
- The purchase must have both feedback and publication before a link can be created
308+
- Link codes are 7 characters long, containing digits (0-9) and letters (a-z, A-Z)
309+
- Each generated code is unique and automatically checked for collisions
310+
- Request: `POST /api/link/public?duration=3600&purchase=550e8400-e29b-41d4-a716-446655440000`
311+
- Response: `{success: boolean, code: string, url: string}`
312+
- Example Response: `{success: true, code: "aBc1234", url: "/link/aBc1234"}`
313+
314+
#### Link Data Retrieval
315+
316+
- **GET `/api/link/:code`** - Access public dispute resolution information via short link - no permission required (public endpoint)
317+
- Path parameter: `code` - The 7-character unique link code
318+
- This endpoint returns the complete dispute resolution information if the link is valid (not expired and properly formatted)
319+
- Returns the following data for eligible purchases:
320+
- Order details: order number, date, amount, purchase screenshot
321+
- Feedback details: submission date, feedback text
322+
- Publication details: publication date, publication screenshot
323+
- Refund details (if applicable): refund amount, transaction ID
324+
- Response: `{success: boolean, data: {orderNumber: string, orderDate: string, purchaseAmount: number, purchaseScreenshot: string, feedbackDate: string, feedbackText: string, publicationDate: string, publicationScreenshot: string, isRefunded: boolean, refundAmount?: number, refundTransactionId?: string}}`
325+
- Error responses:
326+
- `400`: Invalid link code format (must be exactly 7 alphanumeric characters)
327+
- `404`: Link not found or has expired
328+
329+
#### Usage Example
330+
331+
1. Generate a short link for dispute resolution:
332+
```bash
333+
POST /api/link/public?duration=2592000&purchase=550e8400-e29b-41d4-a716-446655440000
334+
# Response: {"success": true, "code": "xY7zQw", "url": "/link/xY7zQw"}
335+
```
336+
337+
2. Share the link publicly (e.g., `https://example.com/link/xY7zQw`) or with dispute resolution services
338+
339+
3. Access the dispute information using the link code:
340+
```bash
341+
GET /link/xY7zQw
342+
# Returns complete dispute resolution data including purchase proof, feedback, and publication proof
343+
```
344+
345+
#### Technical Details
346+
347+
- Link expiration is checked at retrieval time, not during link generation
348+
- The link will only return data if the purchase has both feedback and a publication entry
349+
- No authentication is required to retrieve data via a valid link code
350+
- Link codes use 62 possible characters per position (10 digits + 26 lowercase + 26 uppercase), providing 62^7 ≈ 3.5 trillion possible combinations
351+
- The database includes an index on the `(code, expires_at)` composite key for efficient expiration checks
352+
- Link cleanup can be performed using a background job that calls the repository's `cleanupExpired()` method
353+
295354
### Database Management
296355

297356
- **GET `/api/backup/json`** - Backup the database to JSON format - requires backup:api permission

client/get-openapi.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ const options = {
88
format: "json",
99
info: {
1010
title: "Feedback Flow API",
11-
version: "1.4.0",
11+
version: "1.5.0",
1212
},
1313
definition: {
1414
openapi: "3.0.0",
1515
info: {
1616
title: "Feedback Flow API",
17-
version: "1.4.0",
17+
version: "1.5.0",
1818
},
1919
}, // You can move properties from definition here if needed
2020
apis: [
@@ -30,6 +30,7 @@ const options = {
3030
"../cloudflare-worker/src/routes/refunds/index.ts",
3131
"../cloudflare-worker/src/routes/stats/index.ts",
3232
"../cloudflare-worker/src/routes/system/index.ts",
33+
"../cloudflare-worker/src/routes/links/index.ts",
3334
], // Path to the API docs
3435
};
3536

client/public/openapi.json

Lines changed: 180 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"openapi": "3.0.0",
33
"info": {
44
"title": "Feedback Flow API",
5-
"version": "1.4.0"
5+
"version": "1.5.0"
66
},
77
"paths": {
88
"/api/testers": {
@@ -1830,6 +1830,185 @@
18301830
}
18311831
}
18321832
}
1833+
},
1834+
"/api/link/public": {
1835+
"post": {
1836+
"summary": "Generate a short public link for dispute resolution",
1837+
"description": "Creates a short public link that allows sharing dispute information.\nThe link is only valid if the purchase has:\n- A feedback entry (feedback created)\n- A publication entry (feedback published)\n\nThe link will expire after the specified duration (in seconds).\nRequires write:api permission.\n",
1838+
"tags": [
1839+
"Links"
1840+
],
1841+
"parameters": [
1842+
{
1843+
"name": "duration",
1844+
"in": "query",
1845+
"required": true,
1846+
"description": "Duration in seconds for which the link should be valid",
1847+
"schema": {
1848+
"type": "integer",
1849+
"minimum": 60,
1850+
"maximum": 31536000,
1851+
"example": 3600
1852+
}
1853+
},
1854+
{
1855+
"name": "purchase",
1856+
"in": "query",
1857+
"required": true,
1858+
"description": "The UUID of the purchase to create a link for",
1859+
"schema": {
1860+
"type": "string",
1861+
"format": "uuid"
1862+
}
1863+
}
1864+
],
1865+
"responses": {
1866+
"200": {
1867+
"description": "Short link successfully generated",
1868+
"content": {
1869+
"application/json": {
1870+
"schema": {
1871+
"type": "object",
1872+
"properties": {
1873+
"success": {
1874+
"type": "boolean",
1875+
"example": true
1876+
},
1877+
"code": {
1878+
"type": "string",
1879+
"description": "7-character unique code (0-9, a-z, A-Z)",
1880+
"example": "aBc1234"
1881+
},
1882+
"url": {
1883+
"type": "string",
1884+
"description": "Full URL for accessing the dispute resolution page",
1885+
"example": "/link/aBc1234"
1886+
}
1887+
}
1888+
}
1889+
}
1890+
}
1891+
},
1892+
"400": {
1893+
"description": "Invalid request or purchase not eligible"
1894+
},
1895+
"401": {
1896+
"description": "Unauthorized - authentication required"
1897+
},
1898+
"404": {
1899+
"description": "Purchase not found or not owned by user"
1900+
}
1901+
}
1902+
}
1903+
},
1904+
"/api/link/{code}": {
1905+
"get": {
1906+
"summary": "Access public dispute resolution information",
1907+
"description": "Retrieves complete dispute resolution information for a valid short link.\nThis endpoint requires no authentication and provides:\n- Purchase details (order number, date, amount, screenshot)\n- Feedback details (creation date, content)\n- Publication details (publication date, screenshot)\n- Refund details if the purchase was refunded (amount, transaction ID)\n\nThe link must be valid (code format correct and not expired).\n\nNo permission required - public endpoint accessible with valid link code.\n",
1908+
"tags": [
1909+
"Links"
1910+
],
1911+
"parameters": [
1912+
{
1913+
"name": "code",
1914+
"in": "path",
1915+
"required": true,
1916+
"description": "The 7-character unique link code",
1917+
"schema": {
1918+
"type": "string",
1919+
"pattern": "^[0-9a-zA-Z]{7}$",
1920+
"example": "aBc1234"
1921+
}
1922+
}
1923+
],
1924+
"responses": {
1925+
"200": {
1926+
"description": "Dispute resolution information successfully retrieved",
1927+
"content": {
1928+
"application/json": {
1929+
"schema": {
1930+
"type": "object",
1931+
"properties": {
1932+
"success": {
1933+
"type": "boolean",
1934+
"example": true
1935+
},
1936+
"data": {
1937+
"type": "object",
1938+
"properties": {
1939+
"orderNumber": {
1940+
"type": "string",
1941+
"description": "The order number for the purchase",
1942+
"example": "102-1234567-1234567"
1943+
},
1944+
"orderDate": {
1945+
"type": "string",
1946+
"format": "date",
1947+
"description": "Date of the purchase",
1948+
"example": "2024-03-15"
1949+
},
1950+
"purchaseAmount": {
1951+
"type": "number",
1952+
"description": "Amount paid for the purchase",
1953+
"example": 99.99
1954+
},
1955+
"purchaseScreenshot": {
1956+
"type": "string",
1957+
"description": "Base64-encoded screenshot of the purchase"
1958+
},
1959+
"secondaryScreenshot": {
1960+
"type": "string",
1961+
"description": "Optional secondary screenshot (e.g., from publication)"
1962+
},
1963+
"feedbackDate": {
1964+
"type": "string",
1965+
"format": "date",
1966+
"description": "Date when feedback was submitted",
1967+
"example": "2024-03-16"
1968+
},
1969+
"feedbackText": {
1970+
"type": "string",
1971+
"description": "The feedback text provided by the tester"
1972+
},
1973+
"publicationDate": {
1974+
"type": "string",
1975+
"format": "date",
1976+
"description": "Date when the feedback was published",
1977+
"example": "2024-03-17"
1978+
},
1979+
"publicationScreenshot": {
1980+
"type": "string",
1981+
"description": "Base64-encoded screenshot of the publication"
1982+
},
1983+
"isRefunded": {
1984+
"type": "boolean",
1985+
"description": "Whether the purchase has been refunded",
1986+
"example": false
1987+
},
1988+
"refundAmount": {
1989+
"type": "number",
1990+
"description": "Amount refunded (only if isRefunded is true)",
1991+
"example": 99.99
1992+
},
1993+
"refundTransactionId": {
1994+
"type": "string",
1995+
"description": "Transaction ID of the refund (if available)"
1996+
}
1997+
}
1998+
}
1999+
}
2000+
}
2001+
}
2002+
}
2003+
},
2004+
"400": {
2005+
"description": "Invalid link code format"
2006+
},
2007+
"404": {
2008+
"description": "Link not found or expired"
2009+
}
2010+
}
2011+
}
18332012
}
18342013
},
18352014
"components": {

cloudflare-worker/package.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@
1313
"start": "wrangler dev",
1414
"test": "set -a && source ../.env && set +a && node --experimental-vm-modules ./node_modules/.bin/jest",
1515
"d1:create": "npx wrangler d1 execute --local --file src/db/create.sql feedbackflow-db",
16+
"d1:migrate:v0-v1": "npx wrangler d1 execute --local --file src/db/migrations/v1_add_transaction_id.sql feedbackflow-db",
17+
"d1:migrate:v1-v2": "npx wrangler d1 execute --local --file src/db/migrations/v2_add_screenshot_summary.sql feedbackflow-db",
18+
"d1:migrate:v2-v3": "npx wrangler d1 execute --local --file src/db/migrations/v3_add_links_table.sql feedbackflow-db",
19+
"d1:migrate:all": "npm run d1:migrate:v0-v1 && npm run d1:migrate:v1-v2 && npm run d1:migrate:v2-v3",
20+
"d1:migrate:v0-v1:remote": "npx wrangler d1 execute --remote --file src/db/migrations/v1_add_transaction_id.sql feedbackflow-db",
21+
"d1:migrate:v1-v2:remote": "npx wrangler d1 execute --remote --file src/db/migrations/v2_add_screenshot_summary.sql feedbackflow-db",
22+
"d1:migrate:v2-v3:remote": "npx wrangler d1 execute --remote --file src/db/migrations/v3_add_links_table.sql feedbackflow-db",
23+
"d1:migrate:all:remote": "npm run d1:migrate:v0-v1:remote && npm run d1:migrate:v1-v2:remote && npm run d1:migrate:v2-v3:remote",
1624
"d1:create:remote": "npx wrangler d1 execute --remote --file src/db/create.sql feedbackflow-db",
1725
"d1:init": "npx wrangler d1 execute --local --file src/test/seed-test-data.sql feedbackflow-db",
1826
"cf-typegen": "wrangler types"
@@ -45,4 +53,4 @@
4553
"jose": "^6.1.0",
4654
"uuid": "^13.0.0"
4755
}
48-
}
56+
}

cloudflare-worker/src/db/create.sql

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ PRAGMA foreign_keys = ON;
2727

2828
-- Drop tables if they already exist (for reset)
2929
DROP TABLE IF EXISTS schema_version;
30+
DROP TABLE IF EXISTS links;
3031
DROP TABLE IF EXISTS publications;
3132
DROP TABLE IF EXISTS feedbacks;
3233
DROP TABLE IF EXISTS purchases;
@@ -109,8 +110,30 @@ CREATE TABLE refunds (
109110
FOREIGN KEY (purchase_id) REFERENCES purchases(id) ON DELETE CASCADE
110111
);
111112

112-
-- Index for searching purchases by tester
113-
CREATE INDEX idx_purchases_tester_uuid ON purchases(tester_uuid);
113+
-- Links table for short public dispute resolution links
114+
CREATE TABLE links (
115+
id INTEGER PRIMARY KEY AUTOINCREMENT,
116+
code TEXT NOT NULL UNIQUE, -- 7-character unique code for the short link
117+
purchase_id TEXT NOT NULL, -- Reference to the purchase
118+
expires_at TIMESTAMP NOT NULL, -- Expiration timestamp
119+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
120+
FOREIGN KEY (purchase_id) REFERENCES purchases(id) ON DELETE CASCADE
121+
);
122+
123+
-- Index to speed up refunds lookups
124+
CREATE INDEX idx_refunds_purchase_id ON refunds(purchase_id);
125+
126+
-- Index to speed up links lookups by code
127+
CREATE INDEX idx_links_code ON links(code);
128+
129+
-- Index to speed up links lookups by purchase_id
130+
CREATE INDEX idx_links_purchase_id ON links(purchase_id);
131+
132+
-- Index to efficiently find non-expired links
133+
CREATE INDEX idx_links_expires_at ON links(expires_at);
134+
135+
-- Composite index for checking if a link exists and is still valid
136+
CREATE INDEX idx_links_code_expires_at ON links(code, expires_at);
114137
-- Index for searching refunded/non-refunded purchases
115138
CREATE INDEX idx_purchases_refunded ON purchases(refunded);
116139

@@ -194,4 +217,4 @@ GROUP BY t.uuid;
194217

195218
-- Insert initial schema version (or update if exists)
196219
INSERT INTO schema_version (id, version, description)
197-
VALUES (1, 2, 'Initial schema with transactionId support for refunds and screenshot summary');
220+
VALUES (1, 3, 'Schema with transactionId support for refunds, screenshot summary, and short public links');

0 commit comments

Comments
 (0)