feat: REST API v1 — HMAC-signed product/content creation and file upload
Adds a public/private key pair authentication system and REST API endpoints
for programmatic product and content management. Designed for CI/CD pipelines
(permacomputer.com image hosting).
Auth: HMAC-SHA256 signed requests using public/private key pairs.
The secret key never travels over the wire. Replay window: 300 seconds.
Endpoints:
POST /api/v1/products create product (fiat/crypto priced)
POST /api/v1/content create content (free)
GET /api/v1/products/{id} get product
GET /api/v1/content/{id} get content
POST /api/v1/products/{id}/upload-url presigned S3 POST for direct upload
POST /api/v1/content/{id}/upload-url presigned S3 POST for direct upload
POST /api/v1/products/{id}/files/confirm confirm upload, register metadata
POST /api/v1/content/{id}/files/confirm confirm upload, register metadata
Key management UI in shop settings. Secret shown once on generation.
Migration: mps_api_key table (id, shop_id, public_key, secret_key, label,
created_timestamp, last_used_timestamp, is_active)
Tests: 12 MpsApiKey unit tests, 8 REST API functional tests (269 total passing)
This commit is contained in:
parent
dc06551967
commit
b72fc6bf3b
13 changed files with 1137 additions and 0 deletions
154
docs/tickets/mps-17.md
Normal file
154
docs/tickets/mps-17.md
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
# MPS-17: REST API v1 — HMAC-signed product/content creation + file upload
|
||||
|
||||
## Purpose
|
||||
|
||||
Enable CI/CD pipelines (e.g. permacomputer.com) to programmatically:
|
||||
- Create products (fiat/crypto priced) or content (free)
|
||||
- Upload files directly to Spaces via presigned POST
|
||||
- Confirm uploads and get CDN URLs
|
||||
|
||||
## Auth — HMAC public/private key pairs
|
||||
|
||||
Each shop has API key pairs. A key pair is:
|
||||
- `public_key` — `mps_pub_{32 hex}` — identifies the pair, safe to log
|
||||
- `secret_key` — `mps_sec_{64 hex}` — signs requests, shown **once** on creation
|
||||
|
||||
No bearer tokens. The secret never travels over the wire. Every request is
|
||||
signed with HMAC-SHA256. Replay window: ±300 seconds.
|
||||
|
||||
### Signing scheme
|
||||
|
||||
```
|
||||
string_to_sign = "{METHOD}\n{PATH}\n{TIMESTAMP}\n{SHA256_OF_BODY_HEX}"
|
||||
signature = hmac_sha256(secret_key, string_to_sign).hexdigest()
|
||||
|
||||
Request headers:
|
||||
X-MPS-Key: mps_pub_abc123...
|
||||
X-MPS-Timestamp: 1712345678
|
||||
X-MPS-Signature: sha256=abcdef...
|
||||
```
|
||||
|
||||
### Shell example (for CI)
|
||||
|
||||
```bash
|
||||
METHOD=POST
|
||||
PATH=/api/v1/products
|
||||
TIMESTAMP=$(date +%s)
|
||||
BODY='{"title":"Debian permacomputer","description":"...","price":"0.00"}'
|
||||
BODY_HASH=$(echo -n "$BODY" | sha256sum | awk '{print $1}')
|
||||
STRING_TO_SIGN="${METHOD}\n${PATH}\n${TIMESTAMP}\n${BODY_HASH}"
|
||||
SIG=$(echo -n "$STRING_TO_SIGN" | openssl dgst -sha256 -hmac "$MPS_SECRET_KEY" | awk '{print $2}')
|
||||
|
||||
curl -X POST https://my.makepostsell.com/api/v1/products \
|
||||
-H "X-MPS-Key: $MPS_PUBLIC_KEY" \
|
||||
-H "X-MPS-Timestamp: $TIMESTAMP" \
|
||||
-H "X-MPS-Signature: sha256=$SIG" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$BODY"
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
```
|
||||
POST /api/v1/products create product (is_sellable=True)
|
||||
POST /api/v1/content create content (is_sellable=False)
|
||||
GET /api/v1/products/{id} get product
|
||||
GET /api/v1/content/{id} get content
|
||||
POST /api/v1/products/{id}/upload-url get presigned POST URL for file upload
|
||||
POST /api/v1/content/{id}/upload-url get presigned POST URL for file upload
|
||||
POST /api/v1/products/{id}/files/confirm confirm S3 upload, register file, get CDN URL
|
||||
POST /api/v1/content/{id}/files/confirm confirm S3 upload, register file, get CDN URL
|
||||
```
|
||||
|
||||
### POST /api/v1/products
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"title": "Debian permacomputer 6.12 amd64",
|
||||
"description": "Debian bookworm with CWE-407 patched Linux 6.12 kernel",
|
||||
"price": "9.99",
|
||||
"visibility": "public"
|
||||
}
|
||||
```
|
||||
|
||||
Response 201:
|
||||
```json
|
||||
{
|
||||
"id": "abc123...",
|
||||
"url": "https://my.makepostsell.com/p/abc123/debian-permacomputer",
|
||||
"edit_url": "https://my.makepostsell.com/p/abc123/edit"
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/v1/content
|
||||
|
||||
Same body minus `price`. Response 201 same shape with `/c/` URL.
|
||||
|
||||
### POST /api/v1/products/{id}/upload-url
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"filename": "debian-permacomputer-6.12-amd64.qcow2",
|
||||
"content_type": "application/octet-stream"
|
||||
}
|
||||
```
|
||||
|
||||
Response 200:
|
||||
```json
|
||||
{
|
||||
"upload_url": "https://nyc3.digitaloceanspaces.com/...",
|
||||
"fields": { "key": "...", "AWSAccessKeyId": "...", ... },
|
||||
"confirm_path": "/api/v1/products/{id}/files/confirm",
|
||||
"key": "products/shop_id/product_id/product.qcow2"
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/v1/products/{id}/files/confirm
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"key": "products/shop_id/product_id/product.qcow2",
|
||||
"filename": "debian-permacomputer-6.12-amd64.qcow2"
|
||||
}
|
||||
```
|
||||
|
||||
Response 200:
|
||||
```json
|
||||
{
|
||||
"cdn_url": "https://plan-period-files.nyc3.cdn.digitaloceanspaces.com/..."
|
||||
}
|
||||
```
|
||||
|
||||
## Shop Settings UI
|
||||
|
||||
New section at bottom of `/s/{shop_id}/settings`:
|
||||
- List active key pairs (label, public key, created date, last used)
|
||||
- "Generate new key pair" form (label input)
|
||||
- Secret shown **once** in a flash-style `<output>` element after generation
|
||||
- Per-key revoke button
|
||||
|
||||
Routes:
|
||||
```
|
||||
POST /s/{shop_id}/api-keys/generate
|
||||
POST /s/{shop_id}/api-keys/{key_id}/revoke
|
||||
```
|
||||
|
||||
## Files
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `models/api_key.py` | New: `MpsApiKey` model |
|
||||
| `models/__init__.py` | Import `MpsApiKey` |
|
||||
| `models/meta.py` | Add `MpsApiKey` to `CLASS_TO_TABLE` |
|
||||
| `views/api/__init__.py` | HMAC auth: `api_key_required` decorator |
|
||||
| `views/api/items.py` | All API endpoints |
|
||||
| `routes.py` | Add API + key management routes |
|
||||
| `views/shop.py` | `api_keys_generate`, `api_key_revoke` handlers |
|
||||
| `templates/shop_settings.j2` | API Keys section |
|
||||
| `alembic/versions/` | Migration: `mps_api_key` table |
|
||||
| `tests/test_models.py` | `MpsApiKey` unit tests |
|
||||
| `tests/test_integration.py` | HMAC signing integration tests |
|
||||
| `tests/test_functional.py` | API endpoint functional tests |
|
||||
Loading…
Add table
Add a link
Reference in a new issue