mirror of
https://github.com/DeBrosOfficial/network.git
synced 2026-01-30 11:33:04 +00:00
462 lines
12 KiB
Go
462 lines
12 KiB
Go
package deployments
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/DeBrosOfficial/network/pkg/deployments"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// DomainHandler handles custom domain management
|
|
type DomainHandler struct {
|
|
service *DeploymentService
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// NewDomainHandler creates a new domain handler
|
|
func NewDomainHandler(service *DeploymentService, logger *zap.Logger) *DomainHandler {
|
|
return &DomainHandler{
|
|
service: service,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// HandleAddDomain adds a custom domain to a deployment
|
|
func (h *DomainHandler) HandleAddDomain(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
namespace := ctx.Value("namespace").(string)
|
|
|
|
var req struct {
|
|
DeploymentName string `json:"deployment_name"`
|
|
Domain string `json:"domain"`
|
|
}
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if req.DeploymentName == "" || req.Domain == "" {
|
|
http.Error(w, "deployment_name and domain are required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Normalize domain
|
|
domain := strings.ToLower(strings.TrimSpace(req.Domain))
|
|
domain = strings.TrimPrefix(domain, "http://")
|
|
domain = strings.TrimPrefix(domain, "https://")
|
|
domain = strings.TrimSuffix(domain, "/")
|
|
|
|
// Validate domain format
|
|
if !isValidDomain(domain) {
|
|
http.Error(w, "Invalid domain format", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Check if domain is reserved
|
|
if strings.HasSuffix(domain, ".debros.network") {
|
|
http.Error(w, "Cannot use .debros.network domains as custom domains", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
h.logger.Info("Adding custom domain",
|
|
zap.String("namespace", namespace),
|
|
zap.String("deployment", req.DeploymentName),
|
|
zap.String("domain", domain),
|
|
)
|
|
|
|
// Get deployment
|
|
deployment, err := h.service.GetDeployment(ctx, namespace, req.DeploymentName)
|
|
if err != nil {
|
|
if err == deployments.ErrDeploymentNotFound {
|
|
http.Error(w, "Deployment not found", http.StatusNotFound)
|
|
} else {
|
|
http.Error(w, "Failed to get deployment", http.StatusInternalServerError)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Generate verification token
|
|
token := generateVerificationToken()
|
|
|
|
// Check if domain already exists
|
|
var existingCount int
|
|
checkQuery := `SELECT COUNT(*) FROM deployment_domains WHERE domain = ?`
|
|
var counts []struct {
|
|
Count int `db:"count"`
|
|
}
|
|
err = h.service.db.Query(ctx, &counts, checkQuery, domain)
|
|
if err == nil && len(counts) > 0 {
|
|
existingCount = counts[0].Count
|
|
}
|
|
|
|
if existingCount > 0 {
|
|
http.Error(w, "Domain already in use", http.StatusConflict)
|
|
return
|
|
}
|
|
|
|
// Insert domain record
|
|
query := `
|
|
INSERT INTO deployment_domains (deployment_id, domain, verification_token, verification_status, created_at)
|
|
VALUES (?, ?, ?, 'pending', ?)
|
|
`
|
|
|
|
_, err = h.service.db.Exec(ctx, query, deployment.ID, domain, token, time.Now())
|
|
if err != nil {
|
|
h.logger.Error("Failed to insert domain", zap.Error(err))
|
|
http.Error(w, "Failed to add domain", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
h.logger.Info("Custom domain added, awaiting verification",
|
|
zap.String("domain", domain),
|
|
zap.String("deployment", deployment.Name),
|
|
)
|
|
|
|
// Return verification instructions
|
|
resp := map[string]interface{}{
|
|
"deployment_name": deployment.Name,
|
|
"domain": domain,
|
|
"verification_token": token,
|
|
"status": "pending",
|
|
"instructions": map[string]string{
|
|
"step_1": "Add a TXT record to your DNS:",
|
|
"record": fmt.Sprintf("_orama-verify.%s", domain),
|
|
"value": token,
|
|
"step_2": "Once added, call POST /v1/deployments/domains/verify with the domain",
|
|
"step_3": "After verification, point your domain's A record to your deployment's node IP",
|
|
},
|
|
"created_at": time.Now(),
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
json.NewEncoder(w).Encode(resp)
|
|
}
|
|
|
|
// HandleVerifyDomain verifies domain ownership via TXT record
|
|
func (h *DomainHandler) HandleVerifyDomain(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
namespace := ctx.Value("namespace").(string)
|
|
|
|
var req struct {
|
|
Domain string `json:"domain"`
|
|
}
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
domain := strings.ToLower(strings.TrimSpace(req.Domain))
|
|
|
|
h.logger.Info("Verifying domain",
|
|
zap.String("namespace", namespace),
|
|
zap.String("domain", domain),
|
|
)
|
|
|
|
// Get domain record
|
|
type domainRow struct {
|
|
DeploymentID string `db:"deployment_id"`
|
|
VerificationToken string `db:"verification_token"`
|
|
VerificationStatus string `db:"verification_status"`
|
|
}
|
|
|
|
var rows []domainRow
|
|
query := `
|
|
SELECT dd.deployment_id, dd.verification_token, dd.verification_status
|
|
FROM deployment_domains dd
|
|
JOIN deployments d ON dd.deployment_id = d.id
|
|
WHERE dd.domain = ? AND d.namespace = ?
|
|
`
|
|
|
|
err := h.service.db.Query(ctx, &rows, query, domain, namespace)
|
|
if err != nil || len(rows) == 0 {
|
|
http.Error(w, "Domain not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
domainRecord := rows[0]
|
|
|
|
if domainRecord.VerificationStatus == "verified" {
|
|
resp := map[string]interface{}{
|
|
"domain": domain,
|
|
"status": "verified",
|
|
"message": "Domain already verified",
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(resp)
|
|
return
|
|
}
|
|
|
|
// Verify TXT record
|
|
txtRecord := fmt.Sprintf("_orama-verify.%s", domain)
|
|
verified := h.verifyTXTRecord(txtRecord, domainRecord.VerificationToken)
|
|
|
|
if !verified {
|
|
http.Error(w, "Verification failed: TXT record not found or doesn't match", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Update status
|
|
updateQuery := `
|
|
UPDATE deployment_domains
|
|
SET verification_status = 'verified', verified_at = ?
|
|
WHERE domain = ?
|
|
`
|
|
|
|
_, err = h.service.db.Exec(ctx, updateQuery, time.Now(), domain)
|
|
if err != nil {
|
|
h.logger.Error("Failed to update verification status", zap.Error(err))
|
|
http.Error(w, "Failed to update verification status", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Create DNS record for the domain
|
|
go h.createDNSRecord(ctx, domain, domainRecord.DeploymentID)
|
|
|
|
h.logger.Info("Domain verified successfully",
|
|
zap.String("domain", domain),
|
|
)
|
|
|
|
resp := map[string]interface{}{
|
|
"domain": domain,
|
|
"status": "verified",
|
|
"message": "Domain verified successfully",
|
|
"verified_at": time.Now(),
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(resp)
|
|
}
|
|
|
|
// HandleListDomains lists all domains for a deployment
|
|
func (h *DomainHandler) HandleListDomains(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
namespace := ctx.Value("namespace").(string)
|
|
deploymentName := r.URL.Query().Get("deployment_name")
|
|
|
|
if deploymentName == "" {
|
|
http.Error(w, "deployment_name query parameter is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Get deployment
|
|
deployment, err := h.service.GetDeployment(ctx, namespace, deploymentName)
|
|
if err != nil {
|
|
http.Error(w, "Deployment not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
// Query domains
|
|
type domainRow struct {
|
|
Domain string `db:"domain"`
|
|
VerificationStatus string `db:"verification_status"`
|
|
CreatedAt time.Time `db:"created_at"`
|
|
VerifiedAt *time.Time `db:"verified_at"`
|
|
}
|
|
|
|
var rows []domainRow
|
|
query := `
|
|
SELECT domain, verification_status, created_at, verified_at
|
|
FROM deployment_domains
|
|
WHERE deployment_id = ?
|
|
ORDER BY created_at DESC
|
|
`
|
|
|
|
err = h.service.db.Query(ctx, &rows, query, deployment.ID)
|
|
if err != nil {
|
|
h.logger.Error("Failed to query domains", zap.Error(err))
|
|
http.Error(w, "Failed to query domains", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
domains := make([]map[string]interface{}, len(rows))
|
|
for i, row := range rows {
|
|
domains[i] = map[string]interface{}{
|
|
"domain": row.Domain,
|
|
"verification_status": row.VerificationStatus,
|
|
"created_at": row.CreatedAt,
|
|
}
|
|
if row.VerifiedAt != nil {
|
|
domains[i]["verified_at"] = row.VerifiedAt
|
|
}
|
|
}
|
|
|
|
resp := map[string]interface{}{
|
|
"deployment_name": deploymentName,
|
|
"domains": domains,
|
|
"total": len(domains),
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(resp)
|
|
}
|
|
|
|
// HandleRemoveDomain removes a custom domain
|
|
func (h *DomainHandler) HandleRemoveDomain(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
namespace := ctx.Value("namespace").(string)
|
|
domain := r.URL.Query().Get("domain")
|
|
|
|
if domain == "" {
|
|
http.Error(w, "domain query parameter is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
domain = strings.ToLower(strings.TrimSpace(domain))
|
|
|
|
h.logger.Info("Removing domain",
|
|
zap.String("namespace", namespace),
|
|
zap.String("domain", domain),
|
|
)
|
|
|
|
// Verify ownership
|
|
var deploymentID string
|
|
checkQuery := `
|
|
SELECT dd.deployment_id
|
|
FROM deployment_domains dd
|
|
JOIN deployments d ON dd.deployment_id = d.id
|
|
WHERE dd.domain = ? AND d.namespace = ?
|
|
`
|
|
|
|
type idRow struct {
|
|
DeploymentID string `db:"deployment_id"`
|
|
}
|
|
var rows []idRow
|
|
err := h.service.db.Query(ctx, &rows, checkQuery, domain, namespace)
|
|
if err != nil || len(rows) == 0 {
|
|
http.Error(w, "Domain not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
deploymentID = rows[0].DeploymentID
|
|
|
|
// Delete domain
|
|
deleteQuery := `DELETE FROM deployment_domains WHERE domain = ?`
|
|
_, err = h.service.db.Exec(ctx, deleteQuery, domain)
|
|
if err != nil {
|
|
h.logger.Error("Failed to delete domain", zap.Error(err))
|
|
http.Error(w, "Failed to delete domain", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Delete DNS record
|
|
dnsQuery := `DELETE FROM dns_records WHERE fqdn = ? AND deployment_id = ?`
|
|
h.service.db.Exec(ctx, dnsQuery, domain+".", deploymentID)
|
|
|
|
h.logger.Info("Domain removed",
|
|
zap.String("domain", domain),
|
|
)
|
|
|
|
resp := map[string]interface{}{
|
|
"message": "Domain removed successfully",
|
|
"domain": domain,
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(resp)
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
func generateVerificationToken() string {
|
|
bytes := make([]byte, 16)
|
|
rand.Read(bytes)
|
|
return "orama-verify-" + hex.EncodeToString(bytes)
|
|
}
|
|
|
|
func isValidDomain(domain string) bool {
|
|
// Basic domain validation
|
|
if len(domain) == 0 || len(domain) > 253 {
|
|
return false
|
|
}
|
|
if strings.Contains(domain, "..") || strings.HasPrefix(domain, ".") || strings.HasSuffix(domain, ".") {
|
|
return false
|
|
}
|
|
parts := strings.Split(domain, ".")
|
|
if len(parts) < 2 {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (h *DomainHandler) verifyTXTRecord(record, expectedValue string) bool {
|
|
txtRecords, err := net.LookupTXT(record)
|
|
if err != nil {
|
|
h.logger.Warn("Failed to lookup TXT record",
|
|
zap.String("record", record),
|
|
zap.Error(err),
|
|
)
|
|
return false
|
|
}
|
|
|
|
for _, txt := range txtRecords {
|
|
if txt == expectedValue {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (h *DomainHandler) createDNSRecord(ctx context.Context, domain, deploymentID string) {
|
|
// Get deployment node IP
|
|
type deploymentRow struct {
|
|
HomeNodeID string `db:"home_node_id"`
|
|
}
|
|
|
|
var rows []deploymentRow
|
|
query := `SELECT home_node_id FROM deployments WHERE id = ?`
|
|
err := h.service.db.Query(ctx, &rows, query, deploymentID)
|
|
if err != nil || len(rows) == 0 {
|
|
h.logger.Error("Failed to get deployment node", zap.Error(err))
|
|
return
|
|
}
|
|
|
|
homeNodeID := rows[0].HomeNodeID
|
|
|
|
// Get node IP
|
|
type nodeRow struct {
|
|
IPAddress string `db:"ip_address"`
|
|
}
|
|
|
|
var nodeRows []nodeRow
|
|
nodeQuery := `SELECT ip_address FROM dns_nodes WHERE id = ? AND status = 'active'`
|
|
err = h.service.db.Query(ctx, &nodeRows, nodeQuery, homeNodeID)
|
|
if err != nil || len(nodeRows) == 0 {
|
|
h.logger.Error("Failed to get node IP", zap.Error(err))
|
|
return
|
|
}
|
|
|
|
nodeIP := nodeRows[0].IPAddress
|
|
|
|
// Create DNS A record
|
|
dnsQuery := `
|
|
INSERT INTO dns_records (fqdn, record_type, value, ttl, namespace, deployment_id, node_id, created_by, created_at)
|
|
VALUES (?, 'A', ?, 300, ?, ?, ?, 'system', ?)
|
|
ON CONFLICT(fqdn) DO UPDATE SET value = ?, updated_at = ?
|
|
`
|
|
|
|
fqdn := domain + "."
|
|
now := time.Now()
|
|
|
|
_, err = h.service.db.Exec(ctx, dnsQuery, fqdn, nodeIP, "", deploymentID, homeNodeID, now, nodeIP, now)
|
|
if err != nil {
|
|
h.logger.Error("Failed to create DNS record", zap.Error(err))
|
|
return
|
|
}
|
|
|
|
h.logger.Info("DNS record created for custom domain",
|
|
zap.String("domain", domain),
|
|
zap.String("ip", nodeIP),
|
|
)
|
|
}
|