diff --git a/README.md b/README.md index abd3909..5d07040 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Run tests via Docker (avoids local permission issues, e.g. `var/`): docker compose --profile test run --rm test ``` -Primary deployed base URL: `https://momswap.produktor.duckdns.org/`. +Primary deployed base URL: `https://tenerife.baby/`. Local default (for development): `http://localhost:8122`. @@ -71,7 +71,7 @@ COMPOSE_BAKE=true docker compose --profile dev up --watch Notes: -- `api` service listens on `8122` inside the container, mapped to host `8122` (reverse proxy at `https://momswap.produktor.duckdns.org`). +- `api` service listens on `8122` inside the container, mapped to host `8122` (reverse proxy at `https://tenerife.baby`). - `api` service uses the production `runtime` image target. - `api-dev` profile uses the `dev` image target and Docker Compose watch. - DB defaults can be overridden via `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD`. @@ -89,7 +89,7 @@ go run ./cmd/api Then visit: -- Production: `https://momswap.produktor.duckdns.org/web/` +- Production: `https://tenerife.baby/web/` - Local: `http://localhost:8122/web/` - Local Leaflet demo: `http://localhost:8122/web/leaflet-demo.html` diff --git a/docs/ed25519-security-use-cases.md b/docs/ed25519-security-use-cases.md index 606cb37..291c4d5 100644 --- a/docs/ed25519-security-use-cases.md +++ b/docs/ed25519-security-use-cases.md @@ -51,13 +51,13 @@ sequenceDiagram ```bash # Step 1: Get challenge -CHALLENGE=$(curl -s -X POST https://momswap.produktor.duckdns.org/v1/auth/challenge \ +CHALLENGE=$(curl -s -X POST https://tenerife.baby/v1/auth/challenge \ -H "Content-Type: application/json" \ -d '{"publicKey":"txdkGKNdcZIEoQMJ0dqum3msjT6-2mO4yLVhtidRFJI"}') NONCE=$(echo "$CHALLENGE" | jq -r '.nonce') # Step 2: Sign "login:$NONCE" with your private key, then: -curl -s -X POST https://momswap.produktor.duckdns.org/v1/auth/login \ +curl -s -X POST https://tenerife.baby/v1/auth/login \ -H "Content-Type: application/json" \ -d "{\"publicKey\":\"txdkGKNdcZIEoQMJ0dqum3msjT6-2mO4yLVhtidRFJI\",\"nonce\":\"$NONCE\",\"signature\":\"\"}" ``` @@ -91,10 +91,10 @@ Registers a new user without an invitation. User proves key ownership by signing ```bash # Step 1: Fetch service key -SERVICE_KEY=$(curl -s https://momswap.produktor.duckdns.org/v1/service-key | jq -r '.publicKey') +SERVICE_KEY=$(curl -s https://tenerife.baby/v1/service-key | jq -r '.publicKey') # Step 2: Sign $SERVICE_KEY with your private key, then: -curl -s -X POST https://momswap.produktor.duckdns.org/v1/auth/register-by-signature \ +curl -s -X POST https://tenerife.baby/v1/auth/register-by-signature \ -H "Content-Type: application/json" \ -d "{\"publicKey\":\"\",\"signature\":\"\"}" ``` diff --git a/docs/typescript-frontend-integration.md b/docs/typescript-frontend-integration.md index f1abd96..d46f397 100644 --- a/docs/typescript-frontend-integration.md +++ b/docs/typescript-frontend-integration.md @@ -8,9 +8,9 @@ This document explains how frontend developers should integrate with the backend Primary backend URL for integration: -- `https://momswap.produktor.duckdns.org/` +- `https://tenerife.baby/` -Deployment: API is proxied via reverse proxy from `https://momswap.produktor.duckdns.org` to backend at `172.17.0.1:8122`. Docker Compose maps port 8122 for the reverse proxy. +Deployment: API is proxied via reverse proxy from `https://tenerife.baby` to backend at `172.17.0.1:8122`. Docker Compose maps port 8122 for the reverse proxy. ## Goals @@ -33,60 +33,60 @@ bun test # unit + integration tests (docs flow) bun run build ``` -Integration tests in `test/integration.test.ts` cover the recommended flow: register, login, create collection, create point feature, list features. +Integration tests in `test/integration.test.ts` cover both: + +- Base flow: register, login, collection and feature CRUD +- Asset flow: create/link asset, request signed upload URL, and toggle visibility ## Public API (current) ### Class: `GeoApiClient` -Source: [GeoApiClient.ts](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts) +Source: `libs/geo-api-client/src/GeoApiClient.ts` Constructor: -- [`new GeoApiClient(baseUrl, storage, storageKey?)`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L14) +- `new GeoApiClient(baseUrl, storage, storageKey?)` Key methods: -**Key storage** +- **Key storage** +- `ensureKeysInStorage()` +- `getStoredKeys()` +- `derivePublicKey(privateKey)` +- `importKeys(keys)` +- `exportKeys()` +- `setAccessToken(token)` -- [`ensureKeysInStorage()`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L20) — Ensure a keypair exists; if none found, generate and save. Use on app init. -- [`getStoredKeys()`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L29) — Get stored keypair without generating. Returns null if none. -- [`derivePublicKey(privateKey)`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L33) — Derive public key from private key (Ed25519). Use when importing pk from backup/QR. -- [`importKeys(keys)`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L37) — Overwrite stored keypair (e.g. after import or restore from QR). -- [`exportKeys()`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L41) — Read stored keypair for export/backup. -- [`setAccessToken(token)`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L45) — Set bearer token for authenticated requests. Call after login. +- **Auth** +- `getServicePublicKey()` +- `createChallenge(publicKey)` +- `loginWithSignature(publicKey, privateKey)` +- `registerBySigningServiceKey(publicKey, privateKey)` +- `createInvitation(payload, inviterPrivateKey)` +- `registerWithInvitation(...)` -**Auth** +- **Collections and features** +- `listCollections()` +- `createCollection(name)` +- `updateCollection(collectionId, name)` +- `deleteCollection(collectionId)` +- `listFeatures(collectionId)` (typed with `properties.assets`) +- `createPointFeature(collectionId, lon, lat, properties)` +- `deleteFeature(featureId)` -- [`getServicePublicKey()`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L69) — Fetch API service public key (for register-by-signature). -- [`createChallenge(publicKey)`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L73) — Request login challenge; returns nonce and messageToSign. -- [`loginWithSignature(publicKey, privateKey)`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L86) — Login via challenge-response. Returns bearer token; stores it internally. -- [`registerBySigningServiceKey(publicKey, privateKey)`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L76) — Register without invitation by signing the API service key. 409 if already registered. -- [`createInvitation(payload, inviterPrivateKey)`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L101) — Create invitation for new users. Inviter signs the payload. -- [`registerWithInvitation(...)`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L114) — Register using an invitation. Proves key ownership and redeems invite. +- **Assets (new)** +- `createOrLinkAsset({...})` — create metadata or reuse existing asset by checksum/ext and link it to a feature +- `getAssetSignedUploadUrl(assetId, contentType?)` — get signed `PUT` URL for binary upload +- `setAssetVisibility(assetId, isPublic)` — owner toggles public/private access +- `resolveRelativeLink(path)` — converts backend-relative asset links to absolute URLs for browser usage -**Collections** +## Asset frontend contract -- [`listCollections()`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L133) — List collections for the authenticated user. -- [`createCollection(name)`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L137) — Create a new collection. Returns id and name. -- [`updateCollection(collectionId, name)`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L140) — Rename a collection. -- [`deleteCollection(collectionId)`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L148) — Delete a collection and its features. - -**Features** - -- [`listFeatures(collectionId)`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L153) — List GeoJSON features in a collection. -- [`createPointFeature(collectionId, lon, lat, properties)`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L156) — Add a Point. lon ∈ [-180,180], lat ∈ [-90,90]. Returns feature id. -- [`deleteFeature(featureId)`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L172) — Delete a feature. - -## Asset API integration note - -Asset endpoints are currently available at backend API level (`/v1/assets...`) and can be called from frontend apps directly with authenticated `fetch` requests. - -Current frontend contract points: - -- Feature list responses include linked media under `feature.properties.assets`. -- Each asset includes a backend-relative download path (`link`) like `/v1/assets/{id}/download`. -- Frontend should use this relative path and avoid constructing direct S3 URLs. +- Feature responses include linked assets in `feature.properties.assets`. +- Asset item fields include: `id`, `kind`, `name`, `description`, `checksum`, `ext`, `isPublic`, `link`. +- `link` is always backend-relative (for example `/v1/assets/{id}/download`). +- Frontend should call `resolveRelativeLink(link)` and must not build direct S3 URLs. ## Recommended integration flow @@ -94,8 +94,15 @@ Current frontend contract points: 2. Call `ensureKeysInStorage()` when app initializes. 3. If not yet registered: call `registerBySigningServiceKey(publicKey, privateKey)` (signs the API service key and publishes your public key). 4. Use `loginWithSignature()` to obtain and set a bearer token. -5. Call collection/feature methods after authentication. -6. Use `importKeys`/`exportKeys` in profile settings UX. +5. Create collection and map features. +6. For media upload: + - compute file checksum + - call `createOrLinkAsset` + - call `getAssetSignedUploadUrl` + - upload file to signed URL +7. Render and share assets from `properties.assets` links. +8. Use `setAssetVisibility` to toggle sharing. +9. Use `importKeys`/`exportKeys` in profile settings UX. ## Registration by signing service key @@ -119,7 +126,7 @@ const storageLike = { removeItem: (key: string) => storage.removeItem(key), }; -const client = new GeoApiClient("https://momswap.produktor.duckdns.org", storageLike); +const client = new GeoApiClient("https://tenerife.baby", storageLike); const keys = await client.ensureKeysInStorage(); // Register (ignored if already registered); then login @@ -131,8 +138,29 @@ try { await client.loginWithSignature(keys.publicKey, keys.privateKey); const created = await client.createCollection("My Places"); -await client.createPointFeature(created.id, -16.6291, 28.4636, { name: "Santa Cruz" }); +const feature = await client.createPointFeature(created.id, -16.6291, 28.4636, { name: "Santa Cruz" }); + +// 3D/image asset flow +const asset = await client.createOrLinkAsset({ + featureId: feature.id, + checksum: "sha256hex...", + ext: "glb", + kind: "3d", + mimeType: "model/gltf-binary", + sizeBytes: 1024, + name: "Palm Tree", + description: "Low-poly model", + isPublic: true, +}); +const signed = await client.getAssetSignedUploadUrl(asset.asset.id, "model/gltf-binary"); +// fetch(signed.url, { method: signed.method, body: file }) + const features = await client.listFeatures(created.id); +const firstAsset = features.features[0]?.properties?.assets?.[0]; +if (firstAsset) { + const shareUrl = client.resolveRelativeLink(firstAsset.link); + console.log("Share URL:", shareUrl); +} console.log(features); ``` diff --git a/libs/geo-api-client/src/GeoApiClient.ts b/libs/geo-api-client/src/GeoApiClient.ts index 830a35d..09b5b89 100644 --- a/libs/geo-api-client/src/GeoApiClient.ts +++ b/libs/geo-api-client/src/GeoApiClient.ts @@ -29,7 +29,7 @@ export class GeoApiClient { private accessToken: string | null = null; /** - * @param baseUrl - API base URL (e.g. https://momswap.produktor.duckdns.org) + * @param baseUrl - API base URL (e.g. https://tenerife.baby) * @param storage - Storage adapter (localStorage-like: getItem, setItem, removeItem) * @param storageKey - Key for persisting keypair (default from DEFAULT_KEYS_STORAGE_KEY) */ diff --git a/web/app.js b/web/app.js index cd9c7cb..5a53217 100644 --- a/web/app.js +++ b/web/app.js @@ -6,7 +6,7 @@ const { createApp, ref, reactive, onMounted, watch } = Vue; createApp({ setup() { - const apiBase = ref(localStorage.getItem("geo_api_base") || "https://momswap.produktor.duckdns.org"); + const apiBase = ref(localStorage.getItem("geo_api_base") || "https://tenerife.baby"); const state = reactive({ publicKey: "", privateKey: "",