CI / test (push) Successful in 4s
Demo app (web/):
- Collections: select, rename, remove (x button per row), delete
- Features: add point (lon/lat validation), remove, list in selected collection
- QR codes for pk (private) and pb (public) keys
- Restore public key from private key
- 409 Conflict handled for already-registered login
- Title: Momswap Geo Backend Use-Cases Test
Backend:
- PATCH /v1/collections/{id} for rename
- DELETE /v1/collections/{id}
- Clearer lon/lat validation errors (-180..180, -90..90)
Client:
- updateCollection, deleteCollection, derivePublicKey
Docs:
- docs/frontend-development.md (demo app, local dev)
- README links to all docs
Made-with: Cursor
233 lines
4.6 KiB
Go
233 lines
4.6 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) DeleteCollection(id string) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if _, ok := s.collections[id]; !ok {
|
|
return ErrNotFound
|
|
}
|
|
for fid, f := range s.features {
|
|
if f.CollectionID == id {
|
|
delete(s.features, fid)
|
|
}
|
|
}
|
|
delete(s.collections, id)
|
|
return 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)
|
|
}
|
|
}
|
|
}
|