mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-05 02:41:41 +00:00
@@ -0,0 +1,299 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
v1 "github.com/sonr-io/sonr/api/svc/v1"
|
||||
)
|
||||
|
||||
// Domain verification constants
|
||||
const (
|
||||
// VerificationPrefix is the prefix for DNS TXT records
|
||||
VerificationPrefix = "sonr-verification="
|
||||
|
||||
// TokenLength is the length of the verification token in bytes
|
||||
TokenLength = 32
|
||||
|
||||
// VerificationExpiryHours is how long a verification token is valid
|
||||
VerificationExpiryHours = 24
|
||||
)
|
||||
|
||||
// InitiateDomainVerification creates a new domain verification request
|
||||
func (k Keeper) InitiateDomainVerification(
|
||||
ctx context.Context,
|
||||
domain, owner string,
|
||||
) (*v1.DomainVerification, error) {
|
||||
// Validate domain format
|
||||
if err := k.validateDomainFormat(domain); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "invalid domain format: %v", err)
|
||||
}
|
||||
|
||||
// Check if domain verification already exists and is not expired
|
||||
existing, err := k.OrmDB.DomainVerificationTable().Get(ctx, domain)
|
||||
if err == nil {
|
||||
// Domain verification exists, check if it's still valid
|
||||
if k.isDomainVerificationValid(existing) {
|
||||
return existing, status.Errorf(
|
||||
codes.AlreadyExists,
|
||||
"domain verification already exists and is valid",
|
||||
)
|
||||
}
|
||||
|
||||
// Expired verification exists, we'll update it
|
||||
}
|
||||
|
||||
// Generate a new verification token
|
||||
token, err := k.generateVerificationToken()
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to generate verification token: %v", err)
|
||||
}
|
||||
|
||||
// Create new domain verification record
|
||||
now := time.Now().Unix()
|
||||
verification := &v1.DomainVerification{
|
||||
Domain: domain,
|
||||
Owner: owner,
|
||||
VerificationToken: token,
|
||||
Status: v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_PENDING,
|
||||
ExpiresAt: now + (VerificationExpiryHours * 3600), // 24 hours from now
|
||||
VerifiedAt: 0,
|
||||
}
|
||||
|
||||
// Save or update the verification record
|
||||
if existing != nil {
|
||||
// Update existing record
|
||||
verification.Domain = existing.Domain // Ensure primary key consistency
|
||||
err = k.OrmDB.DomainVerificationTable().Update(ctx, verification)
|
||||
} else {
|
||||
// Insert new record
|
||||
err = k.OrmDB.DomainVerificationTable().Insert(ctx, verification)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to save domain verification: %v", err)
|
||||
}
|
||||
|
||||
return verification, nil
|
||||
}
|
||||
|
||||
// VerifyDomainOwnership validates domain ownership by checking DNS TXT records
|
||||
func (k Keeper) VerifyDomainOwnership(
|
||||
ctx context.Context,
|
||||
domain string,
|
||||
) (*v1.DomainVerification, error) {
|
||||
// Get the domain verification record
|
||||
verification, err := k.OrmDB.DomainVerificationTable().Get(ctx, domain)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.NotFound, "domain verification not found: %v", err)
|
||||
}
|
||||
|
||||
// Check if verification has expired
|
||||
if k.isDomainVerificationExpired(verification) {
|
||||
verification.Status = v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_EXPIRED
|
||||
k.OrmDB.DomainVerificationTable().Update(ctx, verification)
|
||||
return verification, status.Errorf(
|
||||
codes.DeadlineExceeded,
|
||||
"domain verification has expired",
|
||||
)
|
||||
}
|
||||
|
||||
// Check if already verified
|
||||
if verification.Status == v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_VERIFIED {
|
||||
return verification, nil
|
||||
}
|
||||
|
||||
// Perform DNS TXT record lookup
|
||||
verified, err := k.checkDNSTXTRecord(domain, verification.VerificationToken)
|
||||
if err != nil {
|
||||
verification.Status = v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_FAILED
|
||||
k.OrmDB.DomainVerificationTable().Update(ctx, verification)
|
||||
return verification, status.Errorf(
|
||||
codes.FailedPrecondition,
|
||||
"DNS verification failed: %v",
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
if verified {
|
||||
// Mark as verified
|
||||
verification.Status = v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_VERIFIED
|
||||
verification.VerifiedAt = time.Now().Unix()
|
||||
} else {
|
||||
// Verification record not found
|
||||
verification.Status = v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_FAILED
|
||||
}
|
||||
|
||||
// Update the verification record
|
||||
err = k.OrmDB.DomainVerificationTable().Update(ctx, verification)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to update domain verification: %v", err)
|
||||
}
|
||||
|
||||
if !verified {
|
||||
return verification, status.Errorf(
|
||||
codes.FailedPrecondition,
|
||||
"verification record not found in DNS",
|
||||
)
|
||||
}
|
||||
|
||||
return verification, nil
|
||||
}
|
||||
|
||||
// GetDomainVerification retrieves a domain verification record
|
||||
func (k Keeper) GetDomainVerification(
|
||||
ctx context.Context,
|
||||
domain string,
|
||||
) (*v1.DomainVerification, error) {
|
||||
verification, err := k.OrmDB.DomainVerificationTable().Get(ctx, domain)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.NotFound, "domain verification not found: %v", err)
|
||||
}
|
||||
return verification, nil
|
||||
}
|
||||
|
||||
// ListDomainVerificationsByOwner returns all domain verifications for a given owner
|
||||
func (k Keeper) ListDomainVerificationsByOwner(
|
||||
ctx context.Context,
|
||||
owner string,
|
||||
) ([]*v1.DomainVerification, error) {
|
||||
ownerKey := v1.DomainVerificationOwnerIndexKey{}.WithOwner(owner)
|
||||
iter, err := k.OrmDB.DomainVerificationTable().List(ctx, ownerKey)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to list domain verifications: %v", err)
|
||||
}
|
||||
defer iter.Close()
|
||||
|
||||
var verifications []*v1.DomainVerification
|
||||
for iter.Next() {
|
||||
verification, err := iter.Value()
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to read domain verification: %v", err)
|
||||
}
|
||||
verifications = append(verifications, verification)
|
||||
}
|
||||
|
||||
return verifications, nil
|
||||
}
|
||||
|
||||
// IsVerifiedDomain checks if a domain is verified and not expired
|
||||
func (k Keeper) IsVerifiedDomain(ctx context.Context, domain string) bool {
|
||||
verification, err := k.OrmDB.DomainVerificationTable().Get(ctx, domain)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return verification.Status == v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_VERIFIED &&
|
||||
!k.isDomainVerificationExpired(verification)
|
||||
}
|
||||
|
||||
// generateVerificationToken creates a cryptographically secure random token
|
||||
func (k Keeper) generateVerificationToken() (string, error) {
|
||||
bytes := make([]byte, TokenLength)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return "", fmt.Errorf("failed to generate random bytes: %w", err)
|
||||
}
|
||||
return hex.EncodeToString(bytes), nil
|
||||
}
|
||||
|
||||
// checkDNSTXTRecord performs DNS TXT record lookup and validation
|
||||
func (k Keeper) checkDNSTXTRecord(domain, expectedToken string) (bool, error) {
|
||||
// Expected TXT record format: "sonr-verification=<token>"
|
||||
expectedRecord := VerificationPrefix + expectedToken
|
||||
|
||||
// Perform DNS TXT lookup
|
||||
txtRecords, err := net.LookupTXT(domain)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("DNS lookup failed: %w", err)
|
||||
}
|
||||
|
||||
// Check if any TXT record matches our expected verification record
|
||||
for _, record := range txtRecords {
|
||||
if strings.TrimSpace(record) == expectedRecord {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// validateDomainFormat validates that a domain name is properly formatted
|
||||
func (k Keeper) validateDomainFormat(domain string) error {
|
||||
if domain == "" {
|
||||
return fmt.Errorf("domain cannot be empty")
|
||||
}
|
||||
|
||||
// Basic domain validation - check for valid characters and format
|
||||
if len(domain) > 253 {
|
||||
return fmt.Errorf("domain name too long")
|
||||
}
|
||||
|
||||
// Check for valid domain format (basic validation)
|
||||
if !strings.Contains(domain, ".") {
|
||||
return fmt.Errorf("domain must contain at least one dot")
|
||||
}
|
||||
|
||||
// Check for invalid characters
|
||||
for _, char := range domain {
|
||||
if !((char >= 'a' && char <= 'z') ||
|
||||
(char >= 'A' && char <= 'Z') ||
|
||||
(char >= '0' && char <= '9') ||
|
||||
char == '.' || char == '-') {
|
||||
return fmt.Errorf("domain contains invalid character: %c", char)
|
||||
}
|
||||
}
|
||||
|
||||
// Domain cannot start or end with a hyphen
|
||||
if strings.HasPrefix(domain, "-") || strings.HasSuffix(domain, "-") {
|
||||
return fmt.Errorf("domain cannot start or end with hyphen")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isDomainVerificationValid checks if a domain verification is still valid (not expired)
|
||||
func (k Keeper) isDomainVerificationValid(verification *v1.DomainVerification) bool {
|
||||
if verification.Status == v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_VERIFIED {
|
||||
return true // Verified domains don't expire
|
||||
}
|
||||
|
||||
return !k.isDomainVerificationExpired(verification)
|
||||
}
|
||||
|
||||
// isDomainVerificationExpired checks if a domain verification has expired
|
||||
func (k Keeper) isDomainVerificationExpired(verification *v1.DomainVerification) bool {
|
||||
now := time.Now().Unix()
|
||||
return now > verification.ExpiresAt
|
||||
}
|
||||
|
||||
// GetDNSInstructions returns human-readable instructions for setting up DNS verification
|
||||
func (k Keeper) GetDNSInstructions(domain, token string) string {
|
||||
return fmt.Sprintf(
|
||||
"Add the following TXT record to your DNS configuration for domain '%s':\n\n"+
|
||||
"Name: %s\n"+
|
||||
"Type: TXT\n"+
|
||||
"Value: %s%s\n\n"+
|
||||
"Note: DNS propagation may take up to 48 hours. You can verify the record using:\n"+
|
||||
"dig TXT %s",
|
||||
domain, domain, VerificationPrefix, token, domain,
|
||||
)
|
||||
}
|
||||
|
||||
// SetDomainVerified is a helper method for testing to mark a domain as verified
|
||||
func (k Keeper) SetDomainVerified(ctx context.Context, domain string) error {
|
||||
verification, err := k.OrmDB.DomainVerificationTable().Get(ctx, domain)
|
||||
if err != nil {
|
||||
return fmt.Errorf("domain verification not found: %w", err)
|
||||
}
|
||||
|
||||
verification.Status = v1.DomainVerificationStatus_DOMAIN_VERIFICATION_STATUS_VERIFIED
|
||||
return k.OrmDB.DomainVerificationTable().Update(ctx, verification)
|
||||
}
|
||||
Reference in New Issue
Block a user