Add register-by-signature, web fixes, bin scripts, docs
CI / test (push) Successful in 5s

- Register by signing service key: GET /v1/service-key, POST /v1/auth/register-by-signature
- Login auto-attempts register first for new users
- Web: default API URL momswap.produktor.duckdns.org, /libs/ static handler
- Docker: webbuild stage for geo-api-client, copy web+libs to runtime
- Bin scripts: test.sh, run.sh, up.sh, down.sh
- docs/ed25519-security-use-cases.md: use cases, message formats, examples
- SERVICE_PUBLIC_KEY env (defaults to ADMIN_PUBLIC_KEY)

Made-with: Cursor
This commit is contained in:
2026-03-01 12:58:44 +00:00
parent 978e0403eb
commit a5a97a0ad9
19 changed files with 405 additions and 41 deletions
+1
View File
@@ -1,6 +1,7 @@
.git
.gitea
docs
var
node_modules
**/node_modules
**/*.log
+1
View File
@@ -23,6 +23,7 @@ go test ./...
go run ./cmd/api
docker compose up --build -d
docker compose down
docker compose --profile test run --rm test # run tests as root (avoids var/ permission issues)
```
TypeScript client:
+17
View File
@@ -22,15 +22,32 @@ RUN --mount=type=cache,target=/go/pkg/mod \
CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} \
go build -p "$(nproc)" -trimpath -ldflags="-s -w" -o /out/api ./cmd/api
FROM base AS webbuild
RUN apt-get update && apt-get install -y --no-install-recommends unzip curl && rm -rf /var/lib/apt/lists/* \
&& curl -fsSL https://bun.sh/install | bash \
&& mv /root/.bun/bin/bun /usr/local/bin/ && rm -rf /root/.bun
WORKDIR /src/libs/geo-api-client
RUN bun install && bun run build
FROM base AS dev
ENV ADDR=:8122
EXPOSE 8122
CMD ["go", "run", "./cmd/api"]
FROM base AS test
USER root
RUN apt-get update && apt-get install -y --no-install-recommends unzip curl && rm -rf /var/lib/apt/lists/* \
&& curl -fsSL https://bun.sh/install | bash \
&& mv /root/.bun/bin/bun /usr/local/bin/ && rm -rf /root/.bun
WORKDIR /src
CMD ["sh", "-c", "go test ./... && cd libs/geo-api-client && bun install && bun test"]
FROM gcr.io/distroless/static-debian12:nonroot AS runtime
WORKDIR /app
COPY --from=builder /out/api /app/api
COPY --from=builder /src/web /app/web
COPY --from=webbuild /src/libs /app/libs
ENV ADDR=:8122
EXPOSE 8122
+7
View File
@@ -20,6 +20,12 @@ go test ./...
go run ./cmd/api
```
Run tests via Docker (avoids local permission issues, e.g. `var/`):
```bash
docker compose --profile test run --rm test
```
Primary deployed base URL: `https://momswap.produktor.duckdns.org/`.
Local default (for development): `http://localhost:8122`.
@@ -28,6 +34,7 @@ Optional environment variables:
- `ADDR` (default `:8122`)
- `ADMIN_PUBLIC_KEY` (bootstrap initial inviter/admin user)
- `SERVICE_PUBLIC_KEY` (public key users sign to register; defaults to `ADMIN_PUBLIC_KEY`)
## Docker Compose
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
set -e
cd "$(dirname "$0")/.."
docker compose down
Executable
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
set -e
cd "$(dirname "$0")/.."
go run ./cmd/api
Executable
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
set -e
cd "$(dirname "$0")/.."
docker compose --profile test run --rm test
Executable
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
set -e
cd "$(dirname "$0")/.."
docker compose up --build -d
+2 -1
View File
@@ -14,12 +14,13 @@ import (
func main() {
addr := getEnv("ADDR", ":8122")
adminPublicKey := os.Getenv("ADMIN_PUBLIC_KEY")
servicePublicKey := getEnv("SERVICE_PUBLIC_KEY", adminPublicKey)
memory := store.NewMemoryStore()
service := app.NewService(memory, app.Config{
ChallengeTTL: 5 * time.Minute,
SessionTTL: 24 * time.Hour,
})
}, servicePublicKey)
service.BootstrapAdmin(adminPublicKey)
api := httpapi.NewAPI(service)
+13
View File
@@ -68,3 +68,16 @@ services:
- action: rebuild
path: ./Dockerfile
test:
build:
context: .
dockerfile: Dockerfile
target: test
image: momswap-backend:test
volumes:
- .:/src
working_dir: /src
user: root
profiles:
- test
+173 -31
View File
@@ -1,10 +1,13 @@
# Ed25519 Security Use Cases
This document describes how Ed25519 key pairs are used for identity, authentication, and secure access to feature collection data.
This document describes how the Momswap Geo backend uses Ed25519 signatures for authentication and registration.
## 1) Identification by Public Key
## Overview
Each user identity is represented by an Ed25519 public key. The backend never needs the private key.
- **Identity** = Ed25519 public key (base64url, 43 chars, no padding)
- **All keys** use RFC 8032 Ed25519; Go `crypto/ed25519` on server, `@noble/ed25519` in TypeScript client
- **Signatures** are base64url-encoded (64 bytes → 86 chars unpadded)
- **Private keys never leave the client**; server only verifies signatures
```mermaid
flowchart LR
@@ -13,9 +16,13 @@ flowchart LR
backendApi -->|"registers identity by public key"| usersTable[(UsersByPublicKey)]
```
## 2) Authentication via Challenge Signature
## Use Cases
Authentication is challenge-response: client signs server nonce with private key; server verifies with stored public key.
### 1. Login (ChallengeResponse)
Proves the client controls the private key for a registered public key.
**Message to sign:** `login:` + nonce (exact UTF-8 string)
```mermaid
sequenceDiagram
@@ -32,36 +39,117 @@ sequenceDiagram
A-->>C: {accessToken}
```
## 3) Secure Data Storing (Write Feature Collection Data)
**Flow:**
Only an authenticated token linked to a verified public key can create collections and features for that owner key.
1. `POST /v1/auth/challenge` with `{"publicKey":"<base64url>"}`
2. Server returns `{"nonce":"...", "messageToSign":"login:<nonce>"}`
3. Client signs `login:<nonce>` with private key
4. `POST /v1/auth/login` with `{"publicKey","nonce","signature"}`
5. Server verifies and returns `{"accessToken":"..."}`
```mermaid
flowchart TD
requestCreate[CreateCollectionOrFeatureRequest] --> bearerCheck[BearerTokenValidation]
bearerCheck --> sessionStore[(Sessions)]
sessionStore --> ownerKey[ResolvedOwnerPublicKey]
ownerKey --> ownershipRule[OwnershipValidation]
ownershipRule --> writeData[(CollectionsAndFeatures)]
ownershipRule --> reject403[RejectForbidden]
**Example (curl):**
```bash
# Step 1: Get challenge
CHALLENGE=$(curl -s -X POST https://momswap.produktor.duckdns.org/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 \
-H "Content-Type: application/json" \
-d "{\"publicKey\":\"txdkGKNdcZIEoQMJ0dqum3msjT6-2mO4yLVhtidRFJI\",\"nonce\":\"$NONCE\",\"signature\":\"<your_signature_base64url>\"}"
```
## 4) Secure Data Receiving (Read Feature Collection Data)
**Example (TypeScript):**
Reads are filtered by ownership. A token from one key cannot read another key's collections/features.
```mermaid
flowchart TD
requestRead[ReadCollectionsOrFeaturesRequest] --> validateToken[ValidateBearerToken]
validateToken --> ownerKey[OwnerPublicKeyFromSession]
ownerKey --> queryOwned[QueryOnlyOwnerRows]
queryOwned --> responseData[ReturnOwnedCollectionsFeatures]
ownerKey --> denyOther[RejectOtherOwnerAccess]
```ts
const challenge = await client.createChallenge(publicKey);
const signature = await signMessage(privateKey, challenge.messageToSign);
const token = await client.loginWithSignature(publicKey, privateKey);
```
## 5) Invitation-Based Identity Expansion
---
Existing users can issue signed invitations. New users prove ownership of their own private key at registration.
### 2. Register by Signing Service Key
Registers a new user without an invitation. User proves key ownership by signing the API's advertised public key.
**Message to sign:** The service public key string (exact value from `GET /v1/service-key`)
**Flow:**
1. `GET /v1/service-key``{"publicKey":"<base64url>"}`
2. Client signs that exact string with private key
3. `POST /v1/auth/register-by-signature` with `{"publicKey","signature"}`
4. Server verifies and registers the user
**Requires:** `SERVICE_PUBLIC_KEY` or `ADMIN_PUBLIC_KEY` must be set.
**Example (curl):**
```bash
# Step 1: Fetch service key
SERVICE_KEY=$(curl -s https://momswap.produktor.duckdns.org/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 \
-H "Content-Type: application/json" \
-d "{\"publicKey\":\"<your_pubkey_base64url>\",\"signature\":\"<signature_base64url>\"}"
```
**Example (TypeScript):**
```ts
await client.registerBySigningServiceKey(publicKey, privateKey);
```
---
### 3. Create Invitation
An authenticated user creates a signed invitation for new users.
**Message to sign:** `invite:` + base64url(payload)
Payload JSON:
```json
{
"jti": "unique-token-id",
"inviterPublicKey": "<inviter_base64url>",
"inviteePublicKey": "",
"expiresAtUnix": 1735689600,
"maxUses": 1
}
```
**Flow:**
1. Build payload, base64url-encode it → `payloadB64`
2. Sign `invite:` + payloadB64 with inviter's private key
3. `POST /v1/invitations` (with Bearer token) with `{"invitePayloadB64","inviteSignature"}`
**Example (TypeScript):**
```ts
const payload = { jti, inviterPublicKey, expiresAtUnix, maxUses };
const payloadB64 = bytesToBase64Url(textToBytes(JSON.stringify(payload)));
const inviteSig = await signMessage(inviterPrivateKey, `invite:${payloadB64}`);
await client.createInvitation(payload, inviterPrivateKey);
```
---
### 4. Register with Invitation
New user registers using a valid invitation and proves key ownership.
**Messages to sign:**
1. Invite: inviter signs `invite:` + payloadB64 (validated on create)
2. Proof: invitee signs `register:` + `<publicKey>` + `:` + `<jti>`
```mermaid
flowchart LR
@@ -73,9 +161,63 @@ flowchart LR
verifyProof --> usersStore
```
**Flow:**
1. Obtain invitation (payloadB64, inviteSignature, jti) from inviter
2. Sign `register:` + yourPublicKey + `:` + jti with your private key
3. `POST /v1/auth/register` with `{"publicKey","invitePayloadB64","inviteSignature","proofSignature"}`
**Example (TypeScript):**
```ts
const proofSig = await signMessage(privateKey, `register:${publicKey}:${jti}`);
await client.registerWithInvitation({
publicKey, privateKey, invitePayloadB64, inviteSignature, jti
});
```
---
### 5. Secure Data Access (Collections & Features)
Only an authenticated token linked to a verified public key can create and read collections/features for that owner.
```mermaid
flowchart TD
request[CreateOrReadRequest] --> bearerCheck[BearerTokenValidation]
bearerCheck --> sessionStore[(Sessions)]
sessionStore --> ownerKey[ResolvedOwnerPublicKey]
ownerKey --> ownershipRule[OwnershipValidation]
ownershipRule --> data[(CollectionsAndFeatures)]
ownershipRule --> reject403[RejectForbidden]
```
---
## Message Format Summary
| Use Case | Message to Sign |
|-----------------------|------------------------------------------------------|
| Login | `login:` + nonce |
| Register (service) | Service public key (exact string from API) |
| Create invitation | `invite:` + base64url(payload) |
| Register (invitation) | `register:` + publicKey + `:` + jti |
## Security Notes
- Private keys stay client-side.
- Public keys are stable user identifiers.
- Session tokens are granted only after valid Ed25519 signature verification.
- Collection/feature access is enforced by owner key derived from session identity.
- **Replay protection:** Challenges are single-use and expire (5 min TTL). Invitations have expiry and max-use limits.
- **Key binding:** Login challenge is bound to the public key that requested it. Invitation proof binds the new user's key to the JTI.
- **Canonical encoding:** Use UTF-8 for all messages; base64url without padding for keys and signatures.
- **Storage:** Client stores keys in `localStorage`; consider an encrypted wrapper for higher-security deployments.
- **Private keys stay client-side.** Session tokens are granted only after valid Ed25519 signature verification.
## API Endpoints Reference
| Method | Endpoint | Auth | Purpose |
|--------|---------------------------------|------|----------------------------|
| GET | /v1/service-key | No | Fetch service pubkey |
| POST | /v1/auth/challenge | No | Get login nonce |
| POST | /v1/auth/login | No | Exchange signature for JWT |
| POST | /v1/auth/register-by-signature | No | Register (sign service key)|
| POST | /v1/auth/register | No | Register with invitation |
| POST | /v1/invitations | Yes | Create invitation |
+14 -3
View File
@@ -44,8 +44,10 @@ Key methods:
- `importKeys(keys)`
- `exportKeys()`
- `setAccessToken(token)`
- `getServicePublicKey()`
- `createChallenge(publicKey)`
- `loginWithSignature(publicKey, privateKey)`
- `registerBySigningServiceKey(publicKey, privateKey)` — register by signing the API service public key (no invitation required)
- `createInvitation(payload, inviterPrivateKey)`
- `registerWithInvitation(...)`
- `listCollections()`
@@ -57,9 +59,18 @@ Key methods:
1. Create one `GeoApiClient` instance per backend base URL.
2. Call `ensureKeysInStorage()` when app initializes.
3. Use `loginWithSignature()` to obtain and set a bearer token.
4. Call collection/feature methods after authentication.
5. Use `importKeys`/`exportKeys` in profile settings UX.
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.
## Registration by signing service key
When `SERVICE_PUBLIC_KEY` (or `ADMIN_PUBLIC_KEY`) is set, users can register without an invitation:
1. `GET /v1/service-key` — fetch the API public key to sign.
2. Sign that key with your private key.
3. `POST /v1/auth/register-by-signature` with `{ publicKey, signature }`.
## Example (TypeScript app)
+29 -4
View File
@@ -29,12 +29,13 @@ type Config struct {
}
type Service struct {
store *store.MemoryStore
config Config
store *store.MemoryStore
config Config
servicePublicKey string
}
func NewService(memoryStore *store.MemoryStore, cfg Config) *Service {
return &Service{store: memoryStore, config: cfg}
func NewService(memoryStore *store.MemoryStore, cfg Config, servicePublicKey string) *Service {
return &Service{store: memoryStore, config: cfg, servicePublicKey: servicePublicKey}
}
type InvitationPayload struct {
@@ -55,6 +56,30 @@ func (s *Service) BootstrapAdmin(publicKey string) {
})
}
func (s *Service) ServicePublicKey() string {
return s.servicePublicKey
}
func (s *Service) RegisterBySignature(publicKey, signature string) error {
if s.servicePublicKey == "" {
return fmt.Errorf("%w: registration by signature not configured", ErrBadRequest)
}
if publicKey == "" {
return fmt.Errorf("%w: missing public key", ErrBadRequest)
}
if _, err := s.store.GetUser(publicKey); err == nil {
return ErrAlreadyUser
}
if err := auth.VerifySignature(publicKey, s.servicePublicKey, signature); err != nil {
return fmt.Errorf("%w: signature verification failed", ErrBadRequest)
}
s.store.UpsertUser(store.User{
PublicKey: publicKey,
CreatedAt: time.Now().UTC(),
})
return nil
}
func (s *Service) CreateChallenge(publicKey string) (string, error) {
if publicKey == "" {
return "", fmt.Errorf("%w: missing public key", ErrBadRequest)
+57 -1
View File
@@ -21,7 +21,7 @@ func newTestServer(adminPublicKey string) *httptest.Server {
svc := app.NewService(memory, app.Config{
ChallengeTTL: 5 * time.Minute,
SessionTTL: 24 * time.Hour,
})
}, adminPublicKey)
svc.BootstrapAdmin(adminPublicKey)
api := httpapi.NewAPI(svc)
return httptest.NewServer(api.Routes())
@@ -36,6 +36,22 @@ func mustJSON(t *testing.T, value interface{}) []byte {
return b
}
func getJSON(t *testing.T, client *http.Client, url string, token string) (*http.Response, map[string]interface{}) {
t.Helper()
req, _ := http.NewRequest(http.MethodGet, url, nil)
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("do request: %v", err)
}
defer resp.Body.Close()
out := map[string]interface{}{}
_ = json.NewDecoder(resp.Body).Decode(&out)
return resp, out
}
func postJSON(t *testing.T, client *http.Client, url string, body interface{}, token string) (*http.Response, map[string]interface{}) {
t.Helper()
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(mustJSON(t, body)))
@@ -107,6 +123,46 @@ func registerUserViaAdmin(t *testing.T, client *http.Client, baseURL, adminPub s
}
}
func TestRegisterBySignature(t *testing.T) {
adminPub, _, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate admin key: %v", err)
}
adminPubB64 := base64.RawURLEncoding.EncodeToString(adminPub)
server := newTestServer(adminPubB64)
defer server.Close()
client := server.Client()
svcKeyResp, svcKeyData := getJSON(t, client, server.URL+"/v1/service-key", "")
if svcKeyResp.StatusCode != http.StatusOK {
t.Fatalf("get service key status=%d body=%v", svcKeyResp.StatusCode, svcKeyData)
}
if svcKeyData["publicKey"] != adminPubB64 {
t.Fatalf("service key mismatch: got %v", svcKeyData["publicKey"])
}
userPub, userPriv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate user key: %v", err)
}
userPubB64 := base64.RawURLEncoding.EncodeToString(userPub)
sig := base64.RawURLEncoding.EncodeToString(ed25519.Sign(userPriv, []byte(adminPubB64)))
regResp, regData := postJSON(t, client, server.URL+"/v1/auth/register-by-signature", map[string]string{
"publicKey": userPubB64,
"signature": sig,
}, "")
if regResp.StatusCode != http.StatusCreated {
t.Fatalf("register-by-signature status=%d body=%v", regResp.StatusCode, regData)
}
userToken := loginUser(t, client, server.URL, userPubB64, userPriv)
if userToken == "" {
t.Fatal("login after register-by-signature failed")
}
}
func TestRegisterLoginAndProfile(t *testing.T) {
adminPub, adminPriv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
+31
View File
@@ -22,11 +22,14 @@ func NewAPI(svc *app.Service) *API {
func (a *API) Routes() http.Handler {
mux := http.NewServeMux()
staticFiles := http.FileServer(http.Dir("web"))
libsFiles := http.FileServer(http.Dir("libs"))
mux.HandleFunc("GET /healthz", a.health)
mux.HandleFunc("GET /v1/service-key", a.getServiceKey)
mux.HandleFunc("POST /v1/auth/challenge", a.createChallenge)
mux.HandleFunc("POST /v1/auth/login", a.login)
mux.HandleFunc("POST /v1/auth/register", a.register)
mux.HandleFunc("POST /v1/auth/register-by-signature", a.registerBySignature)
mux.HandleFunc("POST /v1/invitations", a.createInvitation)
mux.HandleFunc("GET /v1/me/keys", a.me)
@@ -37,6 +40,7 @@ func (a *API) Routes() http.Handler {
mux.HandleFunc("DELETE /v1/features/{id}", a.deleteFeature)
mux.Handle("/web/", http.StripPrefix("/web/", staticFiles))
mux.Handle("/libs/", http.StripPrefix("/libs/", libsFiles))
mux.Handle("/", http.RedirectHandler("/web/", http.StatusTemporaryRedirect))
return withCORS(mux)
@@ -82,6 +86,8 @@ func statusFromErr(err error) int {
return http.StatusForbidden
case errors.Is(err, app.ErrBadRequest):
return http.StatusBadRequest
case errors.Is(err, app.ErrAlreadyUser):
return http.StatusConflict
case errors.Is(err, app.ErrInviteInvalid),
errors.Is(err, app.ErrInviteExpired),
errors.Is(err, app.ErrInviteExhaust):
@@ -156,6 +162,15 @@ func (a *API) login(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"accessToken": token})
}
func (a *API) getServiceKey(w http.ResponseWriter, _ *http.Request) {
pk := a.service.ServicePublicKey()
if pk == "" {
writeErr(w, app.ErrBadRequest)
return
}
writeJSON(w, http.StatusOK, map[string]string{"publicKey": pk})
}
func (a *API) register(w http.ResponseWriter, r *http.Request) {
var req struct {
PublicKey string `json:"publicKey"`
@@ -174,6 +189,22 @@ func (a *API) register(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusCreated, map[string]string{"status": "registered"})
}
func (a *API) registerBySignature(w http.ResponseWriter, r *http.Request) {
var req struct {
PublicKey string `json:"publicKey"`
Signature string `json:"signature"`
}
if err := readJSON(r, &req); err != nil {
writeErr(w, app.ErrBadRequest)
return
}
if err := a.service.RegisterBySignature(req.PublicKey, req.Signature); err != nil {
writeErr(w, err)
return
}
writeJSON(w, http.StatusCreated, map[string]string{"status": "registered"})
}
func (a *API) createInvitation(w http.ResponseWriter, r *http.Request) {
user, err := a.authUser(r)
if err != nil {
+11
View File
@@ -546,9 +546,20 @@ class GeoApiClient {
}
return await res.json();
}
async getServicePublicKey() {
return this.request("/v1/service-key", { method: "GET" });
}
async createChallenge(publicKey) {
return this.request("/v1/auth/challenge", { method: "POST", body: { publicKey } });
}
async registerBySigningServiceKey(publicKey, privateKey) {
const { publicKey: servicePublicKey } = await this.getServicePublicKey();
const signature = await signMessage(privateKey, servicePublicKey);
await this.request("/v1/auth/register-by-signature", {
method: "POST",
body: { publicKey, signature }
});
}
async loginWithSignature(publicKey, privateKey) {
const challenge = await this.createChallenge(publicKey);
const signature = await signMessage(privateKey, challenge.messageToSign);
+13
View File
@@ -62,10 +62,23 @@ export class GeoApiClient {
return (await res.json()) as T;
}
async getServicePublicKey(): Promise<{ publicKey: string }> {
return this.request("/v1/service-key", { method: "GET" });
}
async createChallenge(publicKey: string): Promise<{ nonce: string; messageToSign: string }> {
return this.request("/v1/auth/challenge", { method: "POST", body: { publicKey } });
}
async registerBySigningServiceKey(publicKey: string, privateKey: string): Promise<void> {
const { publicKey: servicePublicKey } = await this.getServicePublicKey();
const signature = await signMessage(privateKey, servicePublicKey);
await this.request("/v1/auth/register-by-signature", {
method: "POST",
body: { publicKey, signature },
});
}
async loginWithSignature(publicKey: string, privateKey: string): Promise<string> {
const challenge = await this.createChallenge(publicKey);
const signature = await signMessage(privateKey, challenge.messageToSign);
+19 -1
View File
@@ -4,7 +4,7 @@ const { createApp, ref, reactive, onMounted } = Vue;
createApp({
setup() {
const apiBase = ref(localStorage.getItem("geo_api_base") || "http://localhost:8122");
const apiBase = ref(localStorage.getItem("geo_api_base") || "https://momswap.produktor.duckdns.org");
const state = reactive({
publicKey: "",
privateKey: "",
@@ -33,8 +33,25 @@ createApp({
}
};
const register = async () => {
try {
await client.registerBySigningServiceKey(state.publicKey, state.privateKey);
state.status = "Registered. Use Login to authenticate.";
} catch (err) {
state.status = err.message;
}
};
const login = async () => {
try {
try {
await client.registerBySigningServiceKey(state.publicKey, state.privateKey);
} catch (err) {
if (!err.message.includes("already registered") && !err.message.includes("not configured")) {
throw err;
}
// Proceed to login: already registered or registration disabled (invitation flow)
}
state.accessToken = await client.loginWithSignature(state.publicKey, state.privateKey);
client.setAccessToken(state.accessToken);
state.status = "Authenticated.";
@@ -73,6 +90,7 @@ createApp({
state,
rebuildClient,
ensureKeys,
register,
login,
listCollections,
createCollection,
+1
View File
@@ -39,6 +39,7 @@
<v-btn color="secondary" @click="ensureKeys">Ensure Keys in localStorage</v-btn>
<v-textarea v-model="state.publicKey" class="mt-3" label="Public Key" rows="2"></v-textarea>
<v-textarea v-model="state.privateKey" label="Private Key (local only)" rows="2"></v-textarea>
<v-btn color="secondary" class="mt-2" @click="register">Register (sign service key)</v-btn>
<v-btn color="success" class="mt-2" @click="login">Login</v-btn>
</v-card>
</v-col>