package repository import ( "context" "errors" "strings" "time" "evobgp/internal/authkey" "evobgp/internal/store" "github.com/google/uuid" "github.com/jackc/pgx/v5" ) func (p *Postgres) ListAPIKeys(tenantID string) ([]*store.APIKey, error) { ctx := context.Background() rows, err := p.pool.Query(ctx, ` SELECT id::text, name, role, token_prefix, created_at, updated_at, expires_at, revoked_at, last_used_at FROM api_key WHERE tenant_id=$1 ORDER BY created_at DESC`, tenantID) if err != nil { return nil, err } defer rows.Close() var out []*store.APIKey for rows.Next() { k, err := scanAPIKeyRow(rows.Scan, tenantID) if err != nil { return nil, err } out = append(out, k) } return out, rows.Err() } func (p *Postgres) GetAPIKey(tenantID, id string) (*store.APIKey, error) { ctx := context.Background() row := p.pool.QueryRow(ctx, ` SELECT id::text, name, role, token_prefix, created_at, updated_at, expires_at, revoked_at, last_used_at FROM api_key WHERE id=$1 AND tenant_id=$2`, id, tenantID) k, err := scanAPIKeyRow(row.Scan, tenantID) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, store.ErrNotFound } return nil, err } return k, nil } func (p *Postgres) CreateAPIKey(tenantID string, in *store.APIKeyCreate) (*store.APIKeyWithSecret, error) { if in == nil || strings.TrimSpace(in.Name) == "" || !store.ValidAPIKeyRole(in.Role) { return nil, store.ErrInvalidInput } tok, err := authkey.GenerateToken() if err != nil { return nil, err } id := uuid.NewString() hash := authkey.HashToken(tok) prefix := authkey.Prefix(tok) role := strings.ToLower(strings.TrimSpace(in.Role)) ctx := context.Background() _, err = p.pool.Exec(ctx, ` INSERT INTO api_key (id, tenant_id, name, role, token_prefix, token_hash, expires_at) VALUES ($1,$2,$3,$4,$5,$6,$7)`, id, tenantID, strings.TrimSpace(in.Name), role, prefix, hash, in.ExpiresAt) if err != nil { return nil, err } k, err := p.GetAPIKey(tenantID, id) if err != nil { return nil, err } return &store.APIKeyWithSecret{APIKey: *k, Token: tok}, nil } func (p *Postgres) UpdateAPIKey(tenantID, id string, patch *store.APIKeyPatch) (*store.APIKey, error) { cur, err := p.GetAPIKey(tenantID, id) if err != nil { return nil, err } if cur.RevokedAt != nil { return nil, store.ErrInvalidInput } if patch == nil { return nil, store.ErrInvalidInput } if patch.Name != nil { n := strings.TrimSpace(*patch.Name) if n == "" { return nil, store.ErrInvalidInput } cur.Name = n } if patch.Role != nil { if !store.ValidAPIKeyRole(*patch.Role) { return nil, store.ErrInvalidInput } cur.Role = strings.ToLower(strings.TrimSpace(*patch.Role)) } if patch.ClearExpiresAt { cur.ExpiresAt = nil } else if patch.ExpiresAt != nil { cur.ExpiresAt = patch.ExpiresAt } ctx := context.Background() _, err = p.pool.Exec(ctx, ` UPDATE api_key SET name=$3, role=$4, expires_at=$5, updated_at=now() WHERE id=$1 AND tenant_id=$2 AND revoked_at IS NULL`, id, tenantID, cur.Name, cur.Role, cur.ExpiresAt) if err != nil { return nil, err } return p.GetAPIKey(tenantID, id) } func (p *Postgres) RevokeAPIKey(tenantID, id string) error { ctx := context.Background() tag, err := p.pool.Exec(ctx, ` UPDATE api_key SET revoked_at=now(), updated_at=now() WHERE id=$1 AND tenant_id=$2 AND revoked_at IS NULL`, id, tenantID) if err != nil { return err } if tag.RowsAffected() == 0 { return store.ErrNotFound } return nil } func (p *Postgres) RotateAPIKey(tenantID, id string) (*store.APIKeyWithSecret, error) { cur, err := p.GetAPIKey(tenantID, id) if err != nil { return nil, err } if cur.RevokedAt != nil { return nil, store.ErrInvalidInput } tok, err := authkey.GenerateToken() if err != nil { return nil, err } hash := authkey.HashToken(tok) prefix := authkey.Prefix(tok) ctx := context.Background() _, err = p.pool.Exec(ctx, ` UPDATE api_key SET token_hash=$3, token_prefix=$4, updated_at=now() WHERE id=$1 AND tenant_id=$2 AND revoked_at IS NULL`, id, tenantID, hash, prefix) if err != nil { return nil, err } k, err := p.GetAPIKey(tenantID, id) if err != nil { return nil, err } return &store.APIKeyWithSecret{APIKey: *k, Token: tok}, nil } func (p *Postgres) ListActiveAPIKeyHashes() ([]store.APIKeyAuthRow, error) { ctx := context.Background() rows, err := p.pool.Query(ctx, ` SELECT id::text, tenant_id::text, role, token_hash FROM api_key WHERE revoked_at IS NULL AND (expires_at IS NULL OR expires_at > now())`) if err != nil { return nil, err } defer rows.Close() var out []store.APIKeyAuthRow for rows.Next() { var row store.APIKeyAuthRow var hash []byte if err := rows.Scan(&row.ID, &row.TenantID, &row.Role, &hash); err != nil { return nil, err } row.TokenHash = append([]byte(nil), hash...) out = append(out, row) } return out, rows.Err() } func (p *Postgres) TouchAPIKeyLastUsed(id string) error { ctx := context.Background() _, err := p.pool.Exec(ctx, `UPDATE api_key SET last_used_at=now() WHERE id=$1`, id) return err } type scanFn func(dest ...any) error func scanAPIKeyRow(scan scanFn, tenantID string) (*store.APIKey, error) { var k store.APIKey k.TenantID = tenantID var expires, revoked, lastUsed *time.Time if err := scan(&k.ID, &k.Name, &k.Role, &k.Prefix, &k.CreatedAt, &k.UpdatedAt, &expires, &revoked, &lastUsed); err != nil { return nil, err } k.ExpiresAt = expires k.RevokedAt = revoked k.LastUsedAt = lastUsed return &k, nil }