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
218 lines
4.3 KiB
Go
218 lines
4.3 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|