Files
CxIDE/Services/GoDaddyService.swift
T
cx-git-agent 7e79fe89ca Add WebsiteService for static website generation and local preview
- Implemented WebsiteService to create and manage website projects from templates.
- Added functionality to start and stop a local HTTP server for project previews.
- Defined multiple built-in website templates (blank, landing page, portfolio, blog, documentation, dashboard) with corresponding HTML, CSS, and JS generation methods.
- Introduced WebsiteProject and WebsiteTemplate models to encapsulate project data and template details.
- Included error handling for project creation and template management.
2026-04-21 19:10:12 -05:00

314 lines
11 KiB
Swift

// GoDaddyService.swift
// CxIDE GoDaddy API integration for domain and DNS management
//
// Supports both OTE (test) and Production environments.
// API docs: https://developer.godaddy.com/doc
import Foundation
// MARK: - GoDaddy Service
final class GoDaddyService: @unchecked Sendable {
enum Environment: String, CaseIterable, Sendable {
case ote = "OTE (Test)"
case production = "Production"
var baseURL: String {
switch self {
case .ote: return "https://api.ote-godaddy.com"
case .production: return "https://api.godaddy.com"
}
}
}
private let credentialStore: CredentialStore
var environment: Environment
init(credentialStore: CredentialStore = .shared, environment: Environment = .ote) {
self.credentialStore = credentialStore
self.environment = environment
}
// MARK: - Auth Header
private var authHeader: String? {
let key: String?
let secret: String?
switch environment {
case .ote:
key = credentialStore.godaddyOTEKey
secret = credentialStore.godaddyOTESecret
case .production:
key = credentialStore.godaddyProdKey
secret = credentialStore.godaddyProdSecret
}
guard let k = key, let s = secret, !k.isEmpty, !s.isEmpty else { return nil }
return "sso-key \(k):\(s)"
}
// MARK: - Domain Operations
/// List all domains in the account
func listDomains() async throws -> [GoDaddyDomain] {
let data = try await request(path: "/v1/domains", method: "GET")
return try JSONDecoder().decode([GoDaddyDomain].self, from: data)
}
/// Get domain details
func getDomain(_ domain: String) async throws -> GoDaddyDomainDetail {
let data = try await request(path: "/v1/domains/\(domain)", method: "GET")
return try JSONDecoder().decode(GoDaddyDomainDetail.self, from: data)
}
/// Check domain availability
func checkAvailability(domain: String) async throws -> GoDaddyAvailability {
let data = try await request(path: "/v1/domains/available?domain=\(domain)", method: "GET")
return try JSONDecoder().decode(GoDaddyAvailability.self, from: data)
}
/// Get suggested domains
func suggestDomains(query: String, limit: Int = 10) async throws -> [GoDaddySuggestion] {
let encoded = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? query
let data = try await request(
path: "/v1/domains/suggest?query=\(encoded)&limit=\(limit)",
method: "GET"
)
return try JSONDecoder().decode([GoDaddySuggestion].self, from: data)
}
// MARK: - DNS Records
/// Get all DNS records for a domain
func getDNSRecords(domain: String) async throws -> [DNSRecord] {
let data = try await request(path: "/v1/domains/\(domain)/records", method: "GET")
return try JSONDecoder().decode([DNSRecord].self, from: data)
}
/// Get DNS records by type
func getDNSRecords(domain: String, type: DNSRecordType) async throws -> [DNSRecord] {
let data = try await request(
path: "/v1/domains/\(domain)/records/\(type.rawValue)",
method: "GET"
)
return try JSONDecoder().decode([DNSRecord].self, from: data)
}
/// Add DNS records
func addDNSRecords(domain: String, records: [DNSRecord]) async throws {
let body = try JSONEncoder().encode(records)
_ = try await request(path: "/v1/domains/\(domain)/records", method: "PATCH", body: body)
}
/// Replace all DNS records of a specific type and name
func replaceDNSRecord(domain: String, type: DNSRecordType, name: String, records: [DNSRecord]) async throws {
let body = try JSONEncoder().encode(records)
_ = try await request(
path: "/v1/domains/\(domain)/records/\(type.rawValue)/\(name)",
method: "PUT",
body: body
)
}
/// Delete all DNS records of a specific type and name
func deleteDNSRecord(domain: String, type: DNSRecordType, name: String) async throws {
_ = try await request(
path: "/v1/domains/\(domain)/records/\(type.rawValue)/\(name)",
method: "DELETE"
)
}
// MARK: - Convenience: Point domain to hosting
/// Point a domain's A record to an IP address (for hosting)
func pointDomainToIP(domain: String, ip: String, ttl: Int = 600) async throws {
let record = DNSRecord(type: .A, name: "@", data: ip, ttl: ttl)
try await replaceDNSRecord(domain: domain, type: .A, name: "@", records: [record])
}
/// Set a CNAME record (e.g., www -> hosting provider)
func setCNAME(domain: String, name: String, target: String, ttl: Int = 3600) async throws {
let record = DNSRecord(type: .CNAME, name: name, data: target, ttl: ttl)
try await replaceDNSRecord(domain: domain, type: .CNAME, name: name, records: [record])
}
/// Configure DNS for Netlify hosting
func configureForNetlify(domain: String, netlifySubdomain: String) async throws {
// Point @ to Netlify's load balancer
try await pointDomainToIP(domain: domain, ip: "75.2.60.5")
// Point www to Netlify subdomain
try await setCNAME(domain: domain, name: "www", target: "\(netlifySubdomain).netlify.app")
}
/// Configure DNS for GitHub Pages
func configureForGitHubPages(domain: String, githubUsername: String) async throws {
// GitHub Pages IPs
let githubIPs = ["185.199.108.153", "185.199.109.153", "185.199.110.153", "185.199.111.153"]
let records = githubIPs.map { DNSRecord(type: .A, name: "@", data: $0, ttl: 600) }
try await replaceDNSRecord(domain: domain, type: .A, name: "@", records: records)
try await setCNAME(domain: domain, name: "www", target: "\(githubUsername).github.io")
}
/// Configure DNS for Vercel hosting
func configureForVercel(domain: String) async throws {
try await pointDomainToIP(domain: domain, ip: "76.76.21.21")
try await setCNAME(domain: domain, name: "www", target: "cname.vercel-dns.com")
}
/// Configure DNS for Cloudflare Pages
func configureForCloudflare(domain: String, projectName: String) async throws {
try await setCNAME(domain: domain, name: "@", target: "\(projectName).pages.dev")
try await setCNAME(domain: domain, name: "www", target: "\(projectName).pages.dev")
}
// MARK: - HTTP Client
private func request(path: String, method: String, body: Data? = nil) async throws -> Data {
guard let auth = authHeader else {
throw GoDaddyError.notAuthenticated
}
let urlString = environment.baseURL + path
guard let url = URL(string: urlString) else {
throw GoDaddyError.invalidURL(urlString)
}
var req = URLRequest(url: url)
req.httpMethod = method
req.setValue(auth, forHTTPHeaderField: "Authorization")
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.setValue("application/json", forHTTPHeaderField: "Accept")
req.timeoutInterval = 30
if let body = body {
req.httpBody = body
}
let (data, response) = try await URLSession.shared.data(for: req)
guard let httpResponse = response as? HTTPURLResponse else {
throw GoDaddyError.invalidResponse
}
switch httpResponse.statusCode {
case 200...299:
return data
case 401:
throw GoDaddyError.unauthorized
case 403:
throw GoDaddyError.forbidden
case 404:
throw GoDaddyError.notFound(path)
case 422:
let msg = String(data: data, encoding: .utf8) ?? "Validation error"
throw GoDaddyError.validationError(msg)
case 429:
throw GoDaddyError.rateLimited
default:
let msg = String(data: data, encoding: .utf8) ?? "HTTP \(httpResponse.statusCode)"
throw GoDaddyError.apiError(httpResponse.statusCode, msg)
}
}
}
// MARK: - Models
struct GoDaddyDomain: Codable, Sendable {
let domain: String
let status: String?
let expires: String?
let expirationProtected: Bool?
let holdRegistrar: Bool?
let locked: Bool?
let privacy: Bool?
let renewAuto: Bool?
let renewable: Bool?
let transferProtected: Bool?
}
struct GoDaddyDomainDetail: Codable, Sendable {
let domain: String
let domainId: Int?
let status: String?
let expires: String?
let nameServers: [String]?
let locked: Bool?
let privacy: Bool?
let renewAuto: Bool?
let contactAdmin: GoDaddyContact?
let contactRegistrant: GoDaddyContact?
}
struct GoDaddyContact: Codable, Sendable {
let email: String?
let nameFirst: String?
let nameLast: String?
let organization: String?
let phone: String?
}
struct GoDaddyAvailability: Codable, Sendable {
let available: Bool
let domain: String
let definitive: Bool?
let price: Int?
let currency: String?
let period: Int?
}
struct GoDaddySuggestion: Codable, Sendable {
let domain: String
}
enum DNSRecordType: String, Codable, Sendable, CaseIterable {
case A, AAAA, CNAME, MX, NS, SOA, SRV, TXT, CAA
}
struct DNSRecord: Codable, Sendable {
let type: DNSRecordType
let name: String
let data: String
var ttl: Int?
var priority: Int?
var port: Int?
var weight: Int?
var service: String?
var `protocol`: String?
init(type: DNSRecordType, name: String, data: String, ttl: Int? = nil, priority: Int? = nil) {
self.type = type
self.name = name
self.data = data
self.ttl = ttl
self.priority = priority
}
}
// MARK: - Errors
enum GoDaddyError: LocalizedError {
case notAuthenticated
case unauthorized
case forbidden
case notFound(String)
case invalidURL(String)
case invalidResponse
case validationError(String)
case rateLimited
case apiError(Int, String)
var errorDescription: String? {
switch self {
case .notAuthenticated: return "GoDaddy API credentials not configured"
case .unauthorized: return "Invalid GoDaddy API credentials"
case .forbidden: return "Access forbidden — check API key permissions"
case .notFound(let path): return "Resource not found: \(path)"
case .invalidURL(let url): return "Invalid URL: \(url)"
case .invalidResponse: return "Invalid response from GoDaddy API"
case .validationError(let msg): return "Validation error: \(msg)"
case .rateLimited: return "GoDaddy API rate limit exceeded"
case .apiError(let code, let msg): return "GoDaddy API error (\(code)): \(msg)"
}
}
}