- bin/gen-server-keys.sh: generate Ed25519 keypair to etc/server-service.{pub,key,env}
- main.go: read keys from file (ADMIN_PUBLIC_KEY_FILE) when env empty
- docker-compose: env_file etc/server-service.env, mount etc/
- bin/up.sh: auto-run gen-server-keys if etc/server-service.env missing
- ErrRegistrationNotConfigured for clearer 503 when keys not set
- etc/README.md, etc/.gitignore
- bin/gen-admin-key.sh for one-off key gen
- .env.example
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Required for registration-by-signature and bootstrap admin.
|
||||
# Generate with: ./bin/gen-admin-key.sh
|
||||
ADMIN_PUBLIC_KEY=
|
||||
|
||||
# Optional: override service key for registration (defaults to ADMIN_PUBLIC_KEY)
|
||||
# SERVICE_PUBLIC_KEY=
|
||||
@@ -13,6 +13,7 @@ This file gives future coding agents a fast path map for this repository.
|
||||
- TypeScript API client: `libs/geo-api-client/`
|
||||
- CI workflow: `.gitea/workflows/ci.yml`
|
||||
- Architecture/planning docs: `docs/`
|
||||
- Server keys: `etc/` (generated by `./bin/gen-server-keys.sh`)
|
||||
|
||||
## Most common commands
|
||||
|
||||
@@ -21,7 +22,8 @@ From repo root:
|
||||
```bash
|
||||
go test ./...
|
||||
go run ./cmd/api
|
||||
docker compose up --build -d
|
||||
./bin/gen-server-keys.sh # before first docker up (creates etc/server-service.*)
|
||||
./bin/up.sh # or: docker compose up --build -d
|
||||
docker compose down
|
||||
docker compose --profile test run --rm test # run tests as root (avoids var/ permission issues)
|
||||
```
|
||||
|
||||
@@ -33,17 +33,22 @@ Local default (for development): `http://localhost:8122`.
|
||||
Optional environment variables:
|
||||
|
||||
- `ADDR` (default `:8122`)
|
||||
- `ADMIN_PUBLIC_KEY` (bootstrap initial inviter/admin user)
|
||||
- `ADMIN_PUBLIC_KEY` — **required for registration**: bootstrap admin + service key for `register-by-signature`. Generate with `./bin/gen-admin-key.sh`
|
||||
- `SERVICE_PUBLIC_KEY` (public key users sign to register; defaults to `ADMIN_PUBLIC_KEY`)
|
||||
|
||||
**Deployment:** Set `ADMIN_PUBLIC_KEY` before starting. Without it, `/v1/service-key` returns 503 and registration is disabled.
|
||||
|
||||
## Docker Compose
|
||||
|
||||
Build and run the backend service:
|
||||
Generate server keys (creates `etc/server-service.*`), then build and run:
|
||||
|
||||
```bash
|
||||
./bin/gen-server-keys.sh
|
||||
COMPOSE_BAKE=true docker compose up --build -d
|
||||
```
|
||||
|
||||
Or use `./bin/up.sh` which runs the key generation if needed.
|
||||
|
||||
This starts:
|
||||
|
||||
- `db` (`postgis/postgis`) on `5432`
|
||||
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
# Generate Ed25519 keypair for ADMIN_PUBLIC_KEY (one-off, prints to stdout).
|
||||
# For etc/ + docker compose, use ./bin/gen-server-keys.sh instead.
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
(cd libs/geo-api-client && bun run build 2>/dev/null) || true
|
||||
cd libs/geo-api-client
|
||||
bun -e "
|
||||
import { generateKeyPair } from './dist/index.js';
|
||||
const k = await generateKeyPair();
|
||||
console.log('ADMIN_PUBLIC_KEY=' + k.publicKey);
|
||||
console.log('# Private key (keep secret, bootstrap only): ' + k.privateKey);
|
||||
"
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
# Generate server-service Ed25519 keypair. Output in etc/ for docker compose.
|
||||
# Clients download the public key via GET /v1/service-key.
|
||||
set -e
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
mkdir -p "$ROOT/etc"
|
||||
(cd "$ROOT/libs/geo-api-client" && bun run build 2>/dev/null) || true
|
||||
cd "$ROOT/libs/geo-api-client"
|
||||
OUT=$(bun -e "
|
||||
import { generateKeyPair } from './dist/index.js';
|
||||
const k = await generateKeyPair();
|
||||
console.log(k.publicKey);
|
||||
console.log(k.privateKey);
|
||||
")
|
||||
PUB=$(echo "$OUT" | head -1)
|
||||
PRIV=$(echo "$OUT" | tail -1)
|
||||
echo "$PUB" > "$ROOT/etc/server-service.pub"
|
||||
echo "$PRIV" > "$ROOT/etc/server-service.key"
|
||||
echo "ADMIN_PUBLIC_KEY=$PUB" > "$ROOT/etc/server-service.env"
|
||||
echo "SERVICE_PUBLIC_KEY=$PUB" >> "$ROOT/etc/server-service.env"
|
||||
chmod 600 "$ROOT/etc/server-service.key" "$ROOT/etc/server-service.env" 2>/dev/null || true
|
||||
echo "Wrote etc/server-service.pub, etc/server-service.key, etc/server-service.env"
|
||||
@@ -1,4 +1,8 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
if [ ! -f etc/server-service.env ]; then
|
||||
echo "Generating server keys (etc/server-service.env)..."
|
||||
./bin/gen-server-keys.sh
|
||||
fi
|
||||
docker compose up --build -d
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"momswap/backend/internal/app"
|
||||
@@ -16,6 +17,16 @@ func main() {
|
||||
adminPublicKey := os.Getenv("ADMIN_PUBLIC_KEY")
|
||||
servicePublicKey := getEnv("SERVICE_PUBLIC_KEY", adminPublicKey)
|
||||
|
||||
if adminPublicKey == "" {
|
||||
adminPublicKey = readKeyFile(getEnv("ADMIN_PUBLIC_KEY_FILE", "etc/server-service.pub"))
|
||||
}
|
||||
if servicePublicKey == "" {
|
||||
servicePublicKey = readKeyFile(getEnv("SERVICE_PUBLIC_KEY_FILE", "etc/server-service.pub"))
|
||||
}
|
||||
if servicePublicKey == "" {
|
||||
servicePublicKey = adminPublicKey
|
||||
}
|
||||
|
||||
memory := store.NewMemoryStore()
|
||||
service := app.NewService(memory, app.Config{
|
||||
ChallengeTTL: 5 * time.Minute,
|
||||
@@ -37,3 +48,11 @@ func getEnv(key, fallback string) string {
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func readKeyFile(path string) string {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(b))
|
||||
}
|
||||
|
||||
@@ -23,10 +23,14 @@ services:
|
||||
target: runtime
|
||||
image: momswap-backend:latest
|
||||
container_name: momswap-backend-api
|
||||
env_file:
|
||||
- etc/server-service.env
|
||||
environment:
|
||||
ADDR: ":8122"
|
||||
ADMIN_PUBLIC_KEY: "${ADMIN_PUBLIC_KEY:-}"
|
||||
DATABASE_URL: "postgres://${POSTGRES_USER:-momswap}:${POSTGRES_PASSWORD:-momswap}@db:5432/${POSTGRES_DB:-momswap}?sslmode=disable"
|
||||
volumes:
|
||||
- ./etc:/app/etc:ro
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
@@ -42,10 +46,14 @@ services:
|
||||
target: dev
|
||||
image: momswap-backend:dev
|
||||
container_name: momswap-backend-api-dev
|
||||
env_file:
|
||||
- etc/server-service.env
|
||||
environment:
|
||||
ADDR: ":8122"
|
||||
ADMIN_PUBLIC_KEY: "${ADMIN_PUBLIC_KEY:-}"
|
||||
DATABASE_URL: "postgres://${POSTGRES_USER:-momswap}:${POSTGRES_PASSWORD:-momswap}@db:5432/${POSTGRES_DB:-momswap}?sslmode=disable"
|
||||
volumes:
|
||||
- ./etc:/src/etc:ro
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
@@ -68,10 +68,12 @@ Key methods:
|
||||
|
||||
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.
|
||||
1. `GET /v1/service-key` — fetch the server public key (clients use this for registration and further signed communication).
|
||||
2. Sign that key with your private key.
|
||||
3. `POST /v1/auth/register-by-signature` with `{ publicKey, signature }`.
|
||||
|
||||
Server keys are generated with `./bin/gen-server-keys.sh` and stored in `etc/`.
|
||||
|
||||
## Example (TypeScript app)
|
||||
|
||||
```ts
|
||||
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
server-service.pub
|
||||
server-service.key
|
||||
server-service.env
|
||||
@@ -0,0 +1,26 @@
|
||||
# Server Service Keys
|
||||
|
||||
Server Ed25519 keypair for client authentication and registration.
|
||||
|
||||
## Generate
|
||||
|
||||
```bash
|
||||
./bin/gen-server-keys.sh
|
||||
```
|
||||
|
||||
Creates:
|
||||
|
||||
- `server-service.pub` — public key; clients download via `GET /v1/service-key`
|
||||
- `server-service.key` — private key (keep secret)
|
||||
- `server-service.env` — env vars for docker compose (`ADMIN_PUBLIC_KEY`, `SERVICE_PUBLIC_KEY`)
|
||||
|
||||
## Client Usage
|
||||
|
||||
Clients fetch the server public key and use it to:
|
||||
|
||||
1. **Register** — sign the server pubkey, post to `POST /v1/auth/register-by-signature`
|
||||
2. **Verify server identity** — for future signed responses or request validation
|
||||
|
||||
## Docker Compose
|
||||
|
||||
The api service uses `env_file: etc/server-service.env` and mounts `./etc` so keys are available. Run `./bin/gen-server-keys.sh` before first `docker compose up`.
|
||||
@@ -15,6 +15,7 @@ var (
|
||||
ErrUnauthorized = errors.New("unauthorized")
|
||||
ErrForbidden = errors.New("forbidden")
|
||||
ErrBadRequest = errors.New("bad request")
|
||||
ErrRegistrationNotConfigured = errors.New("registration by signature not configured; set ADMIN_PUBLIC_KEY")
|
||||
ErrInviteInvalid = errors.New("invite invalid")
|
||||
ErrInviteExpired = errors.New("invite expired")
|
||||
ErrInviteExhaust = errors.New("invite exhausted")
|
||||
@@ -62,7 +63,7 @@ func (s *Service) ServicePublicKey() string {
|
||||
|
||||
func (s *Service) RegisterBySignature(publicKey, signature string) error {
|
||||
if s.servicePublicKey == "" {
|
||||
return fmt.Errorf("%w: registration by signature not configured", ErrBadRequest)
|
||||
return ErrRegistrationNotConfigured
|
||||
}
|
||||
if publicKey == "" {
|
||||
return fmt.Errorf("%w: missing public key", ErrBadRequest)
|
||||
|
||||
@@ -88,6 +88,8 @@ func statusFromErr(err error) int {
|
||||
return http.StatusBadRequest
|
||||
case errors.Is(err, app.ErrAlreadyUser):
|
||||
return http.StatusConflict
|
||||
case errors.Is(err, app.ErrRegistrationNotConfigured):
|
||||
return http.StatusServiceUnavailable
|
||||
case errors.Is(err, app.ErrInviteInvalid),
|
||||
errors.Is(err, app.ErrInviteExpired),
|
||||
errors.Is(err, app.ErrInviteExhaust):
|
||||
@@ -165,7 +167,7 @@ func (a *API) login(w http.ResponseWriter, r *http.Request) {
|
||||
func (a *API) getServiceKey(w http.ResponseWriter, _ *http.Request) {
|
||||
pk := a.service.ServicePublicKey()
|
||||
if pk == "" {
|
||||
writeErr(w, app.ErrBadRequest)
|
||||
writeErr(w, app.ErrRegistrationNotConfigured)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"publicKey": pk})
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ createApp({
|
||||
try {
|
||||
await client.registerBySigningServiceKey(state.publicKey, state.privateKey);
|
||||
} catch (err) {
|
||||
if (!err.message.includes("already registered") && !err.message.includes("not configured")) {
|
||||
if (!err.message.includes("already registered") && !err.message.includes("not configured") && !err.message.includes("ADMIN_PUBLIC_KEY")) {
|
||||
throw err;
|
||||
}
|
||||
// Proceed to login: already registered or registration disabled (invitation flow)
|
||||
|
||||
Reference in New Issue
Block a user