package memory import ( "context" "sync" "time" "github.com/orchard9/rdev/internal/domain" "github.com/orchard9/rdev/internal/port" ) // APIKeyRepository is an in-memory implementation of port.APIKeyRepository. type APIKeyRepository struct { keys map[domain.APIKeyID]*domain.APIKey keysByHash map[string]domain.APIKeyID nextID int mu sync.RWMutex } // NewAPIKeyRepository creates a new in-memory API key repository. func NewAPIKeyRepository() *APIKeyRepository { return &APIKeyRepository{ keys: make(map[domain.APIKeyID]*domain.APIKey), keysByHash: make(map[string]domain.APIKeyID), } } // Ensure APIKeyRepository implements port.APIKeyRepository at compile time. var _ port.APIKeyRepository = (*APIKeyRepository)(nil) // Create stores a new API key. func (r *APIKeyRepository) Create(ctx context.Context, key *domain.APIKey, keyHash string) error { r.mu.Lock() defer r.mu.Unlock() r.nextID++ key.ID = domain.APIKeyID(itoa(r.nextID)) key.CreatedAt = time.Now() // Store the key r.keys[key.ID] = key r.keysByHash[keyHash] = key.ID return nil } // GetByHash retrieves an API key by its hash. func (r *APIKeyRepository) GetByHash(ctx context.Context, keyHash string) (*domain.APIKey, error) { r.mu.RLock() defer r.mu.RUnlock() id, ok := r.keysByHash[keyHash] if !ok { return nil, domain.ErrKeyNotFound } key, ok := r.keys[id] if !ok { return nil, domain.ErrKeyNotFound } return key, nil } // Get retrieves an API key by ID. func (r *APIKeyRepository) Get(ctx context.Context, id domain.APIKeyID) (*domain.APIKey, error) { r.mu.RLock() defer r.mu.RUnlock() key, ok := r.keys[id] if !ok { return nil, domain.ErrKeyNotFound } return key, nil } // List returns all API keys (without secrets). func (r *APIKeyRepository) List(ctx context.Context) ([]*domain.APIKey, error) { r.mu.RLock() defer r.mu.RUnlock() keys := make([]*domain.APIKey, 0, len(r.keys)) for _, key := range r.keys { keys = append(keys, key) } return keys, nil } // Revoke marks an API key as revoked. func (r *APIKeyRepository) Revoke(ctx context.Context, id domain.APIKeyID) error { r.mu.Lock() defer r.mu.Unlock() key, ok := r.keys[id] if !ok { return domain.ErrKeyNotFound } if key.RevokedAt != nil { return domain.ErrKeyNotFound } now := time.Now() key.RevokedAt = &now return nil } // UpdateLastUsed updates the last used timestamp for a key. func (r *APIKeyRepository) UpdateLastUsed(ctx context.Context, id domain.APIKeyID) error { r.mu.Lock() defer r.mu.Unlock() key, ok := r.keys[id] if !ok { return domain.ErrKeyNotFound } now := time.Now() key.LastUsedAt = &now return nil } // Update applies a partial update to an API key. func (r *APIKeyRepository) Update(ctx context.Context, id domain.APIKeyID, update port.APIKeyUpdate) error { r.mu.Lock() defer r.mu.Unlock() key, ok := r.keys[id] if !ok || key.RevokedAt != nil { return domain.ErrKeyNotFound } if update.Name != nil { key.Name = *update.Name } if update.Scopes != nil { key.Scopes = update.Scopes } if update.ProjectIDs != nil { key.ProjectIDs = *update.ProjectIDs } if update.AllowedIPs != nil { key.AllowedIPs = *update.AllowedIPs } if update.ExpiresAt != nil { key.ExpiresAt = *update.ExpiresAt } return nil } // ListByProjectID returns all active keys that have the given project ID in their project_ids. func (r *APIKeyRepository) ListByProjectID(ctx context.Context, projectID domain.ProjectID) ([]*domain.APIKey, error) { r.mu.RLock() defer r.mu.RUnlock() var result []*domain.APIKey for _, key := range r.keys { if key.RevokedAt != nil { continue } for _, pid := range key.ProjectIDs { if pid == projectID { result = append(result, key) break } } } return result, nil } // itoa converts an integer to a string. func itoa(i int) string { if i == 0 { return "0" } var buf [20]byte pos := len(buf) negative := i < 0 if negative { i = -i } for i > 0 { pos-- buf[pos] = byte('0' + i%10) i /= 10 } if negative { pos-- buf[pos] = '-' } return string(buf[pos:]) }