Implement geo backend, TS client, frontend, and CI tests.
Add a Go HTTP API with Ed25519 auth and invitation onboarding, user-scoped GeoJSON Point management, a Bun-tested @noble/ed25519 TypeScript client, static Vue/Vuetify frontend integration, and a Gitea CI workflow running both Go and Bun test suites. Made-with: Cursor
This commit is contained in:
+17
@@ -0,0 +1,17 @@
|
||||
import { GeoApiClient } from "../libs/geo-api-client/dist/index.js";
|
||||
|
||||
class BrowserStorage {
|
||||
getItem(key) {
|
||||
return localStorage.getItem(key);
|
||||
}
|
||||
setItem(key, value) {
|
||||
localStorage.setItem(key, value);
|
||||
}
|
||||
removeItem(key) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
|
||||
export function createApiClient(baseUrl) {
|
||||
return new GeoApiClient(baseUrl, new BrowserStorage());
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
import { createApiClient } from "./api.js";
|
||||
|
||||
const { createApp, ref, reactive, onMounted } = Vue;
|
||||
|
||||
createApp({
|
||||
setup() {
|
||||
const apiBase = ref(localStorage.getItem("geo_api_base") || "http://localhost:8080");
|
||||
const state = reactive({
|
||||
publicKey: "",
|
||||
privateKey: "",
|
||||
accessToken: "",
|
||||
collections: [],
|
||||
newCollectionName: "",
|
||||
status: "Ready",
|
||||
});
|
||||
|
||||
let client = createApiClient(apiBase.value);
|
||||
|
||||
const rebuildClient = () => {
|
||||
client = createApiClient(apiBase.value);
|
||||
localStorage.setItem("geo_api_base", apiBase.value);
|
||||
state.status = `API base set to ${apiBase.value}`;
|
||||
};
|
||||
|
||||
const ensureKeys = async () => {
|
||||
try {
|
||||
const keys = await client.ensureKeysInStorage();
|
||||
state.publicKey = keys.publicKey;
|
||||
state.privateKey = keys.privateKey;
|
||||
state.status = "Keys loaded from localStorage.";
|
||||
} catch (err) {
|
||||
state.status = err.message;
|
||||
}
|
||||
};
|
||||
|
||||
const login = async () => {
|
||||
try {
|
||||
state.accessToken = await client.loginWithSignature(state.publicKey, state.privateKey);
|
||||
client.setAccessToken(state.accessToken);
|
||||
state.status = "Authenticated.";
|
||||
} catch (err) {
|
||||
state.status = err.message;
|
||||
}
|
||||
};
|
||||
|
||||
const listCollections = async () => {
|
||||
try {
|
||||
client.setAccessToken(state.accessToken);
|
||||
const data = await client.listCollections();
|
||||
state.collections = data.collections || [];
|
||||
} catch (err) {
|
||||
state.status = err.message;
|
||||
}
|
||||
};
|
||||
|
||||
const createCollection = async () => {
|
||||
try {
|
||||
client.setAccessToken(state.accessToken);
|
||||
await client.createCollection(state.newCollectionName);
|
||||
state.newCollectionName = "";
|
||||
await listCollections();
|
||||
} catch (err) {
|
||||
state.status = err.message;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await ensureKeys();
|
||||
});
|
||||
|
||||
return {
|
||||
apiBase,
|
||||
state,
|
||||
rebuildClient,
|
||||
ensureKeys,
|
||||
login,
|
||||
listCollections,
|
||||
createCollection,
|
||||
};
|
||||
},
|
||||
}).use(Vuetify.createVuetify()).mount("#app");
|
||||
@@ -0,0 +1,75 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Momswap Geo Console</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/vuetify@3.7.7/dist/vuetify.min.css" rel="stylesheet" />
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
|
||||
}
|
||||
.glass {
|
||||
backdrop-filter: blur(8px);
|
||||
background: rgba(15, 23, 42, 0.7);
|
||||
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<v-app theme="dark">
|
||||
<v-container class="py-8">
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-card class="glass rounded-xl pa-6">
|
||||
<v-card-title class="text-h4 mb-2">Momswap Geo Backend Console</v-card-title>
|
||||
<v-card-subtitle>Ed25519 auth, invitation onboarding, and user-scoped feature collections</v-card-subtitle>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-card class="glass rounded-xl pa-4">
|
||||
<v-card-title>Connection & Identity</v-card-title>
|
||||
<v-text-field v-model="apiBase" label="API Base URL"></v-text-field>
|
||||
<v-btn color="primary" @click="rebuildClient">Apply API URL</v-btn>
|
||||
<v-divider class="my-4"></v-divider>
|
||||
<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="success" class="mt-2" @click="login">Login</v-btn>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-card class="glass rounded-xl pa-4">
|
||||
<v-card-title>Collections</v-card-title>
|
||||
<v-text-field v-model="state.newCollectionName" label="New collection name"></v-text-field>
|
||||
<v-btn color="primary" @click="createCollection">Create Collection</v-btn>
|
||||
<v-btn class="ml-2" @click="listCollections">Refresh</v-btn>
|
||||
<v-list class="mt-4">
|
||||
<v-list-item v-for="item in state.collections" :key="item.id">
|
||||
<v-list-item-title>{{ item.name }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ item.id }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-alert type="info" variant="tonal">{{ state.status }}</v-alert>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-app>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@3.5.13/dist/vue.global.prod.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vuetify@3.7.7/dist/vuetify.min.js"></script>
|
||||
<script type="module" src="./app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user