wip: Windows desktop app scaffold (WinUI 3/.NET 8) + NSIS installer + docs
Owner decision pending (STATE.md DC-100 tick): adopt/ship/park. Preserved from fragile git stash to named branch 2026-08-23. NOTE: requires a Windows build machine (WinUI XAML compiler + MSIX do not cross-build on Linux) — see WINDOWS_APP_BUILD.md in this tree. Secret-scanned clean 2026-08-23 (no keys/tokens/PEM in tree).
This commit is contained in:
@@ -0,0 +1,382 @@
|
||||
using System.Net.Http.Json;
|
||||
using DashCaddy.Desktop.Models;
|
||||
|
||||
namespace DashCaddy.Desktop.Services;
|
||||
|
||||
public enum DnsProviderType
|
||||
{
|
||||
None, // No DNS management - use existing DNS
|
||||
Technitium,
|
||||
CoreDNS,
|
||||
Cloudflare,
|
||||
Route53,
|
||||
Custom
|
||||
}
|
||||
|
||||
public class DnsProviderConfig
|
||||
{
|
||||
public DnsProviderType Type { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string BaseUrl { get; set; }
|
||||
public string ApiKey { get; set; }
|
||||
public string ZoneId { get; set; }
|
||||
public Dictionary<string, string> Extra { get; set; } = new();
|
||||
}
|
||||
|
||||
public class DnsClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly DnsProviderConfig _config;
|
||||
|
||||
public DnsProviderType ProviderType => _config?.Type ?? DnsProviderType.None;
|
||||
|
||||
public DnsClient(DnsProviderConfig config)
|
||||
{
|
||||
_config = config;
|
||||
if (config?.Type != DnsProviderType.None)
|
||||
{
|
||||
_httpClient = new HttpClient { BaseAddress = new Uri(config.BaseUrl) };
|
||||
if (!string.IsNullOrEmpty(config.ApiKey))
|
||||
{
|
||||
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {config.ApiKey}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static DnsClient Create(DnsProviderType type, Dictionary<string, string> settings)
|
||||
{
|
||||
if (type == DnsProviderType.None)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var config = type switch
|
||||
{
|
||||
DnsProviderType.Technitium => new DnsProviderConfig
|
||||
{
|
||||
Type = DnsProviderType.Technitium,
|
||||
Name = "Technitium DNS",
|
||||
BaseUrl = settings.GetValueOrDefault("technitium_url", "http://localhost:5380/api"),
|
||||
ApiKey = settings.GetValueOrDefault("technitium_api_key", ""),
|
||||
ZoneId = settings.GetValueOrDefault("technitium_zone", "")
|
||||
},
|
||||
DnsProviderType.CoreDNS => new DnsProviderConfig
|
||||
{
|
||||
Type = DnsProviderType.CoreDNS,
|
||||
Name = "CoreDNS",
|
||||
BaseUrl = settings.GetValueOrDefault("coredns_api_url", "http://localhost:8080"),
|
||||
ApiKey = "",
|
||||
ZoneId = settings.GetValueOrDefault("coredns_zone", "local")
|
||||
},
|
||||
DnsProviderType.Cloudflare => new DnsProviderConfig
|
||||
{
|
||||
Type = DnsProviderType.Cloudflare,
|
||||
Name = "Cloudflare",
|
||||
BaseUrl = "https://api.cloudflare.com/client/v4",
|
||||
ApiKey = settings.GetValueOrDefault("cloudflare_api_token", ""),
|
||||
ZoneId = settings.GetValueOrDefault("cloudflare_zone_id", "")
|
||||
},
|
||||
DnsProviderType.Route53 => new DnsProviderConfig
|
||||
{
|
||||
Type = DnsProviderType.Route53,
|
||||
Name = "AWS Route 53",
|
||||
BaseUrl = "https://route53.amazonaws.com",
|
||||
ApiKey = settings.GetValueOrDefault("aws_access_key", ""),
|
||||
Extra = new Dictionary<string, string>
|
||||
{
|
||||
["secret_key"] = settings.GetValueOrDefault("aws_secret_key", ""),
|
||||
["region"] = settings.GetValueOrDefault("aws_region", "us-east-1")
|
||||
}
|
||||
},
|
||||
_ => new DnsProviderConfig
|
||||
{
|
||||
Type = DnsProviderType.Custom,
|
||||
Name = "Custom DNS",
|
||||
BaseUrl = settings.GetValueOrDefault("custom_dns_url", ""),
|
||||
ApiKey = settings.GetValueOrDefault("custom_dns_key", "")
|
||||
}
|
||||
};
|
||||
|
||||
return new DnsClient(config);
|
||||
}
|
||||
|
||||
public async Task<List<DnsRecord>> GetRecordsAsync(string zone = null)
|
||||
{
|
||||
if (_config?.Type == DnsProviderType.None) return new List<DnsRecord>();
|
||||
|
||||
zone ??= _config?.ZoneId;
|
||||
if (string.IsNullOrEmpty(zone)) return new List<DnsRecord>();
|
||||
|
||||
return _config.Type switch
|
||||
{
|
||||
DnsProviderType.Technitium => await GetTechnitiumRecordsAsync(zone),
|
||||
DnsProviderType.CoreDNS => await GetCoreDnsRecordsAsync(zone),
|
||||
DnsProviderType.Cloudflare => await GetCloudflareRecordsAsync(zone),
|
||||
DnsProviderType.Route53 => await GetRoute53RecordsAsync(zone),
|
||||
_ => new List<DnsRecord>()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<DnsRecord> CreateRecordAsync(string zone, DnsRecord record)
|
||||
{
|
||||
if (_config?.Type == DnsProviderType.None) return record;
|
||||
|
||||
return _config.Type switch
|
||||
{
|
||||
DnsProviderType.Technitium => await CreateTechnitiumRecordAsync(zone, record),
|
||||
DnsProviderType.CoreDNS => await CreateCoreDnsRecordAsync(zone, record),
|
||||
DnsProviderType.Cloudflare => await CreateCloudflareRecordAsync(zone, record),
|
||||
DnsProviderType.Route53 => await CreateRoute53RecordAsync(zone, record),
|
||||
_ => record
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<DnsRecord> UpdateRecordAsync(string zone, string recordId, DnsRecord record)
|
||||
{
|
||||
if (_config?.Type == DnsProviderType.None) return record;
|
||||
|
||||
return _config.Type switch
|
||||
{
|
||||
DnsProviderType.Technitium => await UpdateTechnitiumRecordAsync(zone, recordId, record),
|
||||
DnsProviderType.CoreDNS => await UpdateCoreDnsRecordAsync(zone, recordId, record),
|
||||
DnsProviderType.Cloudflare => await UpdateCloudflareRecordAsync(zone, recordId, record),
|
||||
DnsProviderType.Route53 => await UpdateRoute53RecordAsync(zone, recordId, record),
|
||||
_ => record
|
||||
};
|
||||
}
|
||||
|
||||
public async Task DeleteRecordAsync(string zone, string recordId)
|
||||
{
|
||||
if (_config?.Type == DnsProviderType.None) return;
|
||||
|
||||
switch (_config.Type)
|
||||
{
|
||||
case DnsProviderType.Technitium:
|
||||
await DeleteTechnitiumRecordAsync(zone, recordId);
|
||||
break;
|
||||
case DnsProviderType.CoreDNS:
|
||||
await DeleteCoreDnsRecordAsync(zone, recordId);
|
||||
break;
|
||||
case DnsProviderType.Cloudflare:
|
||||
await DeleteCloudflareRecordAsync(zone, recordId);
|
||||
break;
|
||||
case DnsProviderType.Route53:
|
||||
await DeleteRoute53RecordAsync(zone, recordId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Technitium ───
|
||||
private async Task<List<DnsRecord>> GetTechnitiumRecordsAsync(string zone)
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"/zones/{zone}/records");
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<TechnitiumZoneResponse>();
|
||||
return result?.Records?.Select(r => new DnsRecord
|
||||
{
|
||||
Id = r.Id,
|
||||
Name = r.Name,
|
||||
Type = r.Type,
|
||||
Content = r.Value,
|
||||
Ttl = r.Ttl
|
||||
}).ToList() ?? new List<DnsRecord>();
|
||||
}
|
||||
|
||||
private async Task<DnsRecord> CreateTechnitiumRecordAsync(string zone, DnsRecord record)
|
||||
{
|
||||
var req = new TechnitiumRecordRequest
|
||||
{
|
||||
Name = record.Name,
|
||||
Type = record.Type,
|
||||
Value = record.Content,
|
||||
Ttl = record.Ttl
|
||||
};
|
||||
var response = await _httpClient.PostAsJsonAsync($"/zones/{zone}/records", req);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<TechnitiumRecordResponse>();
|
||||
record.Id = result?.Record?.Id;
|
||||
return record;
|
||||
}
|
||||
|
||||
private async Task<DnsRecord> UpdateTechnitiumRecordAsync(string zone, string recordId, DnsRecord record)
|
||||
{
|
||||
var req = new TechnitiumRecordRequest
|
||||
{
|
||||
Name = record.Name,
|
||||
Type = record.Type,
|
||||
Value = record.Content,
|
||||
Ttl = record.Ttl
|
||||
};
|
||||
var response = await _httpClient.PutAsJsonAsync($"/zones/{zone}/records/{recordId}", req);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return record;
|
||||
}
|
||||
|
||||
private async Task DeleteTechnitiumRecordAsync(string zone, string recordId)
|
||||
{
|
||||
var response = await _httpClient.DeleteAsync($"/zones/{zone}/records/{recordId}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
// ─── CoreDNS (via API wrapper) ───
|
||||
private async Task<List<DnsRecord>> GetCoreDnsRecordsAsync(string zone)
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"/api/v1/dns/zones/{zone}/records");
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<ApiResponse<List<DnsRecord>>>();
|
||||
return result?.Data ?? new List<DnsRecord>();
|
||||
}
|
||||
|
||||
private async Task<DnsRecord> CreateCoreDnsRecordAsync(string zone, DnsRecord record)
|
||||
{
|
||||
var response = await _httpClient.PostAsJsonAsync($"/api/v1/dns/zones/{zone}/records", record);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<ApiResponse<DnsRecord>>();
|
||||
return result?.Data ?? record;
|
||||
}
|
||||
|
||||
private async Task<DnsRecord> UpdateCoreDnsRecordAsync(string zone, string recordId, DnsRecord record)
|
||||
{
|
||||
var response = await _httpClient.PutAsJsonAsync($"/api/v1/dns/zones/{zone}/records/{recordId}", record);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<ApiResponse<DnsRecord>>();
|
||||
return result?.Data ?? record;
|
||||
}
|
||||
|
||||
private async Task DeleteCoreDnsRecordAsync(string zone, string recordId)
|
||||
{
|
||||
var response = await _httpClient.DeleteAsync($"/api/v1/dns/zones/{zone}/records/{recordId}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
// ─── Cloudflare ───
|
||||
private async Task<List<DnsRecord>> GetCloudflareRecordsAsync(string zone)
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"/zones/{zone}/dns_records");
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<CloudflareResponse>();
|
||||
return result?.Result?.Select(r => new DnsRecord
|
||||
{
|
||||
Id = r.Id,
|
||||
Name = r.Name,
|
||||
Type = r.Type,
|
||||
Content = r.Content,
|
||||
Ttl = r.Ttl ?? 300,
|
||||
Proxied = r.Proxied ?? false
|
||||
}).ToList() ?? new List<DnsRecord>();
|
||||
}
|
||||
|
||||
private async Task<DnsRecord> CreateCloudflareRecordAsync(string zone, DnsRecord record)
|
||||
{
|
||||
var req = new CloudflareRecordRequest
|
||||
{
|
||||
Type = record.Type,
|
||||
Name = record.Name,
|
||||
Content = record.Content,
|
||||
Ttl = record.Ttl,
|
||||
Proxied = record.Proxied
|
||||
};
|
||||
var response = await _httpClient.PostAsJsonAsync($"/zones/{zone}/dns_records", req);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<CloudflareSingleResponse>();
|
||||
record.Id = result?.Result?.Id;
|
||||
return record;
|
||||
}
|
||||
|
||||
private async Task<DnsRecord> UpdateCloudflareRecordAsync(string zone, string recordId, DnsRecord record)
|
||||
{
|
||||
var req = new CloudflareRecordRequest
|
||||
{
|
||||
Type = record.Type,
|
||||
Name = record.Name,
|
||||
Content = record.Content,
|
||||
Ttl = record.Ttl,
|
||||
Proxied = record.Proxied
|
||||
};
|
||||
var response = await _httpClient.PutAsJsonAsync($"/zones/{zone}/dns_records/{recordId}", req);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return record;
|
||||
}
|
||||
|
||||
private async Task DeleteCloudflareRecordAsync(string zone, string recordId)
|
||||
{
|
||||
var response = await _httpClient.DeleteAsync($"/zones/{zone}/dns_records/{recordId}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
// ─── Route 53 ───
|
||||
private async Task<List<DnsRecord>> GetRoute53RecordsAsync(string zone)
|
||||
{
|
||||
return new List<DnsRecord>();
|
||||
}
|
||||
|
||||
private async Task<DnsRecord> CreateRoute53RecordAsync(string zone, DnsRecord record) => record;
|
||||
private async Task<DnsRecord> UpdateRoute53RecordAsync(string zone, string recordId, DnsRecord record) => record;
|
||||
private async Task DeleteRoute53RecordAsync(string zone, string recordId) { }
|
||||
|
||||
// ─── Response DTOs ───
|
||||
private class ApiResponse<T>
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public T Data { get; set; }
|
||||
public string Error { get; set; }
|
||||
}
|
||||
|
||||
private class TechnitiumZoneResponse
|
||||
{
|
||||
public List<TechnitiumRecord> Records { get; set; }
|
||||
}
|
||||
|
||||
private class TechnitiumRecord
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string Value { get; set; }
|
||||
public int Ttl { get; set; }
|
||||
}
|
||||
|
||||
private class TechnitiumRecordRequest
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string Value { get; set; }
|
||||
public int Ttl { get; set; }
|
||||
}
|
||||
|
||||
private class TechnitiumRecordResponse
|
||||
{
|
||||
public TechnitiumRecord Record { get; set; }
|
||||
}
|
||||
|
||||
private class CloudflareResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public List<CloudflareRecord> Result { get; set; }
|
||||
}
|
||||
|
||||
private class CloudflareRecord
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string Content { get; set; }
|
||||
public int? Ttl { get; set; }
|
||||
public bool? Proxied { get; set; }
|
||||
}
|
||||
|
||||
private class CloudflareRecordRequest
|
||||
{
|
||||
public string Type { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Content { get; set; }
|
||||
public int Ttl { get; set; }
|
||||
public bool Proxied { get; set; }
|
||||
}
|
||||
|
||||
private class CloudflareSingleResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public CloudflareRecord Result { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user