package identity import ( "context" "crypto/rand" "encoding/hex" "errors" "sort" "strings" "sync" "time" ) var ( ErrNotFound = errors.New("not found") ErrAlreadyExists = errors.New("already exists") ErrBootstrapDone = errors.New("bootstrap already completed") ) type Store interface { BootstrapUser(context.Context, User) error CreateUser(context.Context, User) error UpdateUser(context.Context, User) error FindUserByUsername(context.Context, string) (User, error) FindUserByID(context.Context, string) (User, error) ListUsers(context.Context) ([]User, error) CreateSession(context.Context, Session) error SessionActive(context.Context, string, time.Time) (bool, error) RevokeSession(context.Context, string, time.Time) error AppendAudit(context.Context, AuditEntry) error ListAudit(context.Context, int) ([]AuditEntry, error) } type MemoryStore struct { mu sync.RWMutex users map[string]User byName map[string]string sessions map[string]Session audit []AuditEntry } func NewMemoryStore() *MemoryStore { return &MemoryStore{users: map[string]User{}, byName: map[string]string{}, sessions: map[string]Session{}} } func (s *MemoryStore) BootstrapUser(_ context.Context, user User) error { s.mu.Lock() defer s.mu.Unlock() if len(s.users) != 0 { return ErrBootstrapDone } return s.createUserLocked(user) } func (s *MemoryStore) CreateUser(_ context.Context, user User) error { s.mu.Lock() defer s.mu.Unlock() return s.createUserLocked(user) } func (s *MemoryStore) createUserLocked(user User) error { key := strings.ToLower(user.Username) if _, exists := s.byName[key]; exists { return ErrAlreadyExists } s.users[user.ID] = user s.byName[key] = user.ID return nil } func (s *MemoryStore) UpdateUser(_ context.Context, user User) error { s.mu.Lock() defer s.mu.Unlock() previous, exists := s.users[user.ID] if !exists { return ErrNotFound } delete(s.byName, strings.ToLower(previous.Username)) s.users[user.ID] = user s.byName[strings.ToLower(user.Username)] = user.ID return nil } func (s *MemoryStore) FindUserByUsername(_ context.Context, username string) (User, error) { s.mu.RLock() defer s.mu.RUnlock() id, exists := s.byName[strings.ToLower(username)] if !exists { return User{}, ErrNotFound } return s.users[id], nil } func (s *MemoryStore) FindUserByID(_ context.Context, id string) (User, error) { s.mu.RLock() defer s.mu.RUnlock() user, exists := s.users[id] if !exists { return User{}, ErrNotFound } return user, nil } func (s *MemoryStore) ListUsers(_ context.Context) ([]User, error) { s.mu.RLock() defer s.mu.RUnlock() users := make([]User, 0, len(s.users)) for _, user := range s.users { users = append(users, user) } sort.Slice(users, func(i, j int) bool { return users[i].Username < users[j].Username }) return users, nil } func (s *MemoryStore) CreateSession(_ context.Context, session Session) error { s.mu.Lock() defer s.mu.Unlock() s.sessions[session.ID] = session return nil } func (s *MemoryStore) SessionActive(_ context.Context, id string, now time.Time) (bool, error) { s.mu.RLock() defer s.mu.RUnlock() session, exists := s.sessions[id] return exists && session.RevokedAt == nil && now.Before(session.ExpiresAt), nil } func (s *MemoryStore) RevokeSession(_ context.Context, id string, now time.Time) error { s.mu.Lock() defer s.mu.Unlock() session, exists := s.sessions[id] if !exists { return ErrNotFound } session.RevokedAt = &now s.sessions[id] = session return nil } func (s *MemoryStore) AppendAudit(_ context.Context, entry AuditEntry) error { s.mu.Lock() defer s.mu.Unlock() s.audit = append(s.audit, entry) return nil } func (s *MemoryStore) ListAudit(_ context.Context, limit int) ([]AuditEntry, error) { s.mu.RLock() defer s.mu.RUnlock() if limit <= 0 || limit > 200 { limit = 100 } start := len(s.audit) - limit if start < 0 { start = 0 } entries := append([]AuditEntry(nil), s.audit[start:]...) for left, right := 0, len(entries)-1; left < right; left, right = left+1, right-1 { entries[left], entries[right] = entries[right], entries[left] } return entries, nil } func newID(prefix string) string { buffer := make([]byte, 16) if _, err := rand.Read(buffer); err != nil { panic("cryptographic random source unavailable") } return prefix + "_" + hex.EncodeToString(buffer) }