diff --git a/backend/internal/api/handlers/domain_handler.go b/backend/internal/api/handlers/domain_handler.go new file mode 100644 index 00000000..c9945674 --- /dev/null +++ b/backend/internal/api/handlers/domain_handler.go @@ -0,0 +1,57 @@ +package handlers + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models" + "gorm.io/gorm" +) + +type DomainHandler struct { + DB *gorm.DB +} + +func NewDomainHandler(db *gorm.DB) *DomainHandler { + return &DomainHandler{DB: db} +} + +func (h *DomainHandler) List(c *gin.Context) { + var domains []models.Domain + if err := h.DB.Order("name asc").Find(&domains).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch domains"}) + return + } + c.JSON(http.StatusOK, domains) +} + +func (h *DomainHandler) Create(c *gin.Context) { + var input struct { + Name string `json:"name" binding:"required"` + } + + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + domain := models.Domain{ + Name: input.Name, + } + + if err := h.DB.Create(&domain).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create domain"}) + return + } + + c.JSON(http.StatusCreated, domain) +} + +func (h *DomainHandler) Delete(c *gin.Context) { + id := c.Param("id") + if err := h.DB.Where("uuid = ?", id).Delete(&models.Domain{}).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete domain"}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "Domain deleted"}) +} diff --git a/backend/internal/api/routes/routes.go b/backend/internal/api/routes/routes.go index 13875218..bd32d9dd 100644 --- a/backend/internal/api/routes/routes.go +++ b/backend/internal/api/routes/routes.go @@ -28,6 +28,7 @@ func Register(router *gin.Engine, db *gorm.DB, cfg config.Config) error { &models.Setting{}, &models.ImportSession{}, &models.Notification{}, + &models.Domain{}, ); err != nil { return fmt.Errorf("auto migrate: %w", err) } @@ -94,6 +95,12 @@ func Register(router *gin.Engine, db *gorm.DB, cfg config.Config) error { protected.POST("/notifications/:id/read", notificationHandler.MarkAsRead) protected.POST("/notifications/read-all", notificationHandler.MarkAllAsRead) + // Domains + domainHandler := handlers.NewDomainHandler(db) + protected.GET("/domains", domainHandler.List) + protected.POST("/domains", domainHandler.Create) + protected.DELETE("/domains/:id", domainHandler.Delete) + // Docker dockerService, err := services.NewDockerService() if err == nil { // Only register if Docker is available diff --git a/backend/internal/models/domain.go b/backend/internal/models/domain.go new file mode 100644 index 00000000..76b0945f --- /dev/null +++ b/backend/internal/models/domain.go @@ -0,0 +1,24 @@ +package models + +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type Domain struct { + ID uint `json:"id" gorm:"primarykey"` + UUID string `json:"uuid" gorm:"uniqueIndex;not null"` + Name string `json:"name" gorm:"uniqueIndex;not null"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"` +} + +func (d *Domain) BeforeCreate(tx *gorm.DB) (err error) { + if d.UUID == "" { + d.UUID = uuid.New().String() + } + return +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d129c24a..abb72698 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -15,6 +15,7 @@ import SystemSettings from './pages/SystemSettings' import Account from './pages/Account' import Backups from './pages/Backups' import Logs from './pages/Logs' +import Domains from './pages/Domains' import Login from './pages/Login' import Setup from './pages/Setup' @@ -37,6 +38,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> diff --git a/frontend/src/api/domains.ts b/frontend/src/api/domains.ts new file mode 100644 index 00000000..0e1c1fbc --- /dev/null +++ b/frontend/src/api/domains.ts @@ -0,0 +1,22 @@ +import client from './client' + +export interface Domain { + id: number + uuid: string + name: string + created_at: string +} + +export const getDomains = async (): Promise => { + const { data } = await client.get('/domains') + return data +} + +export const createDomain = async (name: string): Promise => { + const { data } = await client.post('/domains', { name }) + return data +} + +export const deleteDomain = async (uuid: string): Promise => { + await client.delete(`/domains/${uuid}`) +} diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 374c960d..1c97c9d7 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -36,6 +36,7 @@ export default function Layout({ children }: LayoutProps) { { name: 'Dashboard', path: '/', icon: '📊' }, { name: 'Proxy Hosts', path: '/proxy-hosts', icon: '🌐' }, { name: 'Remote Servers', path: '/remote-servers', icon: '🖥️' }, + { name: 'Domains', path: '/domains', icon: '🌍' }, { name: 'Certificates', path: '/certificates', icon: '🔒' }, { name: 'Import Caddyfile', path: '/import', icon: '📥' }, { name: 'Settings', path: '/settings/system', icon: '⚙️' }, diff --git a/frontend/src/components/ProxyHostForm.tsx b/frontend/src/components/ProxyHostForm.tsx index 7633f021..b6578a17 100644 --- a/frontend/src/components/ProxyHostForm.tsx +++ b/frontend/src/components/ProxyHostForm.tsx @@ -2,6 +2,7 @@ import { useState } from 'react' import { CircleHelp } from 'lucide-react' import type { ProxyHost } from '../api/proxyHosts' import { useRemoteServers } from '../hooks/useRemoteServers' +import { useDomains } from '../hooks/useDomains' import { useDocker } from '../hooks/useDocker' interface ProxyHostFormProps { @@ -27,7 +28,9 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor }) const { servers: remoteServers } = useRemoteServers() - const [connectionSource, setConnectionSource] = useState<'local' | string>('local') + const { domains } = useDomains() + const [connectionSource, setConnectionSource] = useState<'local' | 'custom' | string>('custom') + const [selectedDomain, setSelectedDomain] = useState('') // Fetch containers based on selected source // If 'local', host is undefined (which defaults to local socket in backend) @@ -39,6 +42,7 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor const getDockerHostString = () => { if (connectionSource === 'local') return undefined; + if (connectionSource === 'custom') return undefined; const server = remoteServers.find(s => s.uuid === connectionSource); if (!server) return undefined; // Construct the Docker host string @@ -63,18 +67,6 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor } } - const handleServerSelect = (serverUuid: string) => { - const server = remoteServers.find(s => s.uuid === serverUuid) - if (server) { - setFormData({ - ...formData, - forward_host: server.host, - forward_port: server.port, - forward_scheme: 'http', - }) - } - } - const handleContainerSelect = (containerId: string) => { const container = dockerContainers.find(c => c.id === containerId) if (container) { @@ -83,11 +75,18 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor // Use the first exposed port if available, otherwise default to 80 const port = container.ports && container.ports.length > 0 ? container.ports[0].private_port : 80 + let newDomainNames = formData.domain_names + if (selectedDomain) { + const subdomain = container.names[0].replace(/^\//, '') + newDomainNames = `${subdomain}.${selectedDomain}` + } + setFormData({ ...formData, forward_host: host, forward_port: port, forward_scheme: 'http', + domain_names: newDomainNames, }) } } @@ -109,42 +108,43 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor )} {/* Domain Names */} -
- - setFormData({ ...formData, domain_names: e.target.value })} - placeholder="example.com, www.example.com" - className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500" - /> -
- -
- {/* Remote Server Quick Select */} - {remoteServers.length > 0 && ( +
+ {domains.length > 0 && (
-
)} +
+ + setFormData({ ...formData, domain_names: e.target.value })} + placeholder="example.com, www.example.com" + className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+
+
{/* Docker Container Quick Select */}