feat: add custom locations management to ProxyHostForm

- Updated ProxyHostForm to include functionality for managing custom locations.
- Introduced add, remove, and update operations for locations in the form.
- Modified the ProxyHost interface to include an array of locations.
- Removed the advanced configuration textarea in favor of a more structured location input.
- Updated the frontend assets in index.html to reflect the latest build.
This commit is contained in:
Wikid82
2025-11-19 11:46:26 -05:00
parent d559a24c45
commit 90ba956d97
21 changed files with 362 additions and 233 deletions
+37 -31
View File
@@ -96,49 +96,55 @@ func main() {
// Seed Proxy Hosts
proxyHosts := []models.ProxyHost{
{
UUID: uuid.NewString(),
Name: "Development App",
Domain: "app.local.dev",
TargetScheme: "http",
TargetHost: "localhost",
TargetPort: 3000,
EnableTLS: false,
EnableWS: true,
Enabled: true,
UUID: uuid.NewString(),
Name: "Development App",
DomainNames: "app.local.dev",
ForwardScheme: "http",
ForwardHost: "localhost",
ForwardPort: 3000,
SSLForced: false,
WebsocketSupport: true,
HSTSEnabled: false,
BlockExploits: true,
Enabled: true,
},
{
UUID: uuid.NewString(),
Name: "API Server",
Domain: "api.local.dev",
TargetScheme: "http",
TargetHost: "192.168.1.100",
TargetPort: 8080,
EnableTLS: false,
EnableWS: false,
Enabled: true,
UUID: uuid.NewString(),
Name: "API Server",
DomainNames: "api.local.dev",
ForwardScheme: "http",
ForwardHost: "192.168.1.100",
ForwardPort: 8080,
SSLForced: false,
WebsocketSupport: false,
HSTSEnabled: false,
BlockExploits: true,
Enabled: true,
},
{
UUID: uuid.NewString(),
Name: "Docker Registry",
Domain: "docker.local.dev",
TargetScheme: "http",
TargetHost: "localhost",
TargetPort: 5000,
EnableTLS: false,
EnableWS: false,
Enabled: false,
UUID: uuid.NewString(),
Name: "Docker Registry",
DomainNames: "docker.local.dev",
ForwardScheme: "http",
ForwardHost: "localhost",
ForwardPort: 5000,
SSLForced: false,
WebsocketSupport: false,
HSTSEnabled: false,
BlockExploits: true,
Enabled: false,
},
}
for _, host := range proxyHosts {
result := db.Where("domain = ?", host.Domain).FirstOrCreate(&host)
result := db.Where("domain_names = ?", host.DomainNames).FirstOrCreate(&host)
if result.Error != nil {
log.Printf("Failed to seed proxy host %s: %v", host.Domain, result.Error)
log.Printf("Failed to seed proxy host %s: %v", host.DomainNames, result.Error)
} else if result.RowsAffected > 0 {
fmt.Printf("✓ Created proxy host: %s -> %s://%s:%d\n",
host.Domain, host.TargetScheme, host.TargetHost, host.TargetPort)
host.DomainNames, host.ForwardScheme, host.ForwardHost, host.ForwardPort)
} else {
fmt.Printf(" Proxy host already exists: %s\n", host.Domain)
fmt.Printf(" Proxy host already exists: %s\n", host.DomainNames)
}
}
+15 -14
View File
@@ -26,6 +26,7 @@ func setupTestDB() *gorm.DB {
// Auto migrate
db.AutoMigrate(
&models.ProxyHost{},
&models.Location{},
&models.RemoteServer{},
&models.ImportSession{},
)
@@ -137,13 +138,13 @@ func TestProxyHostHandler_List(t *testing.T) {
// Create test proxy host
host := &models.ProxyHost{
UUID: uuid.NewString(),
Name: "Test Host",
Domain: "test.local",
TargetScheme: "http",
TargetHost: "localhost",
TargetPort: 3000,
Enabled: true,
UUID: uuid.NewString(),
Name: "Test Host",
DomainNames: "test.local",
ForwardScheme: "http",
ForwardHost: "localhost",
ForwardPort: 3000,
Enabled: true,
}
db.Create(host)
@@ -175,12 +176,12 @@ func TestProxyHostHandler_Create(t *testing.T) {
// Test Create
hostData := map[string]interface{}{
"name": "New Host",
"domain": "new.local",
"target_scheme": "http",
"target_host": "192.168.1.200",
"target_port": 8080,
"enabled": true,
"name": "New Host",
"domain_names": "new.local",
"forward_scheme": "http",
"forward_host": "192.168.1.200",
"forward_port": 8080,
"enabled": true,
}
body, _ := json.Marshal(hostData)
@@ -195,7 +196,7 @@ func TestProxyHostHandler_Create(t *testing.T) {
err := json.Unmarshal(w.Body.Bytes(), &host)
assert.NoError(t, err)
assert.Equal(t, "New Host", host.Name)
assert.Equal(t, "new.local", host.Domain)
assert.Equal(t, "new.local", host.DomainNames)
assert.NotEmpty(t, host.UUID)
}
@@ -157,7 +157,7 @@ func (h *ImportHandler) Commit(c *gin.Context) {
errors := []string{}
for _, host := range proxyHosts {
action := req.Resolutions[host.Domain]
action := req.Resolutions[host.DomainNames]
if action == "skip" {
skipped++
@@ -165,13 +165,13 @@ func (h *ImportHandler) Commit(c *gin.Context) {
}
if action == "rename" {
host.Domain = host.Domain + "-imported"
host.DomainNames = host.DomainNames + "-imported"
}
host.UUID = uuid.NewString()
if err := h.proxyHostSvc.Create(&host); err != nil {
errors = append(errors, fmt.Sprintf("%s: %s", host.Domain, err.Error()))
errors = append(errors, fmt.Sprintf("%s: %s", host.DomainNames, err.Error()))
} else {
created++
}
@@ -228,13 +228,13 @@ func (h *ImportHandler) processImport(caddyfilePath, originalName string) error
existingHosts, _ := h.proxyHostSvc.List()
existingDomains := make(map[string]bool)
for _, host := range existingHosts {
existingDomains[host.Domain] = true
existingDomains[host.DomainNames] = true
}
for _, parsed := range result.Hosts {
if existingDomains[parsed.Domain] {
if existingDomains[parsed.DomainNames] {
result.Conflicts = append(result.Conflicts,
fmt.Sprintf("Domain '%s' already exists in CPM+", parsed.Domain))
fmt.Sprintf("Domain '%s' already exists in CPM+", parsed.DomainNames))
}
}
@@ -53,6 +53,11 @@ func (h *ProxyHostHandler) Create(c *gin.Context) {
host.UUID = uuid.NewString()
// Assign UUIDs to locations
for i := range host.Locations {
host.Locations[i].UUID = uuid.NewString()
}
if err := h.service.Create(&host); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
@@ -20,7 +20,7 @@ func setupTestRouter(t *testing.T) (*gin.Engine, *gorm.DB) {
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.ProxyHost{}))
require.NoError(t, db.AutoMigrate(&models.ProxyHost{}, &models.Location{}))
h := NewProxyHostHandler(db)
r := gin.New()
@@ -33,7 +33,7 @@ func setupTestRouter(t *testing.T) (*gin.Engine, *gorm.DB) {
func TestProxyHostLifecycle(t *testing.T) {
router, _ := setupTestRouter(t)
body := `{"name":"Media","domain":"media.example.com","target_scheme":"http","target_host":"media","target_port":32400}`
body := `{"name":"Media","domain_names":"media.example.com","forward_scheme":"http","forward_host":"media","forward_port":32400,"enabled":true}`
req := httptest.NewRequest(http.MethodPost, "/api/v1/proxy-hosts", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
@@ -43,7 +43,7 @@ func TestProxyHostLifecycle(t *testing.T) {
var created models.ProxyHost
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &created))
require.Equal(t, "media.example.com", created.Domain)
require.Equal(t, "media.example.com", created.DomainNames)
listReq := httptest.NewRequest(http.MethodGet, "/api/v1/proxy-hosts", nil)
listResp := httptest.NewRecorder()
+1
View File
@@ -15,6 +15,7 @@ func Register(router *gin.Engine, db *gorm.DB) error {
// AutoMigrate all models for Issue #5 persistence layer
if err := db.AutoMigrate(
&models.ProxyHost{},
&models.Location{},
&models.CaddyConfig{},
&models.RemoteServer{},
&models.SSLCertificate{},
+5 -4
View File
@@ -24,10 +24,11 @@ func TestClient_Load_Success(t *testing.T) {
client := NewClient(server.URL)
config, _ := GenerateConfig([]models.ProxyHost{
{
UUID: "test",
Domain: "test.com",
TargetHost: "app",
TargetPort: 8080,
UUID: "test",
DomainNames: "test.com",
ForwardHost: "app",
ForwardPort: 8080,
Enabled: true,
},
})
+56 -9
View File
@@ -2,6 +2,7 @@ package caddy
import (
"fmt"
"strings"
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
)
@@ -19,22 +20,69 @@ func GenerateConfig(hosts []models.ProxyHost) (*Config, error) {
}, nil
}
routes := make([]*Route, 0, len(hosts))
routes := make([]*Route, 0)
for _, host := range hosts {
if host.Domain == "" {
return nil, fmt.Errorf("proxy host %s has empty domain", host.UUID)
if !host.Enabled {
continue
}
dial := fmt.Sprintf("%s:%d", host.TargetHost, host.TargetPort)
if host.DomainNames == "" {
return nil, fmt.Errorf("proxy host %s has empty domain names", host.UUID)
}
// Parse comma-separated domains
domains := strings.Split(host.DomainNames, ",")
for i := range domains {
domains[i] = strings.TrimSpace(domains[i])
}
// Build handlers for this host
handlers := make([]Handler, 0)
// Add HSTS header if enabled
if host.HSTSEnabled {
hstsValue := "max-age=31536000"
if host.HSTSSubdomains {
hstsValue += "; includeSubDomains"
}
handlers = append(handlers, HeaderHandler(map[string][]string{
"Strict-Transport-Security": {hstsValue},
}))
}
// Add exploit blocking if enabled
if host.BlockExploits {
handlers = append(handlers, BlockExploitsHandler())
}
// Handle custom locations first (more specific routes)
for _, loc := range host.Locations {
dial := fmt.Sprintf("%s:%d", loc.ForwardHost, loc.ForwardPort)
locRoute := &Route{
Match: []Match{
{
Host: domains,
Path: []string{loc.Path, loc.Path + "/*"},
},
},
Handle: []Handler{
ReverseProxyHandler(dial, host.WebsocketSupport),
},
Terminal: true,
}
routes = append(routes, locRoute)
}
// Main proxy handler
dial := fmt.Sprintf("%s:%d", host.ForwardHost, host.ForwardPort)
mainHandlers := append(handlers, ReverseProxyHandler(dial, host.WebsocketSupport))
route := &Route{
Match: []Match{
{Host: []string{host.Domain}},
},
Handle: []Handler{
ReverseProxyHandler(dial, host.EnableWS),
{Host: domains},
},
Handle: mainHandlers,
Terminal: true,
}
@@ -49,7 +97,6 @@ func GenerateConfig(hosts []models.ProxyHost) (*Config, error) {
Listen: []string{":80", ":443"},
Routes: routes,
AutoHTTPS: &AutoHTTPSConfig{
// Enable automatic HTTPS by default
Disable: false,
},
},
+30 -25
View File
@@ -19,14 +19,15 @@ func TestGenerateConfig_Empty(t *testing.T) {
func TestGenerateConfig_SingleHost(t *testing.T) {
hosts := []models.ProxyHost{
{
UUID: "test-uuid",
Name: "Media",
Domain: "media.example.com",
TargetScheme: "http",
TargetHost: "media",
TargetPort: 32400,
EnableTLS: true,
EnableWS: false,
UUID: "test-uuid",
Name: "Media",
DomainNames: "media.example.com",
ForwardScheme: "http",
ForwardHost: "media",
ForwardPort: 32400,
SSLForced: true,
WebsocketSupport: false,
Enabled: true,
},
}
@@ -55,16 +56,18 @@ func TestGenerateConfig_SingleHost(t *testing.T) {
func TestGenerateConfig_MultipleHosts(t *testing.T) {
hosts := []models.ProxyHost{
{
UUID: "uuid-1",
Domain: "site1.example.com",
TargetHost: "app1",
TargetPort: 8080,
UUID: "uuid-1",
DomainNames: "site1.example.com",
ForwardHost: "app1",
ForwardPort: 8080,
Enabled: true,
},
{
UUID: "uuid-2",
Domain: "site2.example.com",
TargetHost: "app2",
TargetPort: 8081,
UUID: "uuid-2",
DomainNames: "site2.example.com",
ForwardHost: "app2",
ForwardPort: 8081,
Enabled: true,
},
}
@@ -76,11 +79,12 @@ func TestGenerateConfig_MultipleHosts(t *testing.T) {
func TestGenerateConfig_WebSocketEnabled(t *testing.T) {
hosts := []models.ProxyHost{
{
UUID: "uuid-ws",
Domain: "ws.example.com",
TargetHost: "wsapp",
TargetPort: 3000,
EnableWS: true,
UUID: "uuid-ws",
DomainNames: "ws.example.com",
ForwardHost: "wsapp",
ForwardPort: 3000,
WebsocketSupport: true,
Enabled: true,
},
}
@@ -97,10 +101,11 @@ func TestGenerateConfig_WebSocketEnabled(t *testing.T) {
func TestGenerateConfig_EmptyDomain(t *testing.T) {
hosts := []models.ProxyHost{
{
UUID: "bad-uuid",
Domain: "",
TargetHost: "app",
TargetPort: 8080,
UUID: "bad-uuid",
DomainNames: "",
ForwardHost: "app",
ForwardPort: 8080,
Enabled: true,
},
}
+24 -24
View File
@@ -53,14 +53,14 @@ type CaddyHandler struct {
// ParsedHost represents a single host detected during Caddyfile import.
type ParsedHost struct {
Domain string `json:"domain"`
TargetScheme string `json:"target_scheme"`
TargetHost string `json:"target_host"`
TargetPort int `json:"target_port"`
EnableTLS bool `json:"enable_tls"`
EnableWS bool `json:"enable_websockets"`
RawJSON string `json:"raw_json"` // Original Caddy JSON for this route
Warnings []string `json:"warnings"` // Unsupported features
DomainNames string `json:"domain_names"`
ForwardScheme string `json:"forward_scheme"`
ForwardHost string `json:"forward_host"`
ForwardPort int `json:"forward_port"`
SSLForced bool `json:"ssl_forced"`
WebsocketSupport bool `json:"websocket_support"`
RawJSON string `json:"raw_json"` // Original Caddy JSON for this route
Warnings []string `json:"warnings"` // Unsupported features
}
// ImportResult contains parsed hosts and detected conflicts.
@@ -133,8 +133,8 @@ func (i *Importer) ExtractHosts(caddyJSON []byte) (*ImportResult, error) {
// Extract reverse proxy handler
host := ParsedHost{
Domain: domain,
EnableTLS: strings.HasPrefix(domain, "https") || server.TLSConnectionPolicies != nil,
DomainNames: domain,
SSLForced: strings.HasPrefix(domain, "https") || server.TLSConnectionPolicies != nil,
}
// Find reverse_proxy handler
@@ -147,8 +147,8 @@ func (i *Importer) ExtractHosts(caddyJSON []byte) (*ImportResult, error) {
if dial != "" {
parts := strings.Split(dial, ":")
if len(parts) == 2 {
host.TargetHost = parts[0]
fmt.Sscanf(parts[1], "%d", &host.TargetPort)
host.ForwardHost = parts[0]
fmt.Sscanf(parts[1], "%d", &host.ForwardPort)
}
}
}
@@ -159,7 +159,7 @@ func (i *Importer) ExtractHosts(caddyJSON []byte) (*ImportResult, error) {
if upgrade, ok := headers["Upgrade"].([]interface{}); ok {
for _, v := range upgrade {
if v == "websocket" {
host.EnableWS = true
host.WebsocketSupport = true
break
}
}
@@ -167,9 +167,9 @@ func (i *Importer) ExtractHosts(caddyJSON []byte) (*ImportResult, error) {
}
// Default scheme
host.TargetScheme = "http"
if host.EnableTLS {
host.TargetScheme = "https"
host.ForwardScheme = "http"
if host.SSLForced {
host.ForwardScheme = "https"
}
}
@@ -214,18 +214,18 @@ func ConvertToProxyHosts(parsedHosts []ParsedHost) []models.ProxyHost {
hosts := make([]models.ProxyHost, 0, len(parsedHosts))
for _, parsed := range parsedHosts {
if parsed.TargetHost == "" || parsed.TargetPort == 0 {
if parsed.ForwardHost == "" || parsed.ForwardPort == 0 {
continue // Skip invalid entries
}
hosts = append(hosts, models.ProxyHost{
Name: parsed.Domain, // Can be customized by user during review
Domain: parsed.Domain,
TargetScheme: parsed.TargetScheme,
TargetHost: parsed.TargetHost,
TargetPort: parsed.TargetPort,
EnableTLS: parsed.EnableTLS,
EnableWS: parsed.EnableWS,
Name: parsed.DomainNames, // Can be customized by user during review
DomainNames: parsed.DomainNames,
ForwardScheme: parsed.ForwardScheme,
ForwardHost: parsed.ForwardHost,
ForwardPort: parsed.ForwardPort,
SSLForced: parsed.SSLForced,
WebsocketSupport: parsed.WebsocketSupport,
})
}
+20
View File
@@ -78,6 +78,26 @@ func ReverseProxyHandler(dial string, enableWS bool) Handler {
return h
}
// HeaderHandler creates a handler that sets HTTP response headers.
func HeaderHandler(headers map[string][]string) Handler {
return Handler{
"handler": "headers",
"response": map[string]interface{}{
"set": headers,
},
}
}
// BlockExploitsHandler creates a handler that blocks common exploits.
// This uses Caddy's request matchers to block malicious patterns.
func BlockExploitsHandler() Handler {
return Handler{
"handler": "vars",
// Placeholder for future exploit blocking logic
// Can be extended with specific matchers for SQL injection, XSS, etc.
}
}
// TLSApp configures the TLS app for certificate management.
type TLSApp struct {
Automation *AutomationConfig `json:"automation,omitempty"`
+5 -4
View File
@@ -17,10 +17,11 @@ func TestValidate_EmptyConfig(t *testing.T) {
func TestValidate_ValidConfig(t *testing.T) {
hosts := []models.ProxyHost{
{
UUID: "test",
Domain: "test.example.com",
TargetHost: "app",
TargetPort: 8080,
UUID: "test",
DomainNames: "test.example.com",
ForwardHost: "10.0.1.100",
ForwardPort: 8080,
Enabled: true,
},
}
+18
View File
@@ -0,0 +1,18 @@
package models
import (
"time"
)
// Location represents a custom path-based proxy configuration within a ProxyHost.
type Location struct {
ID uint `json:"id" gorm:"primaryKey"`
UUID string `json:"uuid" gorm:"uniqueIndex;not null"`
ProxyHostID uint `json:"proxy_host_id" gorm:"not null;index"`
Path string `json:"path" gorm:"not null"` // e.g., /api, /admin
ForwardScheme string `json:"forward_scheme" gorm:"default:http"`
ForwardHost string `json:"forward_host" gorm:"not null"`
ForwardPort int `json:"forward_port" gorm:"not null"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
+18 -13
View File
@@ -4,18 +4,23 @@ import (
"time"
)
// ProxyHost represents a reverse proxy configuration for a single domain.
// ProxyHost represents a reverse proxy configuration.
type ProxyHost struct {
ID uint `json:"id" gorm:"primaryKey"`
UUID string `json:"uuid" gorm:"uniqueIndex"`
Name string `json:"name"`
Domain string `json:"domain" gorm:"uniqueIndex"`
TargetScheme string `json:"target_scheme"` // http/https
TargetHost string `json:"target_host"`
TargetPort int `json:"target_port"`
EnableTLS bool `json:"enable_tls" gorm:"default:false"`
EnableWS bool `json:"enable_websockets" gorm:"default:false"`
Enabled bool `json:"enabled" gorm:"default:true"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID uint `json:"id" gorm:"primaryKey"`
UUID string `json:"uuid" gorm:"uniqueIndex;not null"`
Name string `json:"name"`
DomainNames string `json:"domain_names" gorm:"not null"` // Comma-separated list
ForwardScheme string `json:"forward_scheme" gorm:"default:http"`
ForwardHost string `json:"forward_host" gorm:"not null"`
ForwardPort int `json:"forward_port" gorm:"not null"`
SSLForced bool `json:"ssl_forced" gorm:"default:false"`
HTTP2Support bool `json:"http2_support" gorm:"default:true"`
HSTSEnabled bool `json:"hsts_enabled" gorm:"default:false"`
HSTSSubdomains bool `json:"hsts_subdomains" gorm:"default:false"`
BlockExploits bool `json:"block_exploits" gorm:"default:true"`
WebsocketSupport bool `json:"websocket_support" gorm:"default:false"`
Enabled bool `json:"enabled" gorm:"default:true"`
Locations []Location `json:"locations" gorm:"foreignKey:ProxyHostID;constraint:OnDelete:CASCADE"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
@@ -20,9 +20,9 @@ func NewProxyHostService(db *gorm.DB) *ProxyHostService {
}
// ValidateUniqueDomain ensures no duplicate domains exist before creation/update.
func (s *ProxyHostService) ValidateUniqueDomain(domain string, excludeID uint) error {
func (s *ProxyHostService) ValidateUniqueDomain(domainNames string, excludeID uint) error {
var count int64
query := s.db.Model(&models.ProxyHost{}).Where("domain = ?", domain)
query := s.db.Model(&models.ProxyHost{}).Where("domain_names = ?", domainNames)
if excludeID > 0 {
query = query.Where("id != ?", excludeID)
@@ -41,7 +41,7 @@ func (s *ProxyHostService) ValidateUniqueDomain(domain string, excludeID uint) e
// Create validates and creates a new proxy host.
func (s *ProxyHostService) Create(host *models.ProxyHost) error {
if err := s.ValidateUniqueDomain(host.Domain, 0); err != nil {
if err := s.ValidateUniqueDomain(host.DomainNames, 0); err != nil {
return err
}
@@ -50,7 +50,7 @@ func (s *ProxyHostService) Create(host *models.ProxyHost) error {
// Update validates and updates an existing proxy host.
func (s *ProxyHostService) Update(host *models.ProxyHost) error {
if err := s.ValidateUniqueDomain(host.Domain, host.ID); err != nil {
if err := s.ValidateUniqueDomain(host.DomainNames, host.ID); err != nil {
return err
}
@@ -71,19 +71,19 @@ func (s *ProxyHostService) GetByID(id uint) (*models.ProxyHost, error) {
return &host, nil
}
// GetByUUID retrieves a proxy host by UUID.
// GetByUUID finds a proxy host by UUID.
func (s *ProxyHostService) GetByUUID(uuid string) (*models.ProxyHost, error) {
var host models.ProxyHost
if err := s.db.Where("uuid = ?", uuid).First(&host).Error; err != nil {
if err := s.db.Preload("Locations").Where("uuid = ?", uuid).First(&host).Error; err != nil {
return nil, err
}
return &host, nil
}
// List retrieves all proxy hosts.
// List returns all proxy hosts.
func (s *ProxyHostService) List() ([]models.ProxyHost, error) {
var hosts []models.ProxyHost
if err := s.db.Find(&hosts).Error; err != nil {
if err := s.db.Preload("Locations").Order("updated_at desc").Find(&hosts).Error; err != nil {
return nil, err
}
return hosts, nil
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -5,8 +5,8 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Caddy Proxy Manager+</title>
<script type="module" crossorigin src="/assets/index-Y4LKIHSS.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Be7wNiFg.css">
<script type="module" crossorigin src="/assets/index-B1xdYwH2.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DDHvSYJS.css">
</head>
<body>
<div id="root"></div>
+100 -13
View File
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'
import { ProxyHost } from '../hooks/useProxyHosts'
import { ProxyHost, Location } from '../hooks/useProxyHosts'
import { remoteServersAPI } from '../services/api'
interface ProxyHostFormProps {
@@ -29,7 +29,7 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
hsts_subdomains: host?.hsts_subdomains ?? false,
block_exploits: host?.block_exploits ?? true,
websocket_support: host?.websocket_support ?? false,
advanced_config: host?.advanced_config || '',
locations: host?.locations || [] as Location[],
enabled: host?.enabled ?? true,
})
@@ -49,6 +49,28 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
fetchServers()
}, [])
const addLocation = () => {
setFormData({
...formData,
locations: [
...formData.locations,
{ path: '/', forward_scheme: 'http', forward_host: '', forward_port: 80 }
]
})
}
const removeLocation = (index: number) => {
const newLocations = [...formData.locations]
newLocations.splice(index, 1)
setFormData({ ...formData, locations: newLocations })
}
const updateLocation = (index: number, field: keyof Location, value: any) => {
const newLocations = [...formData.locations]
newLocations[index] = { ...newLocations[index], [field]: value }
setFormData({ ...formData, locations: newLocations })
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
@@ -231,18 +253,83 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
</label>
</div>
{/* Advanced Config */}
{/* Custom Locations */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Advanced Caddy Config (Optional)
</label>
<textarea
value={formData.advanced_config}
onChange={e => setFormData({ ...formData, advanced_config: e.target.value })}
placeholder="Additional Caddy directives..."
rows={4}
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<div className="flex items-center justify-between mb-4">
<label className="block text-sm font-medium text-gray-300">
Custom Locations
</label>
<button
type="button"
onClick={addLocation}
className="px-3 py-1 bg-gray-700 hover:bg-gray-600 text-white text-sm rounded transition-colors"
>
Add Location
</button>
</div>
<div className="space-y-4">
{formData.locations.map((location, index) => (
<div key={index} className="bg-gray-900/50 p-4 rounded-lg border border-gray-700">
<div className="grid grid-cols-12 gap-4 mb-2">
<div className="col-span-4">
<label className="block text-xs text-gray-400 mb-1">Path</label>
<input
type="text"
value={location.path}
onChange={e => updateLocation(index, 'path', e.target.value)}
placeholder="/api"
className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-1 text-sm text-white focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</div>
<div className="col-span-2">
<label className="block text-xs text-gray-400 mb-1">Scheme</label>
<select
value={location.forward_scheme}
onChange={e => updateLocation(index, 'forward_scheme', e.target.value)}
className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-1 text-sm text-white focus:outline-none focus:ring-1 focus:ring-blue-500"
>
<option value="http">HTTP</option>
<option value="https">HTTPS</option>
</select>
</div>
<div className="col-span-4">
<label className="block text-xs text-gray-400 mb-1">Forward Host</label>
<input
type="text"
value={location.forward_host}
onChange={e => updateLocation(index, 'forward_host', e.target.value)}
placeholder="10.0.0.1"
className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-1 text-sm text-white focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</div>
<div className="col-span-2">
<label className="block text-xs text-gray-400 mb-1">Port</label>
<input
type="number"
value={location.forward_port}
onChange={e => updateLocation(index, 'forward_port', parseInt(e.target.value))}
className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-1 text-sm text-white focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</div>
</div>
<div className="flex justify-end">
<button
type="button"
onClick={() => removeLocation(index)}
className="text-red-400 hover:text-red-300 text-xs"
>
Remove Location
</button>
</div>
</div>
))}
{formData.locations.length === 0 && (
<div className="text-center text-gray-500 text-sm py-4 border border-dashed border-gray-700 rounded-lg">
No custom locations defined
</div>
)}
</div>
</div>
{/* Actions */}
+9 -1
View File
@@ -1,6 +1,14 @@
import { useState, useEffect } from 'react'
import { proxyHostsAPI } from '../services/api'
export interface Location {
uuid?: string
path: string
forward_scheme: string
forward_host: string
forward_port: number
}
export interface ProxyHost {
uuid: string
domain_names: string
@@ -15,7 +23,7 @@ export interface ProxyHost {
hsts_subdomains: boolean
block_exploits: boolean
websocket_support: boolean
advanced_config?: string
locations: Location[]
enabled: boolean
created_at: string
updated_at: string