Files
descrybe/apps/api/internal/auth/apikey.go
T

64 lines
2.1 KiB
Go
Raw Normal View History

package auth
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"strings"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
)
var ErrInvalidAPIKey = errors.New("invalid api key")
// APIKeyIdentity is the tenant binding resolved from a valid API key.
type APIKeyIdentity struct {
KeyID uuid.UUID
CompanyID uuid.UUID
UserID uuid.UUID
MembershipRole string // active membership role for the key owner (admin|member)
}
// HashAPIKey returns a SHA-256 hex digest for O(1) api_keys.key_hash lookup.
// Matches hashes written by dashboard key creation. Also used for invite tokens at rest.
func HashAPIKey(raw string) string {
sum := sha256.Sum256([]byte(raw))
return hex.EncodeToString(sum[:])
}
// AuthenticateAPIKey looks up a non-revoked key by hash and updates last_used_at.
// Keys owned by inactive users or without an active company membership are rejected.
// MembershipRole is returned so HTTP middleware can withhold company-admin powers
// when the owner is no longer an admin (keys are admin-created; scopes/expiry columns
// do not exist yet — empty/full privilege remains the default for admin-owned keys).
func (s *Service) AuthenticateAPIKey(ctx context.Context, raw string) (APIKeyIdentity, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return APIKeyIdentity{}, ErrInvalidAPIKey
}
hash := HashAPIKey(raw)
var id APIKeyIdentity
var role string
err := s.Pool.QueryRow(ctx, `
SELECT k.id, k.company_id, k.user_id, m.role
FROM api_keys k
INNER JOIN users u ON u.id = k.user_id AND u.is_active = true
INNER JOIN memberships m ON m.user_id = k.user_id
AND m.company_id = k.company_id
AND m.status = 'active'
WHERE k.key_hash = $1 AND k.revoked_at IS NULL`, hash).
Scan(&id.KeyID, &id.CompanyID, &id.UserID, &role)
if errors.Is(err, pgx.ErrNoRows) {
return APIKeyIdentity{}, ErrInvalidAPIKey
}
if err != nil {
return APIKeyIdentity{}, err
}
id.MembershipRole = NormalizeMembershipRole(role)
_, _ = s.Pool.Exec(ctx, `
UPDATE api_keys SET last_used_at = now(), updated_at = now() WHERE id = $1`, id.KeyID)
return id, nil
}