Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user