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:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
Reference in New Issue
Block a user