|
|
@@ -8,9 +8,9 @@ This document explains how frontend developers should integrate with the backend
|
|
|
|
|
|
|
|
|
|
|
|
Primary backend URL for integration:
|
|
|
|
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
|
|
|
|
## Goals
|
|
|
|
|
|
|
|
|
|
|
@@ -33,60 +33,60 @@ bun test # unit + integration tests (docs flow)
|
|
|
|
bun run build
|
|
|
|
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)
|
|
|
|
## Public API (current)
|
|
|
|
|
|
|
|
|
|
|
|
### Class: `GeoApiClient`
|
|
|
|
### 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:
|
|
|
|
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 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.
|
|
|
|
- **Auth**
|
|
|
|
- [`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.
|
|
|
|
- `getServicePublicKey()`
|
|
|
|
- [`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.
|
|
|
|
- `createChallenge(publicKey)`
|
|
|
|
- [`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).
|
|
|
|
- `loginWithSignature(publicKey, privateKey)`
|
|
|
|
- [`exportKeys()`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L41) — Read stored keypair for export/backup.
|
|
|
|
- `registerBySigningServiceKey(publicKey, privateKey)`
|
|
|
|
- [`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.
|
|
|
|
- `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).
|
|
|
|
- **Assets (new)**
|
|
|
|
- [`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.
|
|
|
|
- `createOrLinkAsset({...})` — create metadata or reuse existing asset by checksum/ext and link it to a feature
|
|
|
|
- [`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.
|
|
|
|
- `getAssetSignedUploadUrl(assetId, contentType?)` — get signed `PUT` URL for binary upload
|
|
|
|
- [`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.
|
|
|
|
- `setAssetVisibility(assetId, isPublic)` — owner toggles public/private access
|
|
|
|
- [`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.
|
|
|
|
- `resolveRelativeLink(path)` — converts backend-relative asset links to absolute URLs for browser usage
|
|
|
|
- [`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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**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.
|
|
|
|
- Feature responses include linked assets in `feature.properties.assets`.
|
|
|
|
- [`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.
|
|
|
|
- Asset item fields include: `id`, `kind`, `name`, `description`, `checksum`, `ext`, `isPublic`, `link`.
|
|
|
|
- [`updateCollection(collectionId, name)`](https://git.produktor.io/momswap/backend/src/branch/main/libs/geo-api-client/src/GeoApiClient.ts#L140) — Rename a collection.
|
|
|
|
- `link` is always backend-relative (for example `/v1/assets/{id}/download`).
|
|
|
|
- [`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.
|
|
|
|
- Frontend should call `resolveRelativeLink(link)` and must not build direct S3 URLs.
|
|
|
|
|
|
|
|
|
|
|
|
**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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Recommended integration flow
|
|
|
|
## Recommended integration flow
|
|
|
|
|
|
|
|
|
|
|
@@ -94,8 +94,15 @@ Current frontend contract points:
|
|
|
|
2. Call `ensureKeysInStorage()` when app initializes.
|
|
|
|
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).
|
|
|
|
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.
|
|
|
|
4. Use `loginWithSignature()` to obtain and set a bearer token.
|
|
|
|
5. Call collection/feature methods after authentication.
|
|
|
|
5. Create collection and map features.
|
|
|
|
6. Use `importKeys`/`exportKeys` in profile settings UX.
|
|
|
|
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
|
|
|
|
## Registration by signing service key
|
|
|
|
|
|
|
|
|
|
|
@@ -119,7 +126,7 @@ const storageLike = {
|
|
|
|
removeItem: (key: string) => storage.removeItem(key),
|
|
|
|
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();
|
|
|
|
const keys = await client.ensureKeysInStorage();
|
|
|
|
|
|
|
|
|
|
|
|
// Register (ignored if already registered); then login
|
|
|
|
// Register (ignored if already registered); then login
|
|
|
@@ -131,8 +138,29 @@ try {
|
|
|
|
await client.loginWithSignature(keys.publicKey, keys.privateKey);
|
|
|
|
await client.loginWithSignature(keys.publicKey, keys.privateKey);
|
|
|
|
|
|
|
|
|
|
|
|
const created = await client.createCollection("My Places");
|
|
|
|
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 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);
|
|
|
|
console.log(features);
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|