chore: clean .gitignore cache
This commit is contained in:
@@ -1,119 +0,0 @@
|
||||
package builtin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/Wikid82/charon/backend/pkg/dnsprovider"
|
||||
)
|
||||
|
||||
// AzureProvider implements the ProviderPlugin interface for Azure DNS.
|
||||
type AzureProvider struct{}
|
||||
|
||||
func (p *AzureProvider) Type() string {
|
||||
return "azure"
|
||||
}
|
||||
|
||||
func (p *AzureProvider) Metadata() dnsprovider.ProviderMetadata {
|
||||
return dnsprovider.ProviderMetadata{
|
||||
Type: "azure",
|
||||
Name: "Azure DNS",
|
||||
Description: "Microsoft Azure DNS with service principal authentication",
|
||||
DocumentationURL: "https://learn.microsoft.com/en-us/azure/dns/",
|
||||
IsBuiltIn: true,
|
||||
Version: "1.0.0",
|
||||
}
|
||||
}
|
||||
|
||||
func (p *AzureProvider) Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *AzureProvider) Cleanup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *AzureProvider) RequiredCredentialFields() []dnsprovider.CredentialFieldSpec {
|
||||
return []dnsprovider.CredentialFieldSpec{
|
||||
{
|
||||
Name: "tenant_id",
|
||||
Label: "Tenant ID",
|
||||
Type: "text",
|
||||
Placeholder: "Enter your Azure AD tenant ID",
|
||||
Hint: "Azure Active Directory tenant ID",
|
||||
},
|
||||
{
|
||||
Name: "client_id",
|
||||
Label: "Client ID",
|
||||
Type: "text",
|
||||
Placeholder: "Enter your service principal client ID",
|
||||
Hint: "Service principal (app registration) client ID",
|
||||
},
|
||||
{
|
||||
Name: "client_secret",
|
||||
Label: "Client Secret",
|
||||
Type: "password",
|
||||
Placeholder: "Enter your client secret",
|
||||
Hint: "Service principal client secret",
|
||||
},
|
||||
{
|
||||
Name: "subscription_id",
|
||||
Label: "Subscription ID",
|
||||
Type: "text",
|
||||
Placeholder: "Enter your Azure subscription ID",
|
||||
Hint: "Azure subscription containing DNS zone",
|
||||
},
|
||||
{
|
||||
Name: "resource_group",
|
||||
Label: "Resource Group",
|
||||
Type: "text",
|
||||
Placeholder: "Enter resource group name",
|
||||
Hint: "Resource group containing the DNS zone",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *AzureProvider) OptionalCredentialFields() []dnsprovider.CredentialFieldSpec {
|
||||
return []dnsprovider.CredentialFieldSpec{}
|
||||
}
|
||||
|
||||
func (p *AzureProvider) ValidateCredentials(creds map[string]string) error {
|
||||
requiredFields := []string{"tenant_id", "client_id", "client_secret", "subscription_id", "resource_group"}
|
||||
for _, field := range requiredFields {
|
||||
if creds[field] == "" {
|
||||
return fmt.Errorf("%s is required", field)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *AzureProvider) TestCredentials(creds map[string]string) error {
|
||||
return p.ValidateCredentials(creds)
|
||||
}
|
||||
|
||||
func (p *AzureProvider) SupportsMultiCredential() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *AzureProvider) BuildCaddyConfig(creds map[string]string) map[string]any {
|
||||
return map[string]any{
|
||||
"name": "azure",
|
||||
"tenant_id": creds["tenant_id"],
|
||||
"client_id": creds["client_id"],
|
||||
"client_secret": creds["client_secret"],
|
||||
"subscription_id": creds["subscription_id"],
|
||||
"resource_group": creds["resource_group"],
|
||||
}
|
||||
}
|
||||
|
||||
func (p *AzureProvider) BuildCaddyConfigForZone(baseDomain string, creds map[string]string) map[string]any {
|
||||
return p.BuildCaddyConfig(creds)
|
||||
}
|
||||
|
||||
func (p *AzureProvider) PropagationTimeout() time.Duration {
|
||||
return 180 * time.Second
|
||||
}
|
||||
|
||||
func (p *AzureProvider) PollingInterval() time.Duration {
|
||||
return 10 * time.Second
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
package builtin
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/Wikid82/charon/backend/pkg/dnsprovider"
|
||||
)
|
||||
|
||||
func TestCloudflareProvider(t *testing.T) {
|
||||
p := &CloudflareProvider{}
|
||||
|
||||
if p.Type() != "cloudflare" {
|
||||
t.Errorf("expected type cloudflare, got %s", p.Type())
|
||||
}
|
||||
|
||||
meta := p.Metadata()
|
||||
if meta.Name != "Cloudflare" {
|
||||
t.Errorf("expected name Cloudflare, got %s", meta.Name)
|
||||
}
|
||||
if !meta.IsBuiltIn {
|
||||
t.Error("expected IsBuiltIn to be true")
|
||||
}
|
||||
|
||||
if err := p.Init(); err != nil {
|
||||
t.Errorf("Init failed: %v", err)
|
||||
}
|
||||
|
||||
if err := p.Cleanup(); err != nil {
|
||||
t.Errorf("Cleanup failed: %v", err)
|
||||
}
|
||||
|
||||
required := p.RequiredCredentialFields()
|
||||
if len(required) != 1 {
|
||||
t.Errorf("expected 1 required field, got %d", len(required))
|
||||
}
|
||||
if required[0].Name != "api_token" {
|
||||
t.Errorf("expected api_token field, got %s", required[0].Name)
|
||||
}
|
||||
|
||||
optional := p.OptionalCredentialFields()
|
||||
if len(optional) != 1 {
|
||||
t.Errorf("expected 1 optional field, got %d", len(optional))
|
||||
}
|
||||
if optional[0].Name != "zone_id" {
|
||||
t.Errorf("expected zone_id field, got %s", optional[0].Name)
|
||||
}
|
||||
|
||||
// Test credential validation
|
||||
err := p.ValidateCredentials(map[string]string{})
|
||||
if err == nil {
|
||||
t.Error("expected validation error for empty credentials")
|
||||
}
|
||||
|
||||
err = p.ValidateCredentials(map[string]string{"api_token": "test"})
|
||||
if err != nil {
|
||||
t.Errorf("validation failed: %v", err)
|
||||
}
|
||||
|
||||
if p.SupportsMultiCredential() {
|
||||
t.Error("expected SupportsMultiCredential to be false")
|
||||
}
|
||||
|
||||
config := p.BuildCaddyConfig(map[string]string{"api_token": "test"})
|
||||
if config["name"] != "cloudflare" {
|
||||
t.Error("expected caddy config name to be cloudflare")
|
||||
}
|
||||
if config["api_token"] != "test" {
|
||||
t.Error("expected api_token in caddy config")
|
||||
}
|
||||
|
||||
timeout := p.PropagationTimeout()
|
||||
if timeout.Seconds() == 0 {
|
||||
t.Error("expected non-zero propagation timeout")
|
||||
}
|
||||
|
||||
interval := p.PollingInterval()
|
||||
if interval.Seconds() == 0 {
|
||||
t.Error("expected non-zero polling interval")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoute53Provider(t *testing.T) {
|
||||
p := &Route53Provider{}
|
||||
|
||||
if p.Type() != "route53" {
|
||||
t.Errorf("expected type route53, got %s", p.Type())
|
||||
}
|
||||
|
||||
required := p.RequiredCredentialFields()
|
||||
if len(required) != 2 {
|
||||
t.Errorf("expected 2 required fields, got %d", len(required))
|
||||
}
|
||||
|
||||
err := p.ValidateCredentials(map[string]string{})
|
||||
if err == nil {
|
||||
t.Error("expected validation error for empty credentials")
|
||||
}
|
||||
|
||||
err = p.ValidateCredentials(map[string]string{
|
||||
"access_key_id": "test",
|
||||
"secret_access_key": "test",
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("validation failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDigitalOceanProvider(t *testing.T) {
|
||||
p := &DigitalOceanProvider{}
|
||||
|
||||
if p.Type() != "digitalocean" {
|
||||
t.Errorf("expected type digitalocean, got %s", p.Type())
|
||||
}
|
||||
|
||||
required := p.RequiredCredentialFields()
|
||||
if len(required) != 1 {
|
||||
t.Errorf("expected 1 required field, got %d", len(required))
|
||||
}
|
||||
|
||||
err := p.ValidateCredentials(map[string]string{})
|
||||
if err == nil {
|
||||
t.Error("expected validation error for empty credentials")
|
||||
}
|
||||
|
||||
err = p.ValidateCredentials(map[string]string{"api_token": "test"})
|
||||
if err != nil {
|
||||
t.Errorf("validation failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoogleCloudDNSProvider(t *testing.T) {
|
||||
p := &GoogleCloudDNSProvider{}
|
||||
|
||||
if p.Type() != "googleclouddns" {
|
||||
t.Errorf("expected type googleclouddns, got %s", p.Type())
|
||||
}
|
||||
|
||||
required := p.RequiredCredentialFields()
|
||||
if len(required) != 1 {
|
||||
t.Errorf("expected 1 required field, got %d", len(required))
|
||||
}
|
||||
|
||||
err := p.ValidateCredentials(map[string]string{})
|
||||
if err == nil {
|
||||
t.Error("expected validation error for empty credentials")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAzureProvider(t *testing.T) {
|
||||
p := &AzureProvider{}
|
||||
|
||||
if p.Type() != "azure" {
|
||||
t.Errorf("expected type azure, got %s", p.Type())
|
||||
}
|
||||
|
||||
required := p.RequiredCredentialFields()
|
||||
if len(required) != 5 {
|
||||
t.Errorf("expected 5 required fields, got %d", len(required))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamecheapProvider(t *testing.T) {
|
||||
p := &NamecheapProvider{}
|
||||
|
||||
if p.Type() != "namecheap" {
|
||||
t.Errorf("expected type namecheap, got %s", p.Type())
|
||||
}
|
||||
|
||||
required := p.RequiredCredentialFields()
|
||||
if len(required) != 2 {
|
||||
t.Errorf("expected 2 required fields, got %d", len(required))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoDaddyProvider(t *testing.T) {
|
||||
p := &GoDaddyProvider{}
|
||||
|
||||
if p.Type() != "godaddy" {
|
||||
t.Errorf("expected type godaddy, got %s", p.Type())
|
||||
}
|
||||
|
||||
required := p.RequiredCredentialFields()
|
||||
if len(required) != 2 {
|
||||
t.Errorf("expected 2 required fields, got %d", len(required))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHetznerProvider(t *testing.T) {
|
||||
p := &HetznerProvider{}
|
||||
|
||||
if p.Type() != "hetzner" {
|
||||
t.Errorf("expected type hetzner, got %s", p.Type())
|
||||
}
|
||||
|
||||
required := p.RequiredCredentialFields()
|
||||
if len(required) != 1 {
|
||||
t.Errorf("expected 1 required field, got %d", len(required))
|
||||
}
|
||||
}
|
||||
|
||||
func TestVultrProvider(t *testing.T) {
|
||||
p := &VultrProvider{}
|
||||
|
||||
if p.Type() != "vultr" {
|
||||
t.Errorf("expected type vultr, got %s", p.Type())
|
||||
}
|
||||
|
||||
required := p.RequiredCredentialFields()
|
||||
if len(required) != 1 {
|
||||
t.Errorf("expected 1 required field, got %d", len(required))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSimpleProvider(t *testing.T) {
|
||||
p := &DNSimpleProvider{}
|
||||
|
||||
if p.Type() != "dnsimple" {
|
||||
t.Errorf("expected type dnsimple, got %s", p.Type())
|
||||
}
|
||||
|
||||
required := p.RequiredCredentialFields()
|
||||
if len(required) != 1 {
|
||||
t.Errorf("expected 1 required field, got %d", len(required))
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderRegistration(t *testing.T) {
|
||||
// Test that all providers are registered after init
|
||||
providers := []string{
|
||||
"cloudflare",
|
||||
"route53",
|
||||
"digitalocean",
|
||||
"googleclouddns",
|
||||
"azure",
|
||||
"namecheap",
|
||||
"godaddy",
|
||||
"hetzner",
|
||||
"vultr",
|
||||
"dnsimple",
|
||||
}
|
||||
|
||||
for _, providerType := range providers {
|
||||
provider, ok := dnsprovider.Global().Get(providerType)
|
||||
if !ok {
|
||||
t.Errorf("provider %s not registered", providerType)
|
||||
}
|
||||
if provider == nil {
|
||||
t.Errorf("provider %s is nil", providerType)
|
||||
}
|
||||
}
|
||||
|
||||
// Test GetTypes
|
||||
types := dnsprovider.Global().Types()
|
||||
if len(types) < len(providers) {
|
||||
t.Errorf("expected at least %d types, got %d", len(providers), len(types))
|
||||
}
|
||||
|
||||
// Test IsSupported
|
||||
for _, providerType := range providers {
|
||||
if !dnsprovider.Global().IsSupported(providerType) {
|
||||
t.Errorf("provider %s should be supported", providerType)
|
||||
}
|
||||
}
|
||||
|
||||
if dnsprovider.Global().IsSupported("invalid-provider") {
|
||||
t.Error("invalid provider should not be supported")
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
package builtin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/Wikid82/charon/backend/pkg/dnsprovider"
|
||||
)
|
||||
|
||||
// CloudflareProvider implements the ProviderPlugin interface for Cloudflare DNS.
|
||||
type CloudflareProvider struct{}
|
||||
|
||||
func (p *CloudflareProvider) Type() string {
|
||||
return "cloudflare"
|
||||
}
|
||||
|
||||
func (p *CloudflareProvider) Metadata() dnsprovider.ProviderMetadata {
|
||||
return dnsprovider.ProviderMetadata{
|
||||
Type: "cloudflare",
|
||||
Name: "Cloudflare",
|
||||
Description: "Cloudflare DNS with API Token authentication",
|
||||
DocumentationURL: "https://developers.cloudflare.com/api/tokens/create/",
|
||||
IsBuiltIn: true,
|
||||
Version: "1.0.0",
|
||||
}
|
||||
}
|
||||
|
||||
func (p *CloudflareProvider) Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *CloudflareProvider) Cleanup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *CloudflareProvider) RequiredCredentialFields() []dnsprovider.CredentialFieldSpec {
|
||||
return []dnsprovider.CredentialFieldSpec{
|
||||
{
|
||||
Name: "api_token",
|
||||
Label: "API Token",
|
||||
Type: "password",
|
||||
Placeholder: "Enter your Cloudflare API token",
|
||||
Hint: "Token requires Zone:DNS:Edit permission",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *CloudflareProvider) OptionalCredentialFields() []dnsprovider.CredentialFieldSpec {
|
||||
return []dnsprovider.CredentialFieldSpec{
|
||||
{
|
||||
Name: "zone_id",
|
||||
Label: "Zone ID",
|
||||
Type: "text",
|
||||
Placeholder: "Optional: Specific zone ID",
|
||||
Hint: "Leave empty to auto-detect zone",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *CloudflareProvider) ValidateCredentials(creds map[string]string) error {
|
||||
if creds["api_token"] == "" {
|
||||
return fmt.Errorf("api_token is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *CloudflareProvider) TestCredentials(creds map[string]string) error {
|
||||
return p.ValidateCredentials(creds)
|
||||
}
|
||||
|
||||
func (p *CloudflareProvider) SupportsMultiCredential() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *CloudflareProvider) BuildCaddyConfig(creds map[string]string) map[string]any {
|
||||
config := map[string]any{
|
||||
"name": "cloudflare",
|
||||
"api_token": creds["api_token"],
|
||||
}
|
||||
if zoneID := creds["zone_id"]; zoneID != "" {
|
||||
config["zone_id"] = zoneID
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
func (p *CloudflareProvider) BuildCaddyConfigForZone(baseDomain string, creds map[string]string) map[string]any {
|
||||
return p.BuildCaddyConfig(creds)
|
||||
}
|
||||
|
||||
func (p *CloudflareProvider) PropagationTimeout() time.Duration {
|
||||
return 120 * time.Second
|
||||
}
|
||||
|
||||
func (p *CloudflareProvider) PollingInterval() time.Duration {
|
||||
return 5 * time.Second
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package builtin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/Wikid82/charon/backend/pkg/dnsprovider"
|
||||
)
|
||||
|
||||
// DigitalOceanProvider implements the ProviderPlugin interface for DigitalOcean DNS.
|
||||
type DigitalOceanProvider struct{}
|
||||
|
||||
func (p *DigitalOceanProvider) Type() string {
|
||||
return "digitalocean"
|
||||
}
|
||||
|
||||
func (p *DigitalOceanProvider) Metadata() dnsprovider.ProviderMetadata {
|
||||
return dnsprovider.ProviderMetadata{
|
||||
Type: "digitalocean",
|
||||
Name: "DigitalOcean",
|
||||
Description: "DigitalOcean DNS with API token authentication",
|
||||
DocumentationURL: "https://docs.digitalocean.com/reference/api/api-reference/#tag/Domains",
|
||||
IsBuiltIn: true,
|
||||
Version: "1.0.0",
|
||||
}
|
||||
}
|
||||
|
||||
func (p *DigitalOceanProvider) Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *DigitalOceanProvider) Cleanup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *DigitalOceanProvider) RequiredCredentialFields() []dnsprovider.CredentialFieldSpec {
|
||||
return []dnsprovider.CredentialFieldSpec{
|
||||
{
|
||||
Name: "api_token",
|
||||
Label: "API Token",
|
||||
Type: "password",
|
||||
Placeholder: "Enter your DigitalOcean API token",
|
||||
Hint: "Generate from API settings in your DigitalOcean account",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *DigitalOceanProvider) OptionalCredentialFields() []dnsprovider.CredentialFieldSpec {
|
||||
return []dnsprovider.CredentialFieldSpec{}
|
||||
}
|
||||
|
||||
func (p *DigitalOceanProvider) ValidateCredentials(creds map[string]string) error {
|
||||
if creds["api_token"] == "" {
|
||||
return fmt.Errorf("api_token is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *DigitalOceanProvider) TestCredentials(creds map[string]string) error {
|
||||
return p.ValidateCredentials(creds)
|
||||
}
|
||||
|
||||
func (p *DigitalOceanProvider) SupportsMultiCredential() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *DigitalOceanProvider) BuildCaddyConfig(creds map[string]string) map[string]any {
|
||||
return map[string]any{
|
||||
"name": "digitalocean",
|
||||
"api_token": creds["api_token"],
|
||||
}
|
||||
}
|
||||
|
||||
func (p *DigitalOceanProvider) BuildCaddyConfigForZone(baseDomain string, creds map[string]string) map[string]any {
|
||||
return p.BuildCaddyConfig(creds)
|
||||
}
|
||||
|
||||
func (p *DigitalOceanProvider) PropagationTimeout() time.Duration {
|
||||
return 120 * time.Second
|
||||
}
|
||||
|
||||
func (p *DigitalOceanProvider) PollingInterval() time.Duration {
|
||||
return 5 * time.Second
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
package builtin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/Wikid82/charon/backend/pkg/dnsprovider"
|
||||
)
|
||||
|
||||
// DNSimpleProvider implements the ProviderPlugin interface for DNSimple.
|
||||
type DNSimpleProvider struct{}
|
||||
|
||||
func (p *DNSimpleProvider) Type() string {
|
||||
return "dnsimple"
|
||||
}
|
||||
|
||||
func (p *DNSimpleProvider) Metadata() dnsprovider.ProviderMetadata {
|
||||
return dnsprovider.ProviderMetadata{
|
||||
Type: "dnsimple",
|
||||
Name: "DNSimple",
|
||||
Description: "DNSimple DNS with API token authentication",
|
||||
DocumentationURL: "https://developer.dnsimple.com/",
|
||||
IsBuiltIn: true,
|
||||
Version: "1.0.0",
|
||||
}
|
||||
}
|
||||
|
||||
func (p *DNSimpleProvider) Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *DNSimpleProvider) Cleanup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *DNSimpleProvider) RequiredCredentialFields() []dnsprovider.CredentialFieldSpec {
|
||||
return []dnsprovider.CredentialFieldSpec{
|
||||
{
|
||||
Name: "api_token",
|
||||
Label: "API Token",
|
||||
Type: "password",
|
||||
Placeholder: "Enter your DNSimple API token",
|
||||
Hint: "OAuth token or Account API token",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *DNSimpleProvider) OptionalCredentialFields() []dnsprovider.CredentialFieldSpec {
|
||||
return []dnsprovider.CredentialFieldSpec{
|
||||
{
|
||||
Name: "account_id",
|
||||
Label: "Account ID",
|
||||
Type: "text",
|
||||
Placeholder: "12345",
|
||||
Hint: "Optional: Your DNSimple account ID",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *DNSimpleProvider) ValidateCredentials(creds map[string]string) error {
|
||||
if creds["api_token"] == "" {
|
||||
return fmt.Errorf("api_token is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *DNSimpleProvider) TestCredentials(creds map[string]string) error {
|
||||
return p.ValidateCredentials(creds)
|
||||
}
|
||||
|
||||
func (p *DNSimpleProvider) SupportsMultiCredential() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *DNSimpleProvider) BuildCaddyConfig(creds map[string]string) map[string]any {
|
||||
config := map[string]any{
|
||||
"name": "dnsimple",
|
||||
"api_token": creds["api_token"],
|
||||
}
|
||||
if accountID := creds["account_id"]; accountID != "" {
|
||||
config["account_id"] = accountID
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
func (p *DNSimpleProvider) BuildCaddyConfigForZone(baseDomain string, creds map[string]string) map[string]any {
|
||||
return p.BuildCaddyConfig(creds)
|
||||
}
|
||||
|
||||
func (p *DNSimpleProvider) PropagationTimeout() time.Duration {
|
||||
return 120 * time.Second
|
||||
}
|
||||
|
||||
func (p *DNSimpleProvider) PollingInterval() time.Duration {
|
||||
return 5 * time.Second
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
package builtin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/Wikid82/charon/backend/pkg/dnsprovider"
|
||||
)
|
||||
|
||||
// GoDaddyProvider implements the ProviderPlugin interface for GoDaddy DNS.
|
||||
type GoDaddyProvider struct{}
|
||||
|
||||
func (p *GoDaddyProvider) Type() string {
|
||||
return "godaddy"
|
||||
}
|
||||
|
||||
func (p *GoDaddyProvider) Metadata() dnsprovider.ProviderMetadata {
|
||||
return dnsprovider.ProviderMetadata{
|
||||
Type: "godaddy",
|
||||
Name: "GoDaddy",
|
||||
Description: "GoDaddy DNS with API key and secret",
|
||||
DocumentationURL: "https://developer.godaddy.com/doc",
|
||||
IsBuiltIn: true,
|
||||
Version: "1.0.0",
|
||||
}
|
||||
}
|
||||
|
||||
func (p *GoDaddyProvider) Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *GoDaddyProvider) Cleanup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *GoDaddyProvider) RequiredCredentialFields() []dnsprovider.CredentialFieldSpec {
|
||||
return []dnsprovider.CredentialFieldSpec{
|
||||
{
|
||||
Name: "api_key",
|
||||
Label: "API Key",
|
||||
Type: "text",
|
||||
Placeholder: "Enter your GoDaddy API key",
|
||||
Hint: "Production API key from GoDaddy developer portal",
|
||||
},
|
||||
{
|
||||
Name: "api_secret",
|
||||
Label: "API Secret",
|
||||
Type: "password",
|
||||
Placeholder: "Enter your GoDaddy API secret",
|
||||
Hint: "Production API secret (stored encrypted)",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *GoDaddyProvider) OptionalCredentialFields() []dnsprovider.CredentialFieldSpec {
|
||||
return []dnsprovider.CredentialFieldSpec{}
|
||||
}
|
||||
|
||||
func (p *GoDaddyProvider) ValidateCredentials(creds map[string]string) error {
|
||||
if creds["api_key"] == "" {
|
||||
return fmt.Errorf("api_key is required")
|
||||
}
|
||||
if creds["api_secret"] == "" {
|
||||
return fmt.Errorf("api_secret is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *GoDaddyProvider) TestCredentials(creds map[string]string) error {
|
||||
return p.ValidateCredentials(creds)
|
||||
}
|
||||
|
||||
func (p *GoDaddyProvider) SupportsMultiCredential() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *GoDaddyProvider) BuildCaddyConfig(creds map[string]string) map[string]any {
|
||||
return map[string]any{
|
||||
"name": "godaddy",
|
||||
"api_key": creds["api_key"],
|
||||
"api_secret": creds["api_secret"],
|
||||
}
|
||||
}
|
||||
|
||||
func (p *GoDaddyProvider) BuildCaddyConfigForZone(baseDomain string, creds map[string]string) map[string]any {
|
||||
return p.BuildCaddyConfig(creds)
|
||||
}
|
||||
|
||||
func (p *GoDaddyProvider) PropagationTimeout() time.Duration {
|
||||
return 180 * time.Second
|
||||
}
|
||||
|
||||
func (p *GoDaddyProvider) PollingInterval() time.Duration {
|
||||
return 10 * time.Second
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
package builtin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/Wikid82/charon/backend/pkg/dnsprovider"
|
||||
)
|
||||
|
||||
// GoogleCloudDNSProvider implements the ProviderPlugin interface for Google Cloud DNS.
|
||||
type GoogleCloudDNSProvider struct{}
|
||||
|
||||
func (p *GoogleCloudDNSProvider) Type() string {
|
||||
return "googleclouddns"
|
||||
}
|
||||
|
||||
func (p *GoogleCloudDNSProvider) Metadata() dnsprovider.ProviderMetadata {
|
||||
return dnsprovider.ProviderMetadata{
|
||||
Type: "googleclouddns",
|
||||
Name: "Google Cloud DNS",
|
||||
Description: "Google Cloud DNS with service account credentials",
|
||||
DocumentationURL: "https://cloud.google.com/dns/docs",
|
||||
IsBuiltIn: true,
|
||||
Version: "1.0.0",
|
||||
}
|
||||
}
|
||||
|
||||
func (p *GoogleCloudDNSProvider) Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *GoogleCloudDNSProvider) Cleanup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *GoogleCloudDNSProvider) RequiredCredentialFields() []dnsprovider.CredentialFieldSpec {
|
||||
return []dnsprovider.CredentialFieldSpec{
|
||||
{
|
||||
Name: "service_account_json",
|
||||
Label: "Service Account JSON",
|
||||
Type: "textarea",
|
||||
Placeholder: "Paste your service account JSON key",
|
||||
Hint: "JSON key file content for service account with DNS admin role",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *GoogleCloudDNSProvider) OptionalCredentialFields() []dnsprovider.CredentialFieldSpec {
|
||||
return []dnsprovider.CredentialFieldSpec{
|
||||
{
|
||||
Name: "project_id",
|
||||
Label: "Project ID",
|
||||
Type: "text",
|
||||
Placeholder: "my-gcp-project",
|
||||
Hint: "Optional: GCP project ID (auto-detected from service account if not provided)",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *GoogleCloudDNSProvider) ValidateCredentials(creds map[string]string) error {
|
||||
if creds["service_account_json"] == "" {
|
||||
return fmt.Errorf("service_account_json is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *GoogleCloudDNSProvider) TestCredentials(creds map[string]string) error {
|
||||
return p.ValidateCredentials(creds)
|
||||
}
|
||||
|
||||
func (p *GoogleCloudDNSProvider) SupportsMultiCredential() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *GoogleCloudDNSProvider) BuildCaddyConfig(creds map[string]string) map[string]any {
|
||||
config := map[string]any{
|
||||
"name": "googleclouddns",
|
||||
"service_account_json": creds["service_account_json"],
|
||||
}
|
||||
if projectID := creds["project_id"]; projectID != "" {
|
||||
config["project_id"] = projectID
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
func (p *GoogleCloudDNSProvider) BuildCaddyConfigForZone(baseDomain string, creds map[string]string) map[string]any {
|
||||
return p.BuildCaddyConfig(creds)
|
||||
}
|
||||
|
||||
func (p *GoogleCloudDNSProvider) PropagationTimeout() time.Duration {
|
||||
return 180 * time.Second
|
||||
}
|
||||
|
||||
func (p *GoogleCloudDNSProvider) PollingInterval() time.Duration {
|
||||
return 10 * time.Second
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package builtin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/Wikid82/charon/backend/pkg/dnsprovider"
|
||||
)
|
||||
|
||||
// HetznerProvider implements the ProviderPlugin interface for Hetzner DNS.
|
||||
type HetznerProvider struct{}
|
||||
|
||||
func (p *HetznerProvider) Type() string {
|
||||
return "hetzner"
|
||||
}
|
||||
|
||||
func (p *HetznerProvider) Metadata() dnsprovider.ProviderMetadata {
|
||||
return dnsprovider.ProviderMetadata{
|
||||
Type: "hetzner",
|
||||
Name: "Hetzner",
|
||||
Description: "Hetzner DNS with API token authentication",
|
||||
DocumentationURL: "https://dns.hetzner.com/api-docs",
|
||||
IsBuiltIn: true,
|
||||
Version: "1.0.0",
|
||||
}
|
||||
}
|
||||
|
||||
func (p *HetznerProvider) Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *HetznerProvider) Cleanup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *HetznerProvider) RequiredCredentialFields() []dnsprovider.CredentialFieldSpec {
|
||||
return []dnsprovider.CredentialFieldSpec{
|
||||
{
|
||||
Name: "api_token",
|
||||
Label: "API Token",
|
||||
Type: "password",
|
||||
Placeholder: "Enter your Hetzner DNS API token",
|
||||
Hint: "Generate from Hetzner DNS Console",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *HetznerProvider) OptionalCredentialFields() []dnsprovider.CredentialFieldSpec {
|
||||
return []dnsprovider.CredentialFieldSpec{}
|
||||
}
|
||||
|
||||
func (p *HetznerProvider) ValidateCredentials(creds map[string]string) error {
|
||||
if creds["api_token"] == "" {
|
||||
return fmt.Errorf("api_token is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *HetznerProvider) TestCredentials(creds map[string]string) error {
|
||||
return p.ValidateCredentials(creds)
|
||||
}
|
||||
|
||||
func (p *HetznerProvider) SupportsMultiCredential() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *HetznerProvider) BuildCaddyConfig(creds map[string]string) map[string]any {
|
||||
return map[string]any{
|
||||
"name": "hetzner",
|
||||
"api_token": creds["api_token"],
|
||||
}
|
||||
}
|
||||
|
||||
func (p *HetznerProvider) BuildCaddyConfigForZone(baseDomain string, creds map[string]string) map[string]any {
|
||||
return p.BuildCaddyConfig(creds)
|
||||
}
|
||||
|
||||
func (p *HetznerProvider) PropagationTimeout() time.Duration {
|
||||
return 120 * time.Second
|
||||
}
|
||||
|
||||
func (p *HetznerProvider) PollingInterval() time.Duration {
|
||||
return 5 * time.Second
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package builtin
|
||||
|
||||
import (
|
||||
"github.com/Wikid82/charon/backend/internal/logger"
|
||||
"github.com/Wikid82/charon/backend/pkg/dnsprovider"
|
||||
)
|
||||
|
||||
// init automatically registers all built-in DNS provider plugins when the package is imported.
|
||||
func init() {
|
||||
providers := []dnsprovider.ProviderPlugin{
|
||||
&CloudflareProvider{},
|
||||
&Route53Provider{},
|
||||
&DigitalOceanProvider{},
|
||||
&GoogleCloudDNSProvider{},
|
||||
&AzureProvider{},
|
||||
&NamecheapProvider{},
|
||||
&GoDaddyProvider{},
|
||||
&HetznerProvider{},
|
||||
&VultrProvider{},
|
||||
&DNSimpleProvider{},
|
||||
}
|
||||
|
||||
for _, provider := range providers {
|
||||
if err := provider.Init(); err != nil {
|
||||
logger.Log().WithError(err).Warnf("Failed to initialize built-in provider: %s", provider.Type())
|
||||
continue
|
||||
}
|
||||
|
||||
if err := dnsprovider.Global().Register(provider); err != nil {
|
||||
logger.Log().WithError(err).Warnf("Failed to register built-in provider: %s", provider.Type())
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Log().Debugf("Registered built-in DNS provider: %s", provider.Type())
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
package builtin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/Wikid82/charon/backend/pkg/dnsprovider"
|
||||
)
|
||||
|
||||
// NamecheapProvider implements the ProviderPlugin interface for Namecheap DNS.
|
||||
type NamecheapProvider struct{}
|
||||
|
||||
func (p *NamecheapProvider) Type() string {
|
||||
return "namecheap"
|
||||
}
|
||||
|
||||
func (p *NamecheapProvider) Metadata() dnsprovider.ProviderMetadata {
|
||||
return dnsprovider.ProviderMetadata{
|
||||
Type: "namecheap",
|
||||
Name: "Namecheap",
|
||||
Description: "Namecheap DNS with API credentials",
|
||||
DocumentationURL: "https://www.namecheap.com/support/api/intro/",
|
||||
IsBuiltIn: true,
|
||||
Version: "1.0.0",
|
||||
}
|
||||
}
|
||||
|
||||
func (p *NamecheapProvider) Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *NamecheapProvider) Cleanup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *NamecheapProvider) RequiredCredentialFields() []dnsprovider.CredentialFieldSpec {
|
||||
return []dnsprovider.CredentialFieldSpec{
|
||||
{
|
||||
Name: "api_user",
|
||||
Label: "API User",
|
||||
Type: "text",
|
||||
Placeholder: "Enter your Namecheap API username",
|
||||
Hint: "Your Namecheap account username",
|
||||
},
|
||||
{
|
||||
Name: "api_key",
|
||||
Label: "API Key",
|
||||
Type: "password",
|
||||
Placeholder: "Enter your Namecheap API key",
|
||||
Hint: "Enable API access in your Namecheap account",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *NamecheapProvider) OptionalCredentialFields() []dnsprovider.CredentialFieldSpec {
|
||||
return []dnsprovider.CredentialFieldSpec{
|
||||
{
|
||||
Name: "client_ip",
|
||||
Label: "Client IP",
|
||||
Type: "text",
|
||||
Placeholder: "1.2.3.4",
|
||||
Hint: "Optional: Whitelisted IP address for API access",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *NamecheapProvider) ValidateCredentials(creds map[string]string) error {
|
||||
if creds["api_user"] == "" {
|
||||
return fmt.Errorf("api_user is required")
|
||||
}
|
||||
if creds["api_key"] == "" {
|
||||
return fmt.Errorf("api_key is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *NamecheapProvider) TestCredentials(creds map[string]string) error {
|
||||
return p.ValidateCredentials(creds)
|
||||
}
|
||||
|
||||
func (p *NamecheapProvider) SupportsMultiCredential() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *NamecheapProvider) BuildCaddyConfig(creds map[string]string) map[string]any {
|
||||
config := map[string]any{
|
||||
"name": "namecheap",
|
||||
"api_user": creds["api_user"],
|
||||
"api_key": creds["api_key"],
|
||||
}
|
||||
if clientIP := creds["client_ip"]; clientIP != "" {
|
||||
config["client_ip"] = clientIP
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
func (p *NamecheapProvider) BuildCaddyConfigForZone(baseDomain string, creds map[string]string) map[string]any {
|
||||
return p.BuildCaddyConfig(creds)
|
||||
}
|
||||
|
||||
func (p *NamecheapProvider) PropagationTimeout() time.Duration {
|
||||
return 300 * time.Second
|
||||
}
|
||||
|
||||
func (p *NamecheapProvider) PollingInterval() time.Duration {
|
||||
return 15 * time.Second
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
package builtin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/Wikid82/charon/backend/pkg/dnsprovider"
|
||||
)
|
||||
|
||||
// Route53Provider implements the ProviderPlugin interface for AWS Route53.
|
||||
type Route53Provider struct{}
|
||||
|
||||
func (p *Route53Provider) Type() string {
|
||||
return "route53"
|
||||
}
|
||||
|
||||
func (p *Route53Provider) Metadata() dnsprovider.ProviderMetadata {
|
||||
return dnsprovider.ProviderMetadata{
|
||||
Type: "route53",
|
||||
Name: "AWS Route53",
|
||||
Description: "Amazon Route53 DNS with IAM credentials",
|
||||
DocumentationURL: "https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/",
|
||||
IsBuiltIn: true,
|
||||
Version: "1.0.0",
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Route53Provider) Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Route53Provider) Cleanup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Route53Provider) RequiredCredentialFields() []dnsprovider.CredentialFieldSpec {
|
||||
return []dnsprovider.CredentialFieldSpec{
|
||||
{
|
||||
Name: "access_key_id",
|
||||
Label: "Access Key ID",
|
||||
Type: "text",
|
||||
Placeholder: "Enter your AWS Access Key ID",
|
||||
Hint: "IAM user with Route53 permissions",
|
||||
},
|
||||
{
|
||||
Name: "secret_access_key",
|
||||
Label: "Secret Access Key",
|
||||
Type: "password",
|
||||
Placeholder: "Enter your AWS Secret Access Key",
|
||||
Hint: "Stored encrypted",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Route53Provider) OptionalCredentialFields() []dnsprovider.CredentialFieldSpec {
|
||||
return []dnsprovider.CredentialFieldSpec{
|
||||
{
|
||||
Name: "region",
|
||||
Label: "AWS Region",
|
||||
Type: "text",
|
||||
Placeholder: "us-east-1",
|
||||
Hint: "AWS region (default: us-east-1)",
|
||||
},
|
||||
{
|
||||
Name: "hosted_zone_id",
|
||||
Label: "Hosted Zone ID",
|
||||
Type: "text",
|
||||
Placeholder: "Z1234567890ABC",
|
||||
Hint: "Optional: Specific hosted zone ID",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Route53Provider) ValidateCredentials(creds map[string]string) error {
|
||||
if creds["access_key_id"] == "" {
|
||||
return fmt.Errorf("access_key_id is required")
|
||||
}
|
||||
if creds["secret_access_key"] == "" {
|
||||
return fmt.Errorf("secret_access_key is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Route53Provider) TestCredentials(creds map[string]string) error {
|
||||
return p.ValidateCredentials(creds)
|
||||
}
|
||||
|
||||
func (p *Route53Provider) SupportsMultiCredential() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Route53Provider) BuildCaddyConfig(creds map[string]string) map[string]any {
|
||||
config := map[string]any{
|
||||
"name": "route53",
|
||||
"access_key_id": creds["access_key_id"],
|
||||
"secret_access_key": creds["secret_access_key"],
|
||||
}
|
||||
if region := creds["region"]; region != "" {
|
||||
config["region"] = region
|
||||
}
|
||||
if zoneID := creds["hosted_zone_id"]; zoneID != "" {
|
||||
config["hosted_zone_id"] = zoneID
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
func (p *Route53Provider) BuildCaddyConfigForZone(baseDomain string, creds map[string]string) map[string]any {
|
||||
return p.BuildCaddyConfig(creds)
|
||||
}
|
||||
|
||||
func (p *Route53Provider) PropagationTimeout() time.Duration {
|
||||
return 180 * time.Second
|
||||
}
|
||||
|
||||
func (p *Route53Provider) PollingInterval() time.Duration {
|
||||
return 10 * time.Second
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package builtin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/Wikid82/charon/backend/pkg/dnsprovider"
|
||||
)
|
||||
|
||||
// VultrProvider implements the ProviderPlugin interface for Vultr DNS.
|
||||
type VultrProvider struct{}
|
||||
|
||||
func (p *VultrProvider) Type() string {
|
||||
return "vultr"
|
||||
}
|
||||
|
||||
func (p *VultrProvider) Metadata() dnsprovider.ProviderMetadata {
|
||||
return dnsprovider.ProviderMetadata{
|
||||
Type: "vultr",
|
||||
Name: "Vultr",
|
||||
Description: "Vultr DNS with API key authentication",
|
||||
DocumentationURL: "https://www.vultr.com/api/#tag/dns",
|
||||
IsBuiltIn: true,
|
||||
Version: "1.0.0",
|
||||
}
|
||||
}
|
||||
|
||||
func (p *VultrProvider) Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *VultrProvider) Cleanup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *VultrProvider) RequiredCredentialFields() []dnsprovider.CredentialFieldSpec {
|
||||
return []dnsprovider.CredentialFieldSpec{
|
||||
{
|
||||
Name: "api_key",
|
||||
Label: "API Key",
|
||||
Type: "password",
|
||||
Placeholder: "Enter your Vultr API key",
|
||||
Hint: "Generate from Vultr account settings",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *VultrProvider) OptionalCredentialFields() []dnsprovider.CredentialFieldSpec {
|
||||
return []dnsprovider.CredentialFieldSpec{}
|
||||
}
|
||||
|
||||
func (p *VultrProvider) ValidateCredentials(creds map[string]string) error {
|
||||
if creds["api_key"] == "" {
|
||||
return fmt.Errorf("api_key is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *VultrProvider) TestCredentials(creds map[string]string) error {
|
||||
return p.ValidateCredentials(creds)
|
||||
}
|
||||
|
||||
func (p *VultrProvider) SupportsMultiCredential() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *VultrProvider) BuildCaddyConfig(creds map[string]string) map[string]any {
|
||||
return map[string]any{
|
||||
"name": "vultr",
|
||||
"api_key": creds["api_key"],
|
||||
}
|
||||
}
|
||||
|
||||
func (p *VultrProvider) BuildCaddyConfigForZone(baseDomain string, creds map[string]string) map[string]any {
|
||||
return p.BuildCaddyConfig(creds)
|
||||
}
|
||||
|
||||
func (p *VultrProvider) PropagationTimeout() time.Duration {
|
||||
return 120 * time.Second
|
||||
}
|
||||
|
||||
func (p *VultrProvider) PollingInterval() time.Duration {
|
||||
return 5 * time.Second
|
||||
}
|
||||
Reference in New Issue
Block a user