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:
2026-03-01 11:41:21 +00:00
parent 5c73295ce5
commit 6e2becb06a
164 changed files with 446560 additions and 0 deletions
+217
View File
@@ -0,0 +1,217 @@
package store
import (
"errors"
"sync"
"time"
)
var (
ErrNotFound = errors.New("not found")
ErrAlreadyExists = errors.New("already exists")
)
type MemoryStore struct {
mu sync.RWMutex
users map[string]User
challenges map[string]Challenge
sessions map[string]Session
invitations map[string]Invitation
collections map[string]Collection
features map[string]Feature
}
func NewMemoryStore() *MemoryStore {
return &MemoryStore{
users: make(map[string]User),
challenges: make(map[string]Challenge),
sessions: make(map[string]Session),
invitations: make(map[string]Invitation),
collections: make(map[string]Collection),
features: make(map[string]Feature),
}
}
func (s *MemoryStore) UpsertUser(user User) {
s.mu.Lock()
defer s.mu.Unlock()
s.users[user.PublicKey] = user
}
func (s *MemoryStore) GetUser(publicKey string) (User, error) {
s.mu.RLock()
defer s.mu.RUnlock()
user, ok := s.users[publicKey]
if !ok {
return User{}, ErrNotFound
}
return user, nil
}
func (s *MemoryStore) CreateChallenge(ch Challenge) error {
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.challenges[ch.Nonce]; exists {
return ErrAlreadyExists
}
s.challenges[ch.Nonce] = ch
return nil
}
func (s *MemoryStore) GetChallenge(nonce string) (Challenge, error) {
s.mu.RLock()
defer s.mu.RUnlock()
ch, ok := s.challenges[nonce]
if !ok {
return Challenge{}, ErrNotFound
}
return ch, nil
}
func (s *MemoryStore) MarkChallengeUsed(nonce string) error {
s.mu.Lock()
defer s.mu.Unlock()
ch, ok := s.challenges[nonce]
if !ok {
return ErrNotFound
}
ch.Used = true
s.challenges[nonce] = ch
return nil
}
func (s *MemoryStore) SaveSession(session Session) {
s.mu.Lock()
defer s.mu.Unlock()
s.sessions[session.Token] = session
}
func (s *MemoryStore) GetSession(token string) (Session, error) {
s.mu.RLock()
defer s.mu.RUnlock()
session, ok := s.sessions[token]
if !ok {
return Session{}, ErrNotFound
}
return session, nil
}
func (s *MemoryStore) SaveInvitation(inv Invitation) error {
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.invitations[inv.JTI]; exists {
return ErrAlreadyExists
}
s.invitations[inv.JTI] = inv
return nil
}
func (s *MemoryStore) GetInvitation(jti string) (Invitation, error) {
s.mu.RLock()
defer s.mu.RUnlock()
inv, ok := s.invitations[jti]
if !ok {
return Invitation{}, ErrNotFound
}
return inv, nil
}
func (s *MemoryStore) IncrementInvitationUsage(jti string) error {
s.mu.Lock()
defer s.mu.Unlock()
inv, ok := s.invitations[jti]
if !ok {
return ErrNotFound
}
inv.UsedCount++
s.invitations[jti] = inv
return nil
}
func (s *MemoryStore) SaveCollection(c Collection) {
s.mu.Lock()
defer s.mu.Unlock()
s.collections[c.ID] = c
}
func (s *MemoryStore) ListCollectionsByOwner(owner string) []Collection {
s.mu.RLock()
defer s.mu.RUnlock()
result := make([]Collection, 0)
for _, c := range s.collections {
if c.OwnerKey == owner {
result = append(result, c)
}
}
return result
}
func (s *MemoryStore) GetCollection(id string) (Collection, error) {
s.mu.RLock()
defer s.mu.RUnlock()
c, ok := s.collections[id]
if !ok {
return Collection{}, ErrNotFound
}
return c, nil
}
func (s *MemoryStore) SaveFeature(f Feature) {
s.mu.Lock()
defer s.mu.Unlock()
s.features[f.ID] = f
}
func (s *MemoryStore) ListFeaturesByCollection(collectionID string) []Feature {
s.mu.RLock()
defer s.mu.RUnlock()
result := make([]Feature, 0)
for _, f := range s.features {
if f.CollectionID == collectionID {
result = append(result, f)
}
}
return result
}
func (s *MemoryStore) GetFeature(featureID string) (Feature, error) {
s.mu.RLock()
defer s.mu.RUnlock()
f, ok := s.features[featureID]
if !ok {
return Feature{}, ErrNotFound
}
return f, nil
}
func (s *MemoryStore) DeleteFeature(featureID string) error {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.features[featureID]; !ok {
return ErrNotFound
}
delete(s.features, featureID)
return nil
}
func (s *MemoryStore) PruneExpired(now time.Time) {
s.mu.Lock()
defer s.mu.Unlock()
for nonce, ch := range s.challenges {
if ch.ExpiresAt.Before(now) {
delete(s.challenges, nonce)
}
}
for token, sess := range s.sessions {
if sess.ExpiresAt.Before(now) {
delete(s.sessions, token)
}
}
for jti, inv := range s.invitations {
if inv.ExpiresAt.Before(now) {
delete(s.invitations, jti)
}
}
}
+54
View File
@@ -0,0 +1,54 @@
package store
import "time"
type User struct {
PublicKey string `json:"publicKey"`
Inviter string `json:"inviter,omitempty"`
CreatedAt time.Time `json:"createdAt"`
}
type Challenge struct {
Nonce string
PublicKey string
ExpiresAt time.Time
Used bool
}
type Session struct {
Token string
PublicKey string
ExpiresAt time.Time
}
type Invitation struct {
JTI string
InviterPublicKey string
InviteePublicKey string
ExpiresAt time.Time
MaxUses int
UsedCount int
}
type Collection struct {
ID string `json:"id"`
OwnerKey string `json:"ownerKey"`
Name string `json:"name"`
CreatedAt time.Time `json:"createdAt"`
}
type Point struct {
Type string `json:"type"`
Coordinates []float64 `json:"coordinates"`
}
type Feature struct {
ID string `json:"id"`
CollectionID string `json:"collectionId"`
OwnerKey string `json:"ownerKey"`
Type string `json:"type"`
Geometry Point `json:"geometry"`
Properties map[string]interface{} `json:"properties"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}