7e79fe89ca
- 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.
314 lines
11 KiB
Swift
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)"
|
|
}
|
|
}
|
|
}
|